Bulk Uploading Redirects In Drupal 7

Had a client today with a list of 135ish redirects that needed entered, and a decent enough budget for me to do more than shove them in. I put together an admin page that allows you to upload a csv to be processed containing an internal source, destination, and redirect type (status code). Of course, this does require Drupal (7), and the redirect module.

But, if you add the pieces of this below to any given site, you'll have a nice page in the redirects section that will allow you to upload an easy csv. Here's the description it produces:

Upload a file of source and destination redirects mappings to bulk create redirects.

File
Files must be less than 2 MB.
Allowed file extentions: csv.
My file contains a header row.
Column Descriptions

Column 1: Source
Note that any existing paths will be changed to the new target. Source paths must be entered WITHOUT leading slashes.
Column 2: Destination
Note that any existing paths will be changed to the new target. Internal destination should be entered WITHOUT leading slashes.
Column 3: HTTP Status Code Number
Your options are:

300 Multiple Choices
301 Moved Permanently
302 Found
303 See Other
304 Not Modified
305 Use Proxy
307 Temporary Redirect

  1. /**
  2.  * Impelements hook_menu
  3.  */
  4. function custom_menu(){
  5.   $items = array();
  6.  
  7.   if( module_exists('redirect') ){
  8.     $items['admin/config/search/redirect/upload'] = array(
  9.       'type' => MENU_LOCAL_TASK,
  10.       'title' => 'Upload',
  11.       'page callback' => '_custom_admin_redirect_upload',
  12.       'description' => 'Allows users to upload a bulk csv of redirects to be created.',
  13.       'access arguments' => array('administer redirects'),
  14.       'file' => 'custom.admin.inc',
  15.     );
  16.   }
  17.    
  18.   return $items;
  19. }
  20.  
  21. /**
  22.  * Vendor Upload Page
  23.  */
  24.  
  25. function _custom_admin_redirect_upload(){
  26.   $page = array();
  27.  
  28.   $page['form'] = array(
  29.     '#weight' => 5,
  30.     'definition' => drupal_get_form('custom_admin_redirect_upload_form'),
  31.   );
  32.  
  33.   return $page;
  34. }
  35.  
  36. function custom_admin_redirect_upload_form($form_state){
  37.   $form = array();
  38.  
  39.   $form['#attributes'] = array('enctype' => "multipart/form-data");
  40.  
  41.   $form['directions'] = array(
  42.     '#markup' =>
  43.       '<p>' . t('Upload a file of source and destination redirects mappings to bulk create redirects.') . '</p>'
  44.   );
  45.  
  46.   $form['csv'] = array(
  47.     '#type' => 'file',
  48.     '#title' => t('File'),
  49.     '#size' => 40,
  50.     '#description' => implode('<br />', array(
  51.       t('Files must be less than !size.', array('!size' => '<strong>' . format_size( parse_size(file_upload_max_size()) ) . '</strong>') ),
  52.       t('Allowed file extentions: !list.', array('!list' => '<strong>csv</strong>') ),
  53.     )),
  54.   );
  55.  
  56.   $form['includes_header'] = array(
  57.     '#type' => 'checkbox',
  58.     '#default_value' => 1,
  59.     '#title' => t('My file contains a header row.'),
  60.     '#options' => array(
  61.       1 => t('Yes'),
  62.       0 => t('No'),
  63.     ),
  64.   );
  65.  
  66.   $form['column_description'] = array(
  67.     '#prefix' => '<h2>' . t('Column Descriptions') . '</h2>',
  68.   );
  69.   $form['column_description']['columns'] = array(
  70.     '#prefix' => '<dl>',
  71.     '#suffix' => '</dl>',
  72.   );
  73.   $form['column_description']['columns'][] = array(
  74.     '#prefix' => '<dt>' . t('Column 1') . ': ' . t('Source') . '</dt>',
  75.     '#markup' => '<dd>' . t(
  76.       'Note that any existing paths will be changed to the new target.'
  77.       . ' Source paths must be entered WITHOUT leading slashes.'
  78.     ) . '</dd>',
  79.   );
  80.   $form['column_description']['columns'][] = array(
  81.     '#prefix' => '<dt>' . t('Column 2') . ': ' . t('Destination') . '</dt>',
  82.     '#markup' => '<dd>' . t(
  83.       'Note that any existing paths will be changed to the new target.'
  84.       . ' Internal destination should be entered WITHOUT leading slashes.'
  85.     ) . '</dd>',
  86.   );
  87.   $form['column_description']['columns'][] = array(
  88.     '#prefix' => '<dt>' . t('Column 3') . ': ' . t('HTTP Status Code Number') . '</dt>',
  89.     '#markup' =>
  90.       '<p>' . t('Your options are:') . '</p>'
  91.       . '<ul><li>' . implode('</li><li>', redirect_status_code_options()) . '</li></ul>',
  92.   );
  93.  
  94.   $form['import']['submit'] = array(
  95.     '#type' => 'submit',
  96.     '#value' => t('Submit'),
  97.   );
  98.  
  99.   return $form;
  100. }
  101.  
  102. function custom_admin_redirect_upload_form_validate($form, &$form_state){
  103.   // Validate file
  104.   $validators = array(
  105.     'file_validate_name_length' => array(),
  106.     'file_validate_extensions' => array('csv'),
  107.   );
  108.   $file = file_save_upload('csv', $validators);
  109.   if (!$file){
  110.     form_set_error('upload', 'You must select a valid file to upload.');
  111.   } else {
  112.     // Manually add the uploaded file to the $form_state
  113.     $form_state['values']['csv']['title'] = $file->filename;
  114.     $form_state['values']['csv']['file'] = $file;
  115.     $form_state['values']['csv']['file']->filesize = $file->filesize;
  116.   }
  117. }
  118.  
  119. function custom_admin_redirect_upload_form_submit($form, &$form_state){
  120.   // Create a batch
  121.   $parameters = array();
  122.   $batch = array(
  123.     'title' => t('Scanning Vendors'),
  124.     'operations' => array(
  125.       array(
  126.         '_custom_redirect_csv_batch', array(
  127.           $parameters,
  128.           array(
  129.             'path' => $form_state['values']['csv']['file']->uri,
  130.             'size' => $form_state['values']['csv']['file']->filesize,
  131.             'includes_header' => $form_state['values']['includes_header'],
  132.           ),
  133.         ),
  134.       ),
  135.     ),
  136.     'finished' => '_custom_redirect_csv_batch_finished',
  137.     'file' => drupal_get_path('module', 'custom') . '/custom.admin.inc',
  138.   );
  139.   batch_set($batch);
  140. }
  141.  
  142. /**
  143.  * Batch callbacks
  144.  */
  145.  
  146. /**
  147.  * Foreground based csv batch
  148.  * @param array $parameters
  149.  * @param array $file_info
  150.  * @param array $context
  151.  */
  152. function _custom_redirect_csv_batch($parameters, $file_info, &$context){
  153.   global $user;
  154.  
  155.   // open the file for reading
  156.   $file_handle = fopen($file_info['path'], 'r');
  157.  
  158.   if( empty($context['sandbox']['initialized']) ){
  159.     $context['results']['parameters'] = $parameters;
  160.     $context['results']['total'] = 0;
  161.     $context['results']['errors'] = array();
  162.  
  163.     if( $file_info['includes_header'] ){
  164.       // get file line as csv to skip the header
  165.       $csv_line = fgetcsv($file_handle);
  166.       // retain current file pointer position
  167.       $context['sandbox']['file_pointer_position'] = ftell($file_handle);
  168.     } else {
  169.       $context['sandbox']['file_pointer_position'] = 0;
  170.     }
  171.  
  172.     $context['finished'] = .01;
  173.     $context['message'] = t('Initialized');
  174.     $context['sandbox']['initialized'] = TRUE;
  175.  
  176.     if( variable_get('development', FALSE) ){
  177.       watchdog(
  178.         'custom'
  179.         ,'Bulk redirect csv upload started by user %uid with %size file size.'
  180.         ,array(
  181.           '%uid' => $user->uid,
  182.           '%size' => $file_info['size'],
  183.         )
  184.         ,WATCHDOG_NOTICE
  185.       );
  186.     }
  187.  
  188.     return;
  189.   }
  190.  
  191.   // Jump to location in file
  192.   fseek($file_handle, $context['sandbox']['file_pointer_position']);
  193.  
  194.   // loop through the file and stop at batch limit
  195.   for ($i = 0; $i < 50; $i++) {
  196.     // get file line as csv
  197.     $csv_line = fgetcsv($file_handle);
  198.  
  199.     // retain current file pointer position
  200.     $context['sandbox']['file_pointer_position'] = ftell($file_handle);
  201.  
  202.     if(!$csv_line){
  203.       break;
  204.     }
  205.  
  206.     // Clean and name the variables for sanity
  207.     $source = trim($csv_line[0]);
  208.     $destination = trim($csv_line[1]);
  209.     $http_status_code = trim($csv_line[2]);
  210.  
  211.     // Ensure the source is a valid path
  212.     if( stripos($source, 'http') !== FALSE || substr($source, 0, 1) == '/' ){
  213.       $context['results']['errors'][] = t('The source path %value is not valid.', array('%value' => $source));
  214.       continue;
  215.     }
  216.  
  217.     // Normalize the path.
  218.     $destination = drupal_get_normal_path($destination);
  219.     if (!valid_url($destination) && !valid_url($destination, TRUE) && $destination != '<front>' && $destination != '') {
  220.       $context['results']['errors'][] = t('The redirect path %value is not valid.', array('%value' => $destination));
  221.       continue;
  222.     }
  223.  
  224.     // Check if a redirect already exists somehow
  225.     $redirect = redirect_load_by_source($source);
  226.     if($redirect){
  227.       // Update
  228.       $redirect->redirect = $destination;
  229.       $redirect->status_code = $http_status_code;
  230.     } else {
  231.       // Create
  232.       $redirect = new stdClass();
  233.       redirect_object_prepare($redirect);
  234.       $redirect->source = $source;
  235.       $redirect->redirect = $destination;
  236.       $redirect->status_code = $http_status_code;
  237.     }
  238.     redirect_save($redirect);
  239.  
  240.     $context['results']['total']++;
  241.   }
  242.  
  243.   // check for EOF
  244.   if ( feof($file_handle) ) {
  245.     // complete the batch process
  246.     $context['finished'] = 1;
  247.     $context['message'] = t('All redirects processed.');
  248.   } else {
  249.     // Inform the batch engine that we are not finished,
  250.     // and provide an estimation of the completion level we reached.
  251.     $context['finished'] = $context['sandbox']['file_pointer_position'] / $file_info['size'];
  252.     $context['message'] = t('Processed %number redirects.', array('%number' => $context['results']['total']) );
  253.   }
  254.  
  255.   fclose($file_handle);
  256. }
  257.  
  258. /**
  259.  * Foreground csv batch finished
  260.  * @param boolean $success
  261.  * @param array $results
  262.  * @param array $operations
  263.  */
  264. function _custom_redirect_csv_batch_finished($success, $results, $operations){
  265.   global $user;
  266.   if( variable_get('development', FALSE) ){
  267.     watchdog(
  268.     'custom'
  269.     ,'Bulk redirect csv upload completed by user %user.'
  270.     ,array(
  271.       '%user' => $user->uid,
  272.     )
  273.     ,WATCHDOG_NOTICE
  274.     );
  275.   }
  276.  
  277.   // The 'success' parameter means no fatal PHP errors were detected. All
  278.   // other error management should be handled using 'results'.
  279.   if (!$success) {
  280.     drupal_set_message( t('A fatal error occured during import. The batch stopped processing.'), 'error');
  281.   } else {
  282.     drupal_set_message( t('Import complete. Total records processed: %total.', array(
  283.       '%total' => $results['total'],
  284.     )));
  285.     if( !empty($results['errors']) ){
  286.       drupal_set_message( t('The following %number occured during processing: !errors', array(
  287.         '!errors' => '<ol><li>' . implode('</li><li>', $results['errors']) . '</li></ol>',
  288.         '%number' => count($results['errors']),
  289.       )), 'error');
  290.     }
  291.     drupal_goto('admin/config/search/redirect');
  292.   }
  293. }
Tags: 

Comments

There is also a module for this: Path redirect import (https://www.drupal.org/project/path_redirect_import).

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.