scanner.module 44.2 KB
Newer Older
1
2
3
4
<?php
/**
 * @file
 * Search and Replace Scanner - works on all nodes text content.
5
6
7
8
9
10
 *
 * The Search and Replace Scanner can do regular expression matches
 * against the title, body and CCK text content fields on all nodes in your system.
 * This is useful for finding html strings that Drupal's normal search will
 * ignore. And it can replace the matched text. Very handy if you are changing
 * the name of your company, or are changing the URL of a link included
Tao Starbow's avatar
Tao Starbow committed
11
 * multiple times in multiple nodes.
12
13
 *
 * The module allow you to configure which fields and tables to work with,
Tao Starbow's avatar
Tao Starbow committed
14
 * and also to add in custom tables and fields for modules that don't use CCK.
15
 *
Tao Starbow's avatar
Tao Starbow committed
16
17
 * Limitations:
 *  Only works with Mysql
18
 *
Tao Starbow's avatar
Tao Starbow committed
19
 * Warning:
20
 *  This is a very powerful tool, and as such is very dangerous.  You can
Tao Starbow's avatar
Tao Starbow committed
21
22
 *  easy distroy your entire site with it.  Be sure to backup your database
 *  before using it.  No, really.
23
 *
Tao Starbow's avatar
Tao Starbow committed
24
 * Todo:
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 *  Provide better highlighting for search results
 *   - right now there's a known bug where multiple search terms
 *     on the same line aren't all highlighted. (The hit count
 *     is correct, though, and all items are replaced correctly.)
 *
 * Credits:
 *  Version 5.x-1.0 by:
 *   - Tao Starbow http://www.starbowconsulting.com
 *     Drupal username: starbow
 *  Version 5.x-2.0 by:
 *   - Amit Asaravala http://www.returncontrol.com
 *     Drupal username: aasarava
 *   - Jason Salter jason http://www.fivepaths.com
 *     Drupal username: jpsalter
 *   - Sponsored by Five Paths Consulting http://www.fivepaths.com
40
41
42
 *  Version 7.x-1.0 by:
 *   - Michael Rossetti - http://www.mit.edu
 *     Drupal username: MikeyR
43
44
 */

45
46
47
48
49
50
51
52
53
54
55
56
57

//The special characters to escape if a search string is not a regex string:
define('SCANNER_REGEX_CHARS', '.\/+*?[^]$() {}=!<>|:');

//The modes that the search-and-replace process can be in.
//We need to track the modes to prevent accidentally starting a replacement
// or a long search if a user leaves mid-way through the process
// and comes back again w/ the same session variables.
define('SCANNER_STATUS_GO_SEARCH', 1);
define('SCANNER_STATUS_GO_CONFIRM', 2);
define('SCANNER_STATUS_GO_REPLACE', 3);


58
/**
59
 * Implements hook_menu().
60
 */
61
function scanner_menu() {
62

63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
  $items['admin/content/scanner'] = array(
    'title' => 'Search and Replace Scanner',
    'description' => 'Find (and replace) keywords in all your content.',
    'page callback' => 'scanner_view',
    'access arguments' => array('perform search and replace'),
  );
  $items['admin/content/scanner/scan'] = array(
    'title' => 'Search',
    'access arguments' => array('perform search and replace'),
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/content/scanner/scan/confirm'] = array(
    'title' => 'Confirm Replace',
    'access arguments' => array('perform search and replace'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('scanner_confirm_form'),
    'type' => MENU_CALLBACK,
  );
  $items['admin/content/scanner/undo/confirm'] = array(
    'title' => 'Confirm Undo',
    'access arguments' => array('perform search and replace'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('scanner_undo_confirm_form'),
    'type' => MENU_CALLBACK,
  );
  $items['admin/content/scanner/settings'] = array(// Shows up on scanner page as tab.
    'title' => 'Settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('scanner_admin_form'),
    'access arguments' => array('administer scanner settings'),
    'type' => MENU_LOCAL_TASK,
    'weight' => 1,
  );
  $items['admin/content/scanner/undo'] = array(// Shows up on scanner page as tab.
    'title' => 'Undo',
    'page callback' => 'scanner_undo_page',
    'access arguments' => array('perform search and replace'),
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/config/scanner'] = array(// Shows up on admin page.
    'title' => 'Search and Replace Scanner',
    'description' => 'Configure defaults and what fields can be searched and replaced.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('scanner_admin_form'),
    'access arguments' => array('administer scanner settings'),
  );
109
110
111
  return $items;
}

112
113
114
115
/**
 * @todo Please document this function.
 * @see http://drupal.org/node/1354
 */
116
117
118
119
function scanner_theme() {
  return array(
    'scanner_results' => array(
      'file' => 'scanner.module',
120
      'variables' => array(
121
122
123
124
125
        'results' => NULL,
      ),
    ),
    'scanner_item' => array(
      'file' => 'scanner.module',
126
      'variables' => array(
127
128
129
130
131
        'item' => NULL,
      ),
    ),
    'scanner_replace_results' => array(
      'file' => 'scanner.module',
132
      'variables' => array(
133
134
135
136
137
        'results' => NULL,
      ),
    ),
    'scanner_replace_item' => array(
      'file' => 'scanner.module',
138
      'variables' => array(
139
140
141
142
143
144
145
        'item' => NULL,
      ),
    ),
  );
}


146
/**
147
 * Implements hook_permission().
148
 */
149
150
151
152
153
154
155
156
157
158
159
function scanner_permission() {
  return array(
    'administer scanner settings' => array(
      'title' => t('administer scanner settings'),
      'description' => t('TODO Add a description for \'administer scanner settings\''),
    ),
    'perform search and replace' => array(
      'title' => t('perform search and replace'),
      'description' => t('TODO Add a description for \'perform search and replace\''),
    ),
  );
160
161
}

162
163
164
165
/**
 * Menu callback; presents the scan form and results.
 */
function scanner_view() {
166
167
168
    
  $output='';
  
169
170
  //using set_html_head because it seems unecessary to load a separate css
  // file for just two simple declarations:
171
  drupal_add_css('
172
173
      #scanner-form .form-submit { margin-top:0; }
      #scanner-form .form-item { margin-bottom:0; }
174
    ', array('type' => 'inline'));
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192

  //javascript checks to make sure user has entered some search text:
  drupal_add_js("
    $(document).ready(function() {
      $('input[@type=submit][@value=Search]').click(function() {
        var searchfield = $('#edit-search');
        var chars = searchfield.val().length;
        if (chars == 0) {
          alert('Please provide some search text and try again.');
          searchfield.addClass('error');
          searchfield[0].focus();
          return FALSE;
        } else if (chars < 3) {
          return confirm('Searching for a keyword that has fewer than three characters could take a long time. Are you sure you want to continue?');
        }
        return TRUE;
      });
    });
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
  ", array('type' => 'inline', 'scope' => JS_DEFAULT));
  
  if (isset($_SESSION['scanner_search'])) {
    $search = $_SESSION['scanner_search'];
  }
  else{
    $search = NULL;
  }
  
  if (isset($_SESSION['scanner_status'])) {
    $status = $_SESSION['scanner_status'];
  }
  else{
      $status = NULL;
  }
  
209
210
211
212
213
214
  if (!is_NULL($search) && $status >= SCANNER_STATUS_GO_SEARCH) {

    if ($status == SCANNER_STATUS_GO_CONFIRM) {
      drupal_goto('admin/content/scanner/scan/confirm');

    }
215
216
    elseif ($status == SCANNER_STATUS_GO_REPLACE) {
      $resulttxt = '<a name="results"></a>' . t('Replacement Results');
217
218
      $results = scanner_execute('replace');

219
220
    }
    else {
221
222
      $resulttxt = t('Search Results');
      $results = scanner_execute('search');
223
    }
224

225
    if ($results) {
226
227
      // TODO Please change this theme call to use an associative array for the $variables parameter.
      $results = '<a name="results"></a><div><h2>' . $resulttxt . '</h2>' . $results;
228
229
    }
    else {
230
231
      // TODO Please change this theme call to use an associative array for the $variables parameter.
      $results = t('Your search yielded no results.');
232
    }
233
234
    
    $output = drupal_render(drupal_get_form('scanner_form'));
235
236
    $output .= $results;

237
238
239
240
241
242
243
244
245
246
247
248
249
    //clear any old search form input:
    unset($_SESSION['scanner_search']);
    unset($_SESSION['scanner_replace']);
    unset($_SESSION['scanner_preceded']);
    unset($_SESSION['scanner_followed']);
    unset($_SESSION['scanner_mode']);
    unset($_SESSION['scanner_wholeword']);
    unset($_SESSION['scanner_published']);
    unset($_SESSION['scanner_regex']);
    unset($_SESSION['scanner_terms']);
    //clear old status:
    unset($_SESSION['scanner_status']);

250
251
    return $output;
  }
252
253
254
255
   
  $output .= drupal_render(drupal_get_form('scanner_form'));
  
  return $output;
256
257
258
259
260
261
262
263
264
}

/**
 * The search and replace form.
 *
 * @param str $search - regex to search for.
 * @param str $replace - string to substitute.
 * @return $form
 */
265
266
function scanner_form($node, &$form_state) {
    
267
  $form = array();
268

269
  if (isset($_SESSION['scanner_search'])) {
270
  $search = $_SESSION['scanner_search'];
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
  }
  else{
      $search = NULL;
  }
  
  if (isset($_SESSION['scanner_replace'])) {
  $replace = $_SESSION['scanner_replace'];   
  }
  else{
      $replace = NULL;
  }
  if (isset($_SESSION['scanner_preceded'])) {
    $preceded = $_SESSION['scanner_preceded'];
  }
  else{
      $preceded = NULL;
  }
  
  if (isset($_SESSION['scanner_followed'])) {
    $followed = $_SESSION['scanner_followed'];
  }
  else{
      $followed = NULL;
  }
  
296
297
298
299
  $mode = isset($_SESSION['scanner_mode']) ? $_SESSION['scanner_mode'] : variable_get('scanner_mode', 0);
  $wholeword = isset($_SESSION['scanner_wholeword']) ? $_SESSION['scanner_wholeword'] : variable_get('scanner_wholeword', 0);
  $published = isset($_SESSION['scanner_published']) ? $_SESSION['scanner_published'] : variable_get('scanner_published', 1);
  $regex = isset($_SESSION['scanner_regex']) ? $_SESSION['scanner_regex'] : variable_get('scanner_regex', 0);
300
301
302
303
304
305
306
307
  
  if (isset($_SESSION['scanner_terms'])) {
    $terms = $_SESSION['scanner_terms'];
  }
  else{
      $terms = NULL;
  }
  
308
  $form['search'] = array(
309
310
    '#type' => 'textfield',
    '#default_value' => $search,
311
312
    '#title' => t('Step 1: Search for'),
    '#maxlength' => 256,
313
  );
314
315
316
  $form['submit_search'] = array(
    '#type' => 'submit',
    '#value' => t('Search'),
317
318
  );

319
320
321
322
323
  $form['replace'] = array(
    '#type' => 'textfield',
    '#default_value' => $replace,
    '#title' => t('Step 2: Replace with'),
    '#maxlength' => 256,
324
325
326
327
  );
  $form['submit_replace'] = array(
    '#type' => 'submit',
    '#value' => t('Replace'),
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
  );

  $form['options'] = array(
    '#type' => 'fieldset',
    '#title' => t('Search Options'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );

  $form['options']['surrounding'] = array(
    '#type' => 'fieldset',
    '#title' => t('Surrounding Text'),
    '#collapsible' => FALSE,
    '#description' => t('You can limit matches by providing the text that should appear immediately before or after the search text. Remember to account for spaces.  Note: Case sensitivity and regular expression options will all apply here, too. Whole word is not recommended.'),
  );

  $form['options']['surrounding']['preceded'] = array(
    '#type' => 'textfield',
    '#title' => t('Preceded by'),
    '#default_value' => $preceded,
    '#maxlength' => 256,
  );

  /* TODO: for possible future implementation...
   * Depends on whether negative lookahead and negative lookbehind
   *  can accurately be approximated in MySQL...
354
355
356
357
358
359
   $form['options']['surrounding']['notpreceded'] = array(
   '#type' => 'checkbox',
   '#title' => t('NOT preceded by the text above'),
   '#default_value' => $notpreceded,
   );
   */
360
361
362
363
364
365
366
367
368
369
370

  $form['options']['surrounding']['followed'] = array(
    '#type' => 'textfield',
    '#title' => t('Followed by'),
    '#default_value' => $followed,
    '#maxlength' => 256,
  );

  /* TODO: for possible future implementation...
   * Depends on whether negative lookahead and negative lookbehind
   *  can accurately be approximated in MySQL...
371
372
373
374
375
376
   $form['options']['surrounding']['notfollowed'] = array(
   '#type' => 'checkbox',
   '#title' => t('NOT followed by the text above'),
   '#default_value' => $notfollowed,
   );
   */
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406

  $form['options']['mode'] = array(
    '#type' => 'checkbox',
    '#title' => t('Case sensitive search'),
    '#default_value' => $mode,
    '#description' => t("Check this if the search should only return results that exactly match the capitalization of your search terms."),
  );

  $form['options']['wholeword'] = array(
    '#type' => 'checkbox',
    '#title' => t('Match whole word'),
    '#default_value' => $wholeword,
    '#description' => t("Check this if you don't want the search to match any partial words. For instance, if you search for 'run', a whole word search will <em>not</em> match 'running'."),
  );

  $form['options']['regex'] = array(
    '#type' => 'checkbox',
    '#title' => t('Use regular expressions in search'),
    '#default_value' => $regex,
    '#description' => t('Check this if you want to use regular expressions in your search terms.'),
  );

  $form['options']['published'] = array(
    '#type' => 'checkbox',
    '#title' => t('Published nodes only'),
    '#default_value' => $published,
    '#description' => t('Check this if you only want your search and replace to affect fields in nodes that are published.'),
  );

  $scanner_vocabularies = array_filter(variable_get('scanner_vocabulary', array()));
407
  
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433

  if (count($scanner_vocabularies)) {

    $vocabularies = taxonomy_get_vocabularies();
    $options = array();

    foreach ($vocabularies as $vid => $vocabulary) {
      if (in_array($vid, $scanner_vocabularies) ) {
        $tree = taxonomy_get_tree($vid);
        if ($tree && (count($tree) > 0)) {
          $options[$vocabulary->name] = array();
          foreach ($tree as $term) {
            $options[$vocabulary->name][$term->tid] = str_repeat('-', $term->depth) . $term->name;
          }
        }
      }
    }

    $form['options']['terms'] = array(
      '#type' => 'select',
      '#title' => t('Only match nodes with these terms'),
      '#options' => $options,
      '#default_value' => $terms,
      '#multiple' => TRUE,
    );
  }
434
  
435
436
437
438
  return $form;
}

/**
439
440
 * Validate form input.
 */
441
442
function scanner_form_validate($form, &$form_state) {
  $search = trim($form_state['values']['search']);
443
444
445
446
447
448
449
  if ($search == '') {
    form_set_error('search', t('Please enter some keywords.'));
  }
}

/**
 * Handles submission of the search and replace form.
450
 *
451
452
 * @param $form
 * @param $form_state
453
454
 * @return the new path that will be goto'ed.
 */
455
function scanner_form_submit($form, &$form_state) {
456
  //save form input:
457

458
459
460
461
462
463
464
465
466
  $_SESSION['scanner_search']    = $form_state['values']['search'];
  $_SESSION['scanner_preceded']  = $form_state['values']['preceded'];
  //$_SESSION['scanner_notpreceded'] = $form_state['values']['notpreceded'];
  $_SESSION['scanner_followed']  = $form_state['values']['followed'];
  //$_SESSION['scanner_notfollowed'] = $form_state['values']['notfollowed'];
  $_SESSION['scanner_mode']      = $form_state['values']['mode'];
  $_SESSION['scanner_wholeword'] = $form_state['values']['wholeword'];
  $_SESSION['scanner_published'] = $form_state['values']['published'];
  $_SESSION['scanner_regex']     = $form_state['values']['regex'];
467
  if (isset($form_state['values']['terms'])) {
468
  $_SESSION['scanner_terms']     = $form_state['values']['terms'];
469
  }
470
471
  $_SESSION['scanner_replace']   = $form_state['values']['replace'];

472
  /* TODO The 'op' element in the form values is deprecated.
473
474
475
476
477
478
   Each button can have #validate and #submit functions associated with it.
   Thus, there should be one button that submits the form and which invokes
   the normal form_id_validate and form_id_submit handlers. Any additional
   buttons which need to invoke different validate or submit functionality
   should have button-specific functions. */
  if ($form_state['values']['op'] == 'Replace') {
479
480
481
482
    $_SESSION['scanner_status'] = SCANNER_STATUS_GO_CONFIRM;
  }
  else {
    $_SESSION['scanner_status'] = SCANNER_STATUS_GO_SEARCH;
483
  }
484
  $form_state['redirect'] = 'admin/content/scanner';
485
486
487
488
489
490
491
}


/**
 * Scanner confirmation form to prevent people from accidentally
 * replacing things they don't intend to.
 */
492
function scanner_confirm_form($form, &$form_state) {
493
494
495
496
  //using set_html_head because it seems unecessary to load a separate css
  // file for just one declaration:
  //you can override the styles by declaring with something "higher up"
  // the chain, like:  #wrapper #scanner-confirm-form .scanner-buttons .scanner-button-msg {...}
497
  drupal_add_css('
498
499
500
501
502
503
504
505
506
507
      #scanner-confirm-form .scanner-buttons .scanner-button-msg {
        position:absolute;
        top:0; left:0; z-index:100;
        width:100%; height:100%;
        background-color:#000; opacity:0.75;
        font-size:1.2em;
      }
      #scanner-confirm-form .scanner-buttons .scanner-button-msg p {
        color:#fff;
      }
508
    ', array('type' => 'inline'));
509
510
511
512
513
514
515
516
517
518
519
520
521

  //javascript to prevent further clicks on confirmation button after it's clicked once.
  //unfortunately we can't just use css disable to disable the button because then
  // the op values aren't sent to drupal correctly.
  drupal_add_js("
    $(document).ready(function() {
      $('input[@type=submit][@value=Yes, Continue]').click(function() {
        $('.scanner-buttons').css('position','relative')
          .append('<div class=\"scanner-button-msg\"><p>Replacing items... please wait...</p></div>')
        $('.scanner-button-msg').click(function() { return false; });
        return true;
      });
    });
522
  ", array('type' => 'inline', 'scope' => JS_DEFAULT));
523

524
525
526
527
528
529
530
531
532
533
534
535
536
  $form = array();

  $search    = $_SESSION['scanner_search'];
  $replace   = $_SESSION['scanner_replace'];
  $preceded  = $_SESSION['scanner_preceded'];
  $followed  = $_SESSION['scanner_followed'];
  $wholeword = $_SESSION['scanner_wholeword'];
  $regex     = $_SESSION['scanner_regex'];
  $mode      = $_SESSION['scanner_mode'];

  $modetxt = ($mode) ? t('Case sensitive') : t('Not case sensitive: will replace any matches regardless of capitalization.');

  $msg = (
537
538
539
    '<p>' . t('Are you sure you want to make the following replacement?') . '</p>' .
    '<div class="scanner-confirm">' .
    '  <label>' . t('Search for') . ':</label> [' . check_plain($search) . ']' .
540
541
542
543
    '</div>'
  );
  if ($preceded) {
    $msg .= (
544
545
      '<div class="scanner-confirm">' .
      '  <label>' . t('Preceded by') . ':</label> [' . check_plain($preceded) . ']' .
546
547
548
549
550
      '</div>'
    );
  }
  if ($followed) {
    $msg .= (
551
552
      '<div class="scanner-confirm">' .
      '  <label>' . t('Followed by') . ':</label> [' . check_plain($followed) . ']' .
553
554
555
556
      '</div>'
    );
  }
  $msg .= (
557
558
    '<div class="scanner-confirm">' .
    '  <label>' . t('Replace with') . ':</label> [' . check_plain($replace) . ']'
559
560
561
562
563
  );
  if ($replace === '') {
    $msg .= ' <span class="warning">This will delete any occurences of the search terms!</span>';
  }
  $msg .= (
564
565
566
    '</div>' .
    '<div class="scanner-confirm">' .
    '  <label>' . t('Mode') . ':</label> ' . $modetxt .
567
568
569
570
    '</div>'
  );
  if ($wholeword) {
    $msg .= (
571
572
      '<div class="scanner-confirm">' .
      '  <label>' . t('Match whole word') . ':</label> ' . t('Yes') .
573
574
575
576
577
      '</div>'
    );
  }
  if ($regex) {
    $msg .= (
578
579
      '<div class="scanner-confirm">' .
      '  <label>' . t('Use regular expressions') . ':</label> ' . t('Yes') .
580
581
582
583
584
      '</div>'
    );
  }

  $form['warning'] = array(
585
586
    '#type' => 'item',
    '#markup' => $msg,
587
588
589
590
591
  );

  $form['confirm'] = array(
    '#type' => 'submit',
    '#value' => t('Yes, Continue'),
592
    '#prefix' => '<div class="scanner-buttons">', //see suffix in cancel button element
593
594
595
596
  );
  $form['cancel'] = array(
    '#type' => 'submit',
    '#value' => t('No, Cancel'),
597
    '#suffix' => '</div>', //see prefix in confirm button element
598
  );
599
  
600
601
602
603
604
605
606
  return $form;
}


/**
 * Submission handling for scanner confirmation form.
 */
607
function scanner_confirm_form_submit($form, &$form_state) {
608
  /* TODO The 'op' element in the form values is deprecated.
609
610
611
612
613
614
   Each button can have #validate and #submit functions associated with it.
   Thus, there should be one button that submits the form and which invokes
   the normal form_id_validate and form_id_submit handlers. Any additional
   buttons which need to invoke different validate or submit functionality
   should have button-specific functions. */
  if ($form_state['values']['op'] == t('Yes, Continue')) {
615
616
617
618
619
620
    $_SESSION['scanner_status'] = SCANNER_STATUS_GO_REPLACE;
  }
  else {
    unset($_SESSION['scanner_status']);
  }

621
  $form_state['redirect'] = 'admin/content/scanner';
622
623
}

624
625
626
627
/**
 * @todo Please document this function.
 * @see http://drupal.org/node/1354
 */
628
629
630
function scanner_undo_page() {
  $header = array(t('Date'), t('Searched'), t('Replaced'), t('Count'), t('Operation'));

631
632
633
634
635
  $undoQuery = db_select('scanner', 's');
  $undoQuery->fields('s', array('undo_id', 'time', 'searched', 'replaced', 'count', 'undone'))
            ->orderBy('undo_id', 'DESC');
  
  $sandrs = $undoQuery->execute();
636

637
638
639
  $rows = array();
  
  foreach ($sandrs as $sandr) {
640
641

    if ($sandr->undone) {
642
      $operation = l(t('Redo'), 'admin/content/scanner/undo/confirm', array('query' => array('undo_id' => $sandr->undo_id)));
643
644
    }
    else {
645
      $operation = l(t('Undo'), 'admin/content/scanner/undo/confirm', array('query' => array('undo_id' => $sandr->undo_id)));
646
    }
647
648
649
650
651
652
653
654

    $rows[] = array(
      format_date($sandr->time),
      check_plain($sandr->searched),
      check_plain($sandr->replaced),
      $sandr->count,
      $operation,
    );
655
  }
656
657
 
  return theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => NULL, 'caption' => 'Prior Search and Replace Events'));
658
659
}

660
661
662
663
664
665
/**
 * @todo Please document this function.
 * @see http://drupal.org/node/1354
 */
function scanner_undo_confirm_form($form, &$form_state) {
       
666
667
668
  $undo_id = $_GET['undo_id'];

  if ($undo_id > 0) {
669
670
671
672
673
674
675
676
677
678
679
    
    $query = db_select('scanner', 's');
    $query->fields('s', array('undo_id', 'searched', 'replaced'))
          ->condition('undo_id', $undo_id, '=');
    
    $result = $query->execute();
    
    foreach ($result as $undo) {
        $undo = $undo;
    }
    
680
681
682
683
  }

  if ($undo->undo_id > 0) {
    $form['info'] = array(
684
685
686
687
688
      '#markup' => '<h2>' . t('Do you want to undo:') . '</h2>' .
                  '<h3>' . t('Searched for:') . '</h3>' .
                  '<p>[<em>' . check_plain($undo->searched) . '</em>]</p>' .
                  '<h3>' . t('Replaced with:') . '</h3>' .
                  '<p>[<em>' . check_plain($undo->replaced) . '</em>]</p>',
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
    );

    $form['undo_id'] = array(
      '#type' => 'hidden',
      '#value' => $undo->undo_id,
    );

    $form['confirm'] = array(
      '#type' => 'submit',
      '#value' => t('Yes, Continue'),
    );

    $form['cancel'] = array(
      '#type' => 'submit',
      '#value' => t('No, Cancel'),
    );

  }
  else {
    $form['info'] = array(
709
      '#value' => '<h2>' . t('No undo event was found') . '</h2>',
710
711
712
713
714
715
    );
  }

  return $form;
}

716
717
718
719
/**
 * @todo Please document this function.
 * @see http://drupal.org/node/1354
 */
720
function scanner_undo_confirm_form_submit($form, &$form_state) {
721

722
  /* TODO The 'op' element in the form values is deprecated.
723
724
725
726
727
728
   Each button can have #validate and #submit functions associated with it.
   Thus, there should be one button that submits the form and which invokes
   the normal form_id_validate and form_id_submit handlers. Any additional
   buttons which need to invoke different validate or submit functionality
   should have button-specific functions. */
  if ($form_state['values']['op'] == t('Yes, Continue')) {
729
730
731
732
733
734
735
736
737
738
739
    
    $query = db_select('scanner', 's');
    $query->fields('s', array('undo_data', 'undone'))
          ->condition('undo_id', $form_state['values']['undo_id'], '=');
    
    $results = $query->execute();
    
    foreach ($results as $undo) {
          $undo = $undo;
    }
    
740
    $undos = unserialize($undo->undo_data);
741
742
743
    
    $count = NULL;
    
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
    foreach ($undos as $nid => $sandr_event) {

      if ($undo->undone == 0) {
        $vid = $sandr_event['old_vid'];
        $undone = 1;
      }
      else {
        $vid = $sandr_event['new_vid'];
        $undone = 0;
      }

      $node = node_load($nid, $vid);

      $node->revision = TRUE;
      $node->log = t('Copy of the revision from %date via Search and Replace Undo', array('%date' => format_date($node->revision_timestamp)));

      node_save($node);
      ++$count;

    }

765
766
767
768
769
770
771
772
773
    drupal_set_message($count . ' ' . t('Nodes reverted'));
    // TODO Please review the conversion of this statement to the D7 database API syntax.

    db_update('scanner')
    ->fields(array(
      'undone' => $undone,
    ))
    ->condition('undo_id', $form_state['values']['undo_id'])
    ->execute();
774
775
776
777
778
779

  }
  else {
    drupal_set_message(t('Undo / Redo canceled'));
  }

780
781
  $form_state['redirect'] = 'admin/content/scanner/undo';
  $form_state['nid'] = $node->nid;
782
783
784
}

/**
785
 * Handles the actual search and replace.
786
 *
787
 * @param str $searchtype - either 'search', or 'replace'
788
789
 * @return The themed results.
 */
790
791
function scanner_execute($searchtype = 'search') {
  global $user;
792

793
794
  // variables to monitor possible timeout
  $max_execution_time = ini_get('max_execution_time');
795
  $start_time = REQUEST_TIME;
796
797
798
  $expanded = FALSE;

  // get process and undo data if saved from timeout
799
800
  $processed = variable_get('scanner_partially_processed_' . $user->uid, array());
  $undo_data = variable_get('scanner_partial_undo_' . $user->uid, array());
801

802
803
  unset($_SESSION['scanner_status']);
  $results = NULL;
804
805
806
807
808
809
810
811
812
813
  $search      = $_SESSION['scanner_search'];
  $replace     = $_SESSION['scanner_replace'];
  $preceded    = $_SESSION['scanner_preceded'];
  //$notpreceded = $_SESSION['scanner_notpreceded'];
  $followed    = $_SESSION['scanner_followed'];
  //$notfollowed = $_SESSION['scanner_notfollowed'];
  $mode        = $_SESSION['scanner_mode'];
  $wholeword   = $_SESSION['scanner_wholeword'];
  $published   = $_SESSION['scanner_published'];
  $regex       = $_SESSION['scanner_regex'];
814
  if (isset($_SESSION['scanner_terms'])) {
815
  $terms       = $_SESSION['scanner_terms'];
816
817
818
819
820
  }
  else{
      $terms = NULL;
  }
  
821
822
823
824
825
826
827
828
829
830
831
832
833
834
  if ($searchtype == 'search') {
    drupal_set_message(t('Scanning for: [%search] ...', array('%search' => $search)));
  }
  else { //searchtype == 'replace'
    drupal_set_message(t('Replacing [%search] with [%replace] ...', array('%search' => $search, '%replace' => $replace)));
  }

  if ($mode) { // Case Sensitive
    $flag = NULL;
  }
  else { // Case Insensitive
    $flag = 'i'; //ci flag for use in php preg_search and preg_replace
  }

835
  
836
837
838
839
840
  $preceded_php = '';
  if (!empty($preceded)) {
    if (!$regex) {
      $preceded = addcslashes($preceded, SCANNER_REGEX_CHARS);
    }
841
    $preceded_php = '(?<=' . $preceded . ')';
842
843
844
845
846
847
  }
  $followed_php = '';
  if (!empty($followed)) {
    if (!$regex) {
      $followed = addcslashes($followed, SCANNER_REGEX_CHARS);
    }
848
    $followed_php = '(?=' . $followed . ')';
849
850
851
852
  }

  //Case 1:
  if ($wholeword && $regex) {
853
    $where = "[[:<:]]" . $search . "[[:>:]]";
854
    $search_db = $preceded . $search . $followed;
855
    $search_php = '\b' . $preceded_php . $search . $followed_php . '\b';
856
857
  }
  //Case 2:
858
859
  elseif ($wholeword && !$regex) {
    $where = "[[:<:]]" . $search . "[[:>:]]";
860
    $search_db = $preceded . addcslashes($search, SCANNER_REGEX_CHARS) . $followed;
861
    $search_php = '\b' . $preceded_php . addcslashes($search, SCANNER_REGEX_CHARS) . $followed_php . '\b';
862
863
  }
  //Case 3:
864
865
  elseif (!$wholeword && $regex) {
    $where = $search;
866
867
868
869
870
    $search_db = $preceded . $search . $followed;
    $search_php = $preceded_php . $search . $followed_php;
  }
  //Case 4:
  else { //!wholeword and !regex:
871
    $where = $search;
872
873
874
875
876
877
878
879
880
881
882
883
884
    $search_db = $preceded . addcslashes($search, SCANNER_REGEX_CHARS) . $followed;
    $search_php = $preceded_php . addcslashes($search, SCANNER_REGEX_CHARS) . $followed_php;
  }

  //if terms selected, then put together extra join and where clause:
  $join = '';
  if (is_array($terms) && count($terms)) {
    $terms_where = array();
    $terms_params = array();
    foreach ($terms as $term) {
      $terms_where[] = 'tn.tid = %d';
      $terms_params[] = $term;
    }
885
886
    $join = 'INNER JOIN {taxonomy_term_node} tn ON t.nid = tn.nid';
    $where .= ' AND (' . implode(' OR ', $terms_where) . ')';
887
888
889
890
891
  }

  $tables_map = _scanner_get_selected_tables_map();

  foreach ( $tables_map as $map ) {
892
893
    $table = $map['table'];
    $field = $map['field'];
894
895
    $type  = $map['type'];

896
    $query_params = array($field, $table, $type, $field, $search_db);
897
898
899
900
    if (!empty($join)) {
      $query_params = array_merge($query_params, $terms_params);
    }

901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
    // TODO Please convert this statement to the D7 database API syntax.
    
    $query = db_select($table, 't');
    if ($table == 'node_revision') {
        $nid = 'nid';
        $vid = 'vid';
    }
    else{
        $field = $field . '_value';
        $nid = 'entity_id';
        $vid = 'revision_id';
    }
    $query->join('node', 'n', 't.' . $vid . ' = n.vid'); //Must use vid and revision_id here.  Make sure it saves as new revision.
    if (is_array($terms) && count($terms)) {
        $db_or = db_or();
        $query->join('taxonomy_index', 'tx', 't.' . $nid . ' = tx.nid');
        foreach ($terms as $term) {
            $db_or->condition('tx.tid', $term);
        }
        $query->condition($db_or);
    }
    $query->addField('t', $field, 'content');
    $query->fields('n', array('nid', 'title'));
    $query->condition('n.type', $type, '=');
    if ($mode) {
        $query->condition('t.' . $field, $search_db, 'REGEXP BINARY');
    }
    else{
        $query->condition('t.' . $field, $search_db, 'REGEXP');    
    }
    if ($published) {
        $query->condition('n.status', '1', '=');
    }
    $result = $query->execute();
935

936
937
938
    $shutting_down = FALSE;
    
    foreach ($result as $row) {
939
      $content = $row->content;
940
941
      $matches = array();
      $text = '';
942
      
943
944
      // checking for possible timeout
      // if within 5 seconds of timeout - attempt to expand environment
945
946
947

      
      if (REQUEST_TIME >= ($start_time + $max_execution_time - 5)) {
948
        if (!$expanded) {
949
950
951
952
953
          if ($user->uid > 0) {
            $verbose = TRUE;
          }
          else {
            $verbose = FALSE;
954
          }
955
956
957
          if (_scanner_change_env('max_execution_time', '600', $verbose)) {
            drupal_set_message(t('Default max_execution_time too small and changed to 10 minutes.'), 'error');
            $max_execution_time = 600;
958
          }
959
960
961
962
963
          $expanded = TRUE;
        }
        // if expanded environment still running out of time - shutdown process
        else {
          $shutting_down = TRUE;
964
965
          variable_set('scanner_partially_processed_' . $user->uid, $processed);
          variable_set('scanner_partial_undo_' . $user->uid, $undo_data);
966
967
          if ($searchtype == 'search') {
            drupal_set_message(t('Did not have enough time to complete search.'), 'error');
968
969
          }
          else {
970
            drupal_set_message(t('Did not have enough time to complete. Please re-submit replace'), 'error');
971
972
          }
          break 2;
973
974
        }
      }
975
976
977
978
979
980
981
982
983
984
985

      /*
       * SEARCH
       */
      if ($searchtype == 'search') {
        //pull out the terms and highlight them for display in search results:
        $regexstr = "/(.{0,130}?)($search_php)(.{0,130})/$flag";
        $hits = preg_match_all($regexstr, $content, $matches, PREG_SET_ORDER);
        if ($hits > 0) {
          foreach ( $matches as $match ) {
            if ( $match[1] ) {
986
              $text .= '...' . htmlentities($match[1], ENT_COMPAT, 'UTF-8');
987
            }
988
            $text .= '<strong>' . htmlentities($match[2], ENT_COMPAT, 'UTF-8') . '</strong>';
989
            if ( $match[3] ) {
990
              $text .= htmlentities($match[3], ENT_COMPAT, 'UTF-8') . '...';
991
992
993
994
            }
          }
        }
        else {
995
          $text = "<div class='warning'>" . t("Can't display search result due to conflict between search term and internal preg_match_all function.") . '</div>';
996
997
998
999
1000
        }

        $results[] = array(
          'title' => $row->title,
          'type' => $type,