scanner.module in Search and Replace Scanner 7
Same filename and directory in other branches
Search and Replace Scanner - works on all nodes text content.
File
scanner.moduleView source
<?php
/**
* @file
* Search and Replace Scanner - works on all nodes text content.
*/
// 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);
/**
* Implements hook_menu().
*/
function scanner_menu() {
$items['admin/content/scanner'] = array(
'title' => 'Search and Replace Scanner',
'description' => 'Find (and replace) keywords in all your content.',
'page callback' => 'scanner_view',
'type' => MENU_LOCAL_TASK | MENU_NORMAL_ITEM,
'access arguments' => array(
'perform search',
),
);
$items['admin/content/scanner/scan'] = array(
'title' => 'Search',
'access arguments' => array(
'perform search',
),
'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'] = array(
'title' => 'Undo',
'page callback' => 'scanner_undo_page',
'access arguments' => array(
'perform search and replace',
),
'type' => MENU_LOCAL_TASK,
);
$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/config/content/scanner'] = array(
'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',
),
'file' => 'scanner.admin.inc',
);
return $items;
}
/**
* Implements hook_theme().
*/
function scanner_theme() {
return array(
'scanner_results' => array(
'file' => 'scanner.theme.inc',
'variables' => array(
'results' => NULL,
),
),
'scanner_item' => array(
'file' => 'scanner.theme.inc',
'variables' => array(
'item' => NULL,
),
),
'scanner_replace_results' => array(
'file' => 'scanner.theme.inc',
'variables' => array(
'results' => NULL,
),
),
'scanner_replace_item' => array(
'file' => 'scanner.theme.inc',
'variables' => array(
'item' => NULL,
),
),
);
}
/**
* Implements hook_permission().
*/
function scanner_permission() {
return array(
'administer scanner settings' => array(
'title' => t('Administer scanner settings'),
),
'perform search and replace' => array(
'title' => t('Perform search and replace'),
),
'perform search' => array(
'title' => t('Perform search only'),
),
);
}
/**
* Menu callback; presents the scan form and results.
*/
function scanner_view() {
$output = '';
// Using set_html_head because it seems unecessary to load a separate css
// file for just two simple declarations.
drupal_add_css('
#scanner-form .form-submit { margin-top:0; }
#scanner-form .form-item { margin-bottom:0; }
', array(
'type' => 'inline',
));
// Javascript checks to make sure user has entered some search text.
drupal_add_js("\n jQuery(document).ready(function() {\n var searchfield = jQuery('#edit-search');\n jQuery('input[type=submit][value=" . str_replace('\'', '\\\'', t('Search')) . "]').click(function() {\n var chars = searchfield.val().length;\n if (chars == 0) {\n alert('" . str_replace('\'', '\\\'', t('Please provide some search text and try again.')) . "');\n searchfield.addClass('error');\n searchfield[0].focus();\n return false;\n } else if (chars < 3) {\n return confirm('" . str_replace('\'', '\\\'', t('Searching for a keyword that has fewer than three characters could take a long time. Are you sure you want to continue?')) . "');\n }\n return true;\n });\n searchfield.keyup(function() {\n searchfield.removeClass('error');\n });\n });\n ", array(
'type' => 'inline',
'group' => 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;
}
if (!is_null($search) && $status >= SCANNER_STATUS_GO_SEARCH) {
if ($status == SCANNER_STATUS_GO_CONFIRM) {
drupal_goto('admin/content/scanner/scan/confirm');
}
elseif ($status == SCANNER_STATUS_GO_REPLACE) {
$resulttxt = '<a name="results"></a>' . t('Replacement Results');
$results = scanner_execute('replace');
}
else {
$resulttxt = t('Search Results');
$results = scanner_execute('search');
}
// @todo Please change this theme call to use an associative array for
// the $variables parameter.
if ($results) {
$results = '<a name="results"></a><div><h2>' . $resulttxt . '</h2>' . $results;
}
else {
$results = t('Your search yielded no results.');
}
$scanner_form = drupal_get_form('scanner_form');
$output = drupal_render($scanner_form);
$output .= $results;
// 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_pathauto']);
unset($_SESSION['scanner_regex']);
unset($_SESSION['scanner_terms']);
// Clear old status.
unset($_SESSION['scanner_status']);
return $output;
}
$scanner_form = drupal_get_form('scanner_form');
$output = drupal_render($scanner_form);
return $output;
}
/**
* Form constructor for the search and replace form.
*
* @see scanner_form_validate()
* @see scanner_form_submit()
*
* @ingroup forms
*/
function scanner_form($form, &$form_state) {
$form = array();
if (isset($_SESSION['scanner_search'])) {
$search = $_SESSION['scanner_search'];
}
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;
}
$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);
$regex = isset($_SESSION['scanner_regex']) ? $_SESSION['scanner_regex'] : variable_get('scanner_regex', 0);
$published = isset($_SESSION['scanner_published']) ? $_SESSION['scanner_published'] : variable_get('scanner_published', 1);
$pathauto = isset($_SESSION['scanner_pathauto']) ? $_SESSION['scanner_pathauto'] : variable_get('scanner_pathauto', 1);
if (isset($_SESSION['scanner_terms'])) {
$terms = $_SESSION['scanner_terms'];
}
else {
$terms = NULL;
}
$form['settings_link'] = array(
'#markup' => t('The list of fields which will be searched and other options can be controlled from the !link.', array(
'!link' => l('settings page', 'admin/config/content/scanner'),
)),
'#prefix' => '<p>',
'#suffix' => '</p>',
'#access' => user_access('administer scanner settings'),
);
$form['search'] = array(
'#type' => 'textfield',
'#default_value' => $search,
'#title' => t('Step 1: Search for'),
'#maxlength' => 256,
);
$form['submit_search'] = array(
'#type' => 'submit',
'#value' => t('Search'),
);
$form['replace'] = array(
'#type' => 'textfield',
'#default_value' => $replace,
'#title' => t('Step 2: Replace with'),
'#maxlength' => 256,
'#access' => user_access('perform search and replace') ? TRUE : FALSE,
);
$form['submit_replace'] = array(
'#type' => 'submit',
'#value' => t('Replace'),
'#access' => user_access('perform search and replace') ? TRUE : FALSE,
);
$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,
);
$form['options']['surrounding']['followed'] = array(
'#type' => 'textfield',
'#title' => t('Followed by'),
'#default_value' => $followed,
'#maxlength' => 256,
);
$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.'),
);
$form['options']['pathauto'] = array(
'#type' => 'checkbox',
'#title' => t('Maintain custom aliases'),
'#default_value' => $pathauto,
'#description' => t("Prevent custom URL aliases from being overwritten with ones generated from Path Auto's URL alias patterns."),
);
$scanner_vocabularies = array_filter(variable_get('scanner_vocabulary', array()));
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,
);
}
return $form;
}
/**
* Form validation handler for scanner_form().
*
* @see scanner_form()
* @see scanner_form_submit()
*/
function scanner_form_validate($form, &$form_state) {
$search = trim($form_state['values']['search']);
if ($search == '') {
form_set_error('search', t('Please enter some keywords.'));
}
}
/**
* Form submission handler for scanner_form().
*
* @see scanner_form()
* @see scanner_form_validate()
*/
function scanner_form_submit($form, &$form_state) {
// Save form input into session.
$_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_regex'] = $form_state['values']['regex'];
$_SESSION['scanner_published'] = $form_state['values']['published'];
$_SESSION['scanner_pathauto'] = $form_state['values']['pathauto'];
if (isset($form_state['values']['terms'])) {
$_SESSION['scanner_terms'] = $form_state['values']['terms'];
}
$_SESSION['scanner_replace'] = $form_state['values']['replace'];
// @todo The 'op' element in the form values is deprecated.
// 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('Replace')) {
$_SESSION['scanner_status'] = SCANNER_STATUS_GO_CONFIRM;
}
else {
$_SESSION['scanner_status'] = SCANNER_STATUS_GO_SEARCH;
}
$form_state['redirect'] = 'admin/content/scanner';
}
/**
* Form constructor for the confirmation form.
*
* @see block_add_block_form_submit()
*
* @ingroup forms
*/
function scanner_confirm_form($form, &$form_state) {
$form = array();
$form['#attached']['js'][] = drupal_get_path('module', 'scanner') . '/scanner.js';
$form['#attached']['css'][] = drupal_get_path('module', 'scanner') . '/scanner.css';
$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 = '<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) . ']' . '</div>';
if ($preceded) {
$msg .= '<div class="scanner-confirm">' . ' <label>' . t('Preceded by') . ':</label> [' . check_plain($preceded) . ']' . '</div>';
}
if ($followed) {
$msg .= '<div class="scanner-confirm">' . ' <label>' . t('Followed by') . ':</label> [' . check_plain($followed) . ']' . '</div>';
}
$msg .= '<div class="scanner-confirm">' . ' <label>' . t('Replace with') . ':</label> [' . check_plain($replace) . ']';
if ($replace === '') {
$msg .= ' <span class="warning">This will delete any occurences of the search terms!</span>';
}
$msg .= '</div>' . '<div class="scanner-confirm">' . ' <label>' . t('Mode') . ':</label> ' . $modetxt . '</div>';
if ($wholeword) {
$msg .= '<div class="scanner-confirm">' . ' <label>' . t('Match whole word') . ':</label> ' . t('Yes') . '</div>';
}
if ($regex) {
$msg .= '<div class="scanner-confirm">' . ' <label>' . t('Use regular expressions') . ':</label> ' . t('Yes') . '</div>';
}
$form['warning'] = array(
'#type' => 'item',
'#markup' => $msg,
);
$form['confirm'] = array(
'#type' => 'submit',
'#value' => t('Yes, Continue'),
// see suffix in cancel button element.
'#prefix' => '<div class="scanner-buttons">',
);
$form['cancel'] = array(
'#type' => 'submit',
'#value' => t('No, Cancel'),
// see prefix in confirm button element.
'#suffix' => '</div>',
);
return $form;
}
/**
* Form submission handler for scanner_confirm_form().
*
* @see scanner_confirm_form()
*/
function scanner_confirm_form_submit($form, &$form_state) {
// @todo The 'op' element in the form values is deprecated.
// 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')) {
$_SESSION['scanner_status'] = SCANNER_STATUS_GO_REPLACE;
}
else {
unset($_SESSION['scanner_status']);
}
$form_state['redirect'] = 'admin/content/scanner';
}
/**
* Page callback to display table of executed replace actions with undo/redo operation.
*/
function scanner_undo_page() {
$header = array(
t('Date'),
t('Searched'),
t('Replaced'),
t('Count'),
t('Operation'),
);
$undoQuery = db_select('scanner', 's');
$undoQuery
->fields('s', array(
'undo_id',
'time',
'searched',
'replaced',
'count',
'undone',
))
->orderBy('undo_id', 'DESC');
$sandrs = $undoQuery
->execute();
$rows = array();
foreach ($sandrs as $sandr) {
if ($sandr->undone) {
$operation = l(t('Redo'), 'admin/content/scanner/undo/confirm', array(
'query' => array(
'undo_id' => $sandr->undo_id,
),
));
}
else {
$operation = l(t('Undo'), 'admin/content/scanner/undo/confirm', array(
'query' => array(
'undo_id' => $sandr->undo_id,
),
));
}
$rows[] = array(
format_date($sandr->time),
check_plain($sandr->searched),
check_plain($sandr->replaced),
$sandr->count,
$operation,
);
}
return theme('table', array(
'header' => $header,
'rows' => $rows,
'attributes' => NULL,
'caption' => 'Prior Search and Replace Events',
));
}
/**
* Form constructor for the undo confirmation form.
*
* @see scanner_undo_confirm_form_submit()
*
* @ingroup forms
*/
function scanner_undo_confirm_form($form, &$form_state) {
$undo_id = $_GET['undo_id'];
if ($undo_id > 0) {
$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;
}
}
if ($undo->undo_id > 0) {
$form['info'] = array(
'#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>',
);
$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(
'#value' => '<h2>' . t('No undo event was found') . '</h2>',
);
}
return $form;
}
/**
* Form submission handler for scanner_undo_confirm_form().
*
* @see scanner_undo_confirm_form()
*/
function scanner_undo_confirm_form_submit($form, &$form_state) {
// @todo The 'op' element in the form values is deprecated.
// 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')) {
$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;
}
$undos = unserialize($undo->undo_data);
$count = NULL;
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;
}
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();
}
else {
drupal_set_message(t('Undo / Redo canceled'));
}
$form_state['redirect'] = 'admin/content/scanner/undo';
$form_state['nid'] = $node->nid;
}
/**
* Handles the actual search and replace.
*
* @param string $searchtype
*
* @return The themed results.
*/
function scanner_execute($searchtype = 'search') {
global $user;
// Variables to monitor possible timeout.
$max_execution_time = ini_get('max_execution_time');
$start_time = REQUEST_TIME;
$expanded = FALSE;
// Get process and undo data if saved from timeout.
$processed = variable_get('scanner_partially_processed_' . $user->uid, array());
$undo_data = variable_get('scanner_partial_undo_' . $user->uid, array());
// Get the field collection field to use when joining revisions, based on
// whether the current version of the field_collection module has revisions
// enabled (7.x-1.0-beta5)
$fc_revision_field = drupal_get_schema('field_collection_item_revision') ? 'revision_id' : 'value';
unset($_SESSION['scanner_status']);
$search = $_SESSION['scanner_search'];
$replace = $_SESSION['scanner_replace'];
$preceded = $_SESSION['scanner_preceded'];
$followed = $_SESSION['scanner_followed'];
$mode = $_SESSION['scanner_mode'];
// Case sensitivity flag for use in php preg_search and preg_replace.
$flag = $mode ? NULL : 'i';
$wholeword = $_SESSION['scanner_wholeword'];
$regex = $_SESSION['scanner_regex'];
$published = $_SESSION['scanner_published'];
$pathauto = $_SESSION['scanner_pathauto'];
$terms = isset($_SESSION['scanner_terms']) ? $_SESSION['scanner_terms'] : NULL;
$results = NULL;
if ($searchtype == 'search') {
drupal_set_message(t('Searching for: [%search] ...', array(
'%search' => $search,
)));
}
else {
drupal_set_message(t('Replacing [%search] with [%replace] ...', array(
'%search' => $search,
'%replace' => $replace,
)));
}
$preceded_php = '';
if (!empty($preceded)) {
if (!$regex) {
$preceded = addcslashes($preceded, SCANNER_REGEX_CHARS);
}
$preceded_php = '(?<=' . $preceded . ')';
}
$followed_php = '';
if (!empty($followed)) {
if (!$regex) {
$followed = addcslashes($followed, SCANNER_REGEX_CHARS);
}
$followed_php = '(?=' . $followed . ')';
}
// Case 1.
if ($wholeword && $regex) {
$where = "[[:<:]]" . $preceded . $search . $followed . "[[:>:]]";
$search_php = '\\b' . $preceded_php . $search . $followed_php . '\\b';
}
elseif ($wholeword && !$regex) {
$where = "[[:<:]]" . $preceded . addcslashes($search, SCANNER_REGEX_CHARS) . $followed . "[[:>:]]";
$search_php = '\\b' . $preceded_php . addcslashes($search, SCANNER_REGEX_CHARS) . $followed_php . '\\b';
}
elseif (!$wholeword && $regex) {
$where = $preceded . $search . $followed;
$search_php = $preceded_php . $search . $followed_php;
}
else {
$where = $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;
}
$join = 'INNER JOIN {taxonomy_term_node} tn ON t.nid = tn.nid';
$where .= ' AND (' . implode(' OR ', $terms_where) . ')';
}
$table_map = _scanner_get_selected_tables_map();
usort($table_map, '_scanner_compare_fields_by_name');
// Examine each field instance as chosen in settings.
foreach ($table_map as $map) {
$table = $map['table'];
$field = $map['field'];
$field_label = $map['field_label'];
$type = $map['type'];
$module = isset($map['module']) ? $map['module'] : NULL;
$field_collection_parents = isset($map['field_collection_parents']) ? $map['field_collection_parents'] : NULL;
// Allow the table suffix to be altered.
$suffix = 'value';
// Trigger hook_scanner_field_suffix_alter().
drupal_alter('scanner_field_suffix', $suffix, $map);
$query = db_select($table, 't');
if ($table == 'node_revision') {
$vid = 'vid';
}
else {
$field = $field . '_' . $suffix;
$field_label = $field_label . '_' . $suffix;
$vid = 'revision_id';
}
if (!empty($field_collection_parents)) {
$cnt_fc = count($field_collection_parents);
// Loop thru the parents backwards, so that the joins can all be created.
for ($i = $cnt_fc; $i > 0; $i--) {
$fc_this = $field_collection_parents[$i - 1];
$fc_alias = 'fc' . ($i - 1);
$fc_table = 'field_revision_' . $fc_this;
$prev_alias = $i == $cnt_fc ? 't' : 'fc' . $i;
$query
->join($fc_table, $fc_alias, format_string('!PREV_ALIAS.entity_id = !FC_ALIAS.!FC_THIS_value AND !PREV_ALIAS.revision_id = !FC_ALIAS.!FC_THIS_!FC_REV', array(
'!FC_ALIAS' => $fc_alias,
'!FC_THIS' => $fc_this,
'!PREV_ALIAS' => $prev_alias,
'!FC_REV' => $fc_revision_field,
)));
if ($i == 1) {
$query
->join('node', 'n', format_string('!FC_ALIAS.entity_id = n.nid AND !FC_ALIAS.revision_id = n.vid', array(
'!FC_ALIAS' => $fc_alias,
)));
}
}
}
else {
// Must use vid and revision_id here. Make sure it saves as new revision.
$query
->join('node', 'n', 't.' . $vid . ' = n.vid');
}
if (is_array($terms) && !empty($terms)) {
$terms_or = db_or();
$query
->join('taxonomy_index', 'tx', 'n.nid = tx.nid');
foreach ($terms as $term) {
$terms_or
->condition('tx.tid', $term);
}
$query
->condition($terms_or);
}
$query
->addField('t', $field, 'content');
if ($table != 'node_revision') {
$query
->fields('t', array(
'delta',
));
}
$query
->fields('n', array(
'nid',
'title',
));
$query
->condition('n.type', $type, '=');
// Build the master 'where' arguments.
$or = db_or();
$binary = $mode ? ' BINARY' : '';
// Trigger hook_scanner_query_where().
foreach (module_implements('scanner_query_where') as $module_name) {
$function = $module_name . '_scanner_query_where';
$function($or, $table, $field, $where, $binary);
}
$query
->condition($or);
if ($published) {
$query
->condition('n.status', '1', '=');
}
$result = $query
->execute();
$shutting_down = FALSE;
// Perform the search or replace on each hit for the current field instance.
foreach ($result as $row) {
// Results of an entity property, e.g. the node title, won't have a
// 'delta' attribute, so make sure there is one.
if (!isset($row->delta)) {
$row->delta = 0;
}
$content = $row->content;
$summary = isset($row->summary) ? $row->summary : '';
$matches = array();
$text = '';
// If the max_execution_time setting has been set then check for possible
// timeout. If within 5 seconds of timeout, attempt to expand environment.
if ($max_execution_time > 0 && REQUEST_TIME >= $start_time + $max_execution_time - 5) {
if (!$expanded) {
if ($user->uid > 0) {
$verbose = TRUE;
}
else {
$verbose = FALSE;
}
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;
}
$expanded = TRUE;
}
else {
$shutting_down = TRUE;
variable_set('scanner_partially_processed_' . $user->uid, $processed);
variable_set('scanner_partial_undo_' . $user->uid, $undo_data);
if ($searchtype == 'search') {
drupal_set_message(t('Did not have enough time to complete search.'), 'error');
}
else {
drupal_set_message(t('Did not have enough time to complete. Please re-submit replace'), 'error');
}
break 2;
}
}
$node = node_load($row->nid);
// Build the regular expression used later.
$regexstr = "/{$search_php}/{$flag}";
// Search.
if ($searchtype == 'search') {
$matches = array(
'0' => array(),
);
$hits = 0;
// Assign matches in the base text field to $matches[0].
// Trigger hook_scanner_preg_match_all().
foreach (module_implements('scanner_preg_match_all') as $module_name) {
$function = $module_name . '_scanner_preg_match_all';
$new_matches = array();
$hits += $function($new_matches, $regexstr, $row);
$matches = array_merge($matches, $new_matches);
}
if ($hits > 0) {
$context_length = 70;
$text .= '<ul>';
foreach ($matches as $key => $item) {
$string = $key == 0 ? $content : $summary;
foreach ($item as $match) {
$text .= '<li>';
// if ($key == 1) {
// $text .= '<i>Summary:</i> ';
// }
// Don't want substr to wrap.
$start = $match[1] - $context_length > 0 ? $match[1] - $context_length : 0;
// If the match is close to the beginning of the string, need
// less context.
$length = $match[1] >= $context_length ? $context_length : $match[1];
if ($prepend = substr($string, $start, $length)) {
if ($length == $context_length) {
$text .= '...';
}
$text .= htmlentities($prepend, ENT_COMPAT, 'UTF-8');
}
$text .= '<strong>' . htmlentities($match[0], ENT_COMPAT, 'UTF-8') . '</strong>';
if ($append = substr($string, $match[1] + strlen($match[0]), $context_length)) {
$text .= htmlentities($append, ENT_COMPAT, 'UTF-8');
if (strlen($string) - ($match[1] + strlen($match[0])) > $context_length) {
$text .= '...';
}
}
$text .= '</li>';
}
}
$text .= '</ul>';
}
else {
$text = '<div class="messages warning"><h2 class="element-invisible">Warning message</h2>' . t("Can't display search result due to conflict between search term and internal preg_match_all function.") . '</div>';
}
$results[] = array(
'title' => $row->title,
'type' => $type,
'count' => $hits,
'field' => $field,
'field_label' => $field_label,
'nid' => $row->nid,
'text' => $text,
);
}
elseif (!isset($processed[$field][$row->nid][$row->delta])) {
// Check first if pathauto_persist, a newer version of pathauto, or some
// other module has already set $node->path['pathauto']. If not, set it
// to false (to prevent pathauto from touching the node during
// node_save()) if a custom alias exists that doesn't follow pathauto
// rules.
if (!isset($node->path['pathauto']) && module_exists('pathauto') && $pathauto) {
list($id, , $bundle) = entity_extract_ids('node', $node);
if (!empty($id)) {
module_load_include('inc', 'pathauto');
$uri = entity_uri('node', $node);
$path = drupal_get_path_alias($uri['path']);
$pathauto_alias = pathauto_create_alias('node', 'return', $uri['path'], array(
'node' => $node,
), $bundle);
$node->path['pathauto'] = $path != $uri['path'] && $path == $pathauto_alias;
}
}
$hits = 0;
preg_match('/(.+)_' . $suffix . '$/', $field, $matches);
// Field collections.
if (!empty($field_collection_parents)) {
foreach ($node->{$field_collection_parents[0]} as $fc_lang => $fc_data) {
foreach ($fc_data as $key => $fc_item) {
$fc = field_collection_item_load($fc_item['value']);
$fc_changed = FALSE;
foreach ($fc->{$matches[1]}[LANGUAGE_NONE] as $fc_key => $fc_val) {
$fc_hits = 0;
$fc_content = preg_replace($regexstr, $replace, $fc_val[$suffix], -1, $fc_hits);
if ($fc_content != $fc_val['value']) {
$fc_changed = TRUE;
$fc->{$matches[1]}[LANGUAGE_NONE][$fc_key][$suffix] = $fc_content;
}
// Also need to handle the summary part of text+summary fields.
if (isset($fc_val['summary'])) {
$summary_hits = 0;
$fc_summary = preg_replace($regexstr, $replace, $fc_val['summary'], -1, $summary_hits);
if ($fc_summary != $fc_val['summary']) {
$fc_hits += $summary_hits;
$fc_changed = TRUE;
$fc->{$matches[1]}[LANGUAGE_NONE][$fc_key]['summary'] = $fc_summary;
}
}
if ($fc_hits > 0) {
$results[] = array(
'title' => $node->title,
'type' => $node->type,
'count' => $fc_hits,
'field' => $field,
'field_label' => $field_label,
'nid' => $node->nid,
);
}
}
// If field collection revision handling is enabled, update the
// revision ID on the field.
// @todo Handle scenarios were the same FC is updated multiple
// times on the same request.
if ($fc_revision_field == 'revision_id') {
$fc->revision = 1;
}
// Update the field collection.
$fc
->save(TRUE);
// If field collection revision handling is enabled, update the
// revision ID on the field; the entity's revision_id is updated
// during the save() method, so this is safe to do.
if ($fc_revision_field == 'revision_id') {
$node->{$field_collection_parents[0]}[$fc_lang][$key]['revision_id'] = $fc->revision_id;
}
}
}
}
else {
// Trigger hook_scanner_preg_replace().
foreach (module_implements('scanner_preg_replace') as $module_name) {
$function = $module_name . '_scanner_preg_replace';
$hits += $function($node, $field, $matches, $row, $regexstr, $replace);
}
// Update the counter.
$results[] = array(
'title' => $node->title,
'nid' => $node->nid,
'type' => $node->type,
'count' => $hits,
'field' => $field,
'field_label' => $field_label,
);
}
// A revision only created for the first change of the node. Subsequent
// changes of the same node do not generate additional revisions.
// @todo Need a better way of handling this.
if (!isset($undo_data[$node->nid]['new_vid'])) {
$node->revision = TRUE;
$node->log = t('@name replaced %search with %replace via Scanner Search and Replace module.', array(
'@name' => $user->name,
'%search' => $search,
'%replace' => $replace,
));
$undo_data[$node->nid]['old_vid'] = $node->vid;
}
node_save($node);
// Array to log completed fields in case of shutdown.
$processed[$field][$row->nid][$row->delta] = TRUE;
// Undo data construction.
// Now set to updated vid after node_save().
$undo_data[$node->nid]['new_vid'] = $node->vid;
}
}
}
// If completed.
if (isset($shutting_down) && !$shutting_down) {
variable_del('scanner_partially_processed_' . $user->uid);
variable_del('scanner_partial_undo_' . $user->uid);
}
if ($searchtype == 'search') {
return theme('scanner_results', array(
'results' => $results,
));
}
else {
if (count($undo_data) && !$shutting_down) {
db_insert('scanner')
->fields(array(
'undo_data' => serialize($undo_data),
'undone' => 0,
'searched' => $search,
'replaced' => $replace,
'count' => count($undo_data),
'time' => REQUEST_TIME,
))
->execute();
}
return theme('scanner_replace_results', array(
'results' => $results,
));
}
}
/**
* Implements hook_scanner_fields_alter().
*/
function field_collection_scanner_fields_alter(array &$all_field_records, $node_bundle = NULL, $parent_bundle = NULL, $parents = NULL) {
// On behalf of field_collection.module.
$query = db_select('field_config_instance', 'instance_parent');
$query
->join('field_config', 'config_parent', 'instance_parent.field_name = config_parent.field_name');
$query
->join('field_config_instance', 'instance_child', 'instance_child.bundle = config_parent.field_name');
$query
->join('field_config', 'config_child', 'instance_child.field_name = config_child.field_name');
$query
->fields('config_child', array(
'field_name',
'module',
));
$query
->fields('instance_parent', array(
'bundle',
));
$query
->addField('config_parent', 'field_name', 'field_collection_name');
if ($parent_bundle) {
$query
->condition('config_parent.field_name', $parent_bundle);
$query
->condition('instance_parent.entity_type', 'field_collection_item');
}
else {
$query
->condition('instance_parent.entity_type', 'node');
}
$query
->condition('instance_child.entity_type', 'field_collection_item');
$query
->condition('config_child.module', array(
'text',
'field_collection',
), 'IN');
$result = $query
->execute();
foreach ($result as $record) {
$field_parents = isset($parents) ? $parents : array(
$record->field_collection_name,
);
$record->node_bundle = isset($node_bundle) ? $node_bundle : $record->bundle;
if ($record->module == 'text') {
$record->field_collection_parents = $field_parents;
$all_field_records[] = $record;
}
elseif ($record->module == 'field_collection') {
// This if statement prevents infinite recursion if a field collection is
// nested within itself.
if (!in_array($record->field_name, $field_parents)) {
$field_parents[] = $record->field_name;
field_collection_scanner_fields_alter($all_field_records, $record->node_bundle, $record->field_name, $field_parents);
}
}
}
}
/**
* Comparison function for sorting fields by table/field label.
*
* @param array $left
* One field.
* @param array $right
* The other field.
*
* @return number
* Comparison value determining which order these two fields should be sorted
* in relation to each other based on field label.
*/
function _scanner_compare_fields_by_label(array $left, array $right) {
$cmp = strcmp($left['type'], $right['type']);
if ($cmp != 0) {
return $cmp;
}
return strcmp($left['field_label'], $right['field_label']);
}
/**
* Comparison function for sorting fields by table/field name.
*
* @param array $left
* One field.
* @param array $right
* The other field.
*
* @return number
* Comparison value determining which order these two fields should be sorted
* in relation to each other based on field name.
*/
function _scanner_compare_fields_by_name(array $left, array $right) {
$cmp = strcmp($left['type'], $right['type']);
if ($cmp != 0) {
return $cmp;
}
return strcmp($left['field'], $right['field']);
}
/**
* Get all text fields.
*
* @return array
* List of all fields, each of which is an array containing relevant data
* used for diplaying/querying.
*/
function _scanner_get_all_tables_map() {
$tables_map = array();
// Build list of title fields for all node types.
foreach (node_type_get_types() as $type) {
if ($type->has_title) {
$tables_map[] = array(
'type' => $type->type,
'field' => 'title',
'field_label' => 'title',
'table' => 'node_revision',
);
}
}
$all_field_records = array();
// Trigger hook_scanner_field_types().
foreach (module_invoke_all('scanner_field_types') as $field_type) {
$query = db_select('field_config_instance', 'fci');
$query
->join('field_config', 'fc', 'fci.field_name = fc.field_name');
$query
->fields('fci', array(
'field_name',
));
$query
->fields('fc', array(
'module',
));
$query
->addField('fci', 'bundle', 'node_bundle');
$query
->condition('fci.entity_type', 'node');
$query
->condition('fc.module', $field_type, '=');
foreach ($query
->execute() as $record) {
$all_field_records[] = $record;
}
}
// Trigger hook_scanner_fields_alter().
drupal_alter('scanner_fields', $all_field_records);
if (!empty($all_field_records)) {
foreach ($all_field_records as $record) {
$tables_map[] = array(
'type' => $record->node_bundle,
'field' => $record->field_name,
'field_label' => (empty($record->field_collection_parents) ? '' : join('->', $record->field_collection_parents) . '->') . $record->field_name,
'table' => 'field_revision_' . $record->field_name,
'field_collection_parents' => isset($record->field_collection_parents) ? $record->field_collection_parents : NULL,
'module' => $record->module,
);
}
}
return $tables_map;
}
/**
* Get the fields that have been selected for scanning.
*
* @return map of selected fields and tables.
*/
function _scanner_get_selected_tables_map() {
$tables_map = _scanner_get_all_tables_map();
foreach ($tables_map as $i => $item) {
$key = 'scanner_' . $item['field'] . '_' . $item['table'] . '_' . $item['type'];
if (!variable_get($key, TRUE)) {
unset($tables_map[$i]);
}
}
return $tables_map;
}
/**
* Attempt to stretch the amount of time available for processing.
*
* This way timeouts won't interrupt search and replace actions. This only works
* in hosting environments where changing PHP and Apache settings on the fly is
* allowed.
*
* @param $setting
* The name of the PHP setting to change.
* @param $value
* The new value to assign.
* @param bool $verbose
* If set to TRUE, an extra message will be displayed indicating the status of
* the execution.
*
* @return bool
* Indicates whether the setting is changed.
*/
function _scanner_change_env($setting, $value, $verbose = FALSE) {
$old_value = ini_get($setting);
if ($old_value != $value && $old_value != 0) {
if (ini_set($setting, $value)) {
if ($verbose) {
drupal_set_message(t('%setting changed from %old_value to %value.', array(
'%setting' => $setting,
'%old_value' => $old_value,
'%value' => $value,
)));
}
return TRUE;
}
else {
if ($verbose) {
drupal_set_message(t('%setting could not be changed from %old_value to %value.', array(
'%setting' => $setting,
'%old_value' => $old_value,
'%value' => $value,
)), 'error');
}
return FALSE;
}
}
}
/**
* Implements hook_scanner_query_where().
*/
function scanner_scanner_query_where(&$or, $table, $field, $where, $binary) {
// Base field.
$or
->condition('t.' . $field, $where, 'REGEXP' . $binary);
}
/**
* Implements hook_scanner_preg_match_all().
*/
function scanner_scanner_preg_match_all(&$matches, $regexstr, $row) {
return preg_match_all($regexstr, $row->content, $matches, PREG_OFFSET_CAPTURE);
}
/**
* Implements hook_scanner_preg_replace().
*/
function scanner_scanner_preg_replace(&$node, $field, $matches, $row, $regexstr, $replace) {
$language = NULL;
$hits = 0;
$old_value = NULL;
if (!empty($matches[1])) {
$language = field_language('node', $node, $matches[1]);
// Not every field has a 'value'.
if (isset($node->{$matches[1]}[$language][$row->delta]['value'])) {
$old_value = $node->{$matches[1]}[$language][$row->delta]['value'];
}
}
else {
$old_value = $node->{$field};
}
// Replace the string if one was identified.
if (isset($old_value)) {
$new_value = preg_replace($regexstr, $replace, $old_value, -1, $hits);
}
if (!empty($matches[1])) {
// Not every field has a 'value'.
if (isset($node->{$matches[1]}[$language][$row->delta]['value'])) {
$node->{$matches[1]}[$language][$row->delta]['value'] = $new_value;
}
}
else {
$node->{$field} = $new_value;
}
return $hits;
}
/**
* Implements hook_scanner_query_alter().
*/
function node_scanner_query_alter(&$query, $map, $table, $field) {
// Performed on behalf of node.module.
$query
->condition('n.status', '1', '=');
}
/**
* Implements hook_scanner_field_types().
*/
function text_scanner_field_types() {
// Performed on behalf of text.module.
return array(
'text',
);
}
/**
* Implements hook_scanner_query_where().
*/
function text_scanner_query_where(&$or, $table, $field, $where, $binary) {
// Performed on behalf of text.module.
$field_summary = rtrim($field, '_value') . '_summary';
if (db_field_exists($table, $field_summary)) {
$or
->condition('t.' . $field_summary, $where, 'REGEXP' . $binary);
}
}
/**
* Implements hook_scanner_query_alter().
*/
function text_scanner_query_alter(&$query, $map, $table, $field) {
// Performed on behalf of text.module.
$summary_field = rtrim($field, '_value') . '_summary';
// A 'summary' value exists for this field.
if (db_field_exists($table, $summary_field)) {
$query
->addField('t', $field_summary, 'summary');
}
}
/**
* Implements hook_scanner_preg_match_all().
*/
function text_scanner_preg_match_all(&$matches, $regexstr, $row) {
// Performed on behalf of text.module.
if (!empty($row->summary)) {
return preg_match_all($regexstr, $row->summary, $matches, PREG_OFFSET_CAPTURE);
}
return 0;
}
/**
* Implements hook_scanner_preg_replace().
*/
function text_scanner_preg_replace(&$node, $field, $matches, $row, $regexstr, $replace) {
// Performed on behalf of text.module.
if (!empty($row->summary) && !empty($matches)) {
$old_value = $node->{$matches[1]}[$language][$row->delta]['summary'];
$new_value = preg_replace($regexstr, $replace, $old_value, -1, $hits);
$node->{$matches[1]}[$language][$row->delta]['summary'] = $new_value;
return $hits;
}
return 0;
}
/**
* Implements hook_scanner_field_types().
*/
function link_scanner_field_types() {
// Performed on behalf of link.module.
return array(
'link',
);
}
/**
* Implements hook_scanner_field_suffix_alter().
*/
function link_scanner_field_suffix_alter(&$suffix, $map) {
// Performed on behalf of link.module.
if (isset($map['module']) && $map['module'] == 'link') {
$suffix = 'title';
}
}
/**
* Implements hook_scanner_query_where().
*/
function link_scanner_query_where(&$or, $table, $field, $where, $binary) {
// Performed on behalf of link.module.
if (strpos($field, '_title') != FALSE) {
$field_url = rtrim($field, '_title') . '_url';
if (db_field_exists($table, $field_url)) {
$or
->condition('t.' . $field_url, $where, 'REGEXP' . $binary);
}
}
}
/**
* Implements hook_scanner_preg_replace().
*/
function link_scanner_preg_replace(&$node, $field, $matches, $row, $regexstr, $replace) {
// Performed on behalf of link.module.
$hits = 0;
if (!empty($matches[1])) {
$language = field_language('node', $node, $matches[1]);
// The Link module uses two field values - 'title' and 'url', so each one
// needs to be checked separately.
foreach (array(
'title',
'url',
) as $field_key) {
if (isset($node->{$matches[1]}[$language][$row->delta][$field_key])) {
$old_value = $node->{$matches[1]}[$language][$row->delta][$field_key];
$new_value = preg_replace($regexstr, $replace, $old_value, -1, $new_hits);
if ($new_hits > 0) {
$node->{$matches[1]}[$language][$row->delta][$field_key] = $new_value;
$hits += $new_hits;
}
}
}
}
return $hits;
}