|:'); //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); /** * Implementation of hook_menu(). */ function scanner_menu($may_cache) { global $user; $items = array(); if ($may_cache) { $items[] = array( 'path' => 'admin/content/scanner', 'title' => t('Search and Replace Scanner'), 'callback' => 'scanner_view', 'access' => user_access('perform search and replace'), ); $items[] = array( 'path' => 'admin/content/scanner/scan', 'title' => t('Search'), 'access' => user_access('perform search and replace'), 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items[] = array( 'path' => 'admin/content/scanner/scan/confirm', 'title' => t('Confirm Replace'), 'access' => user_access('perform search and replace'), 'callback' => 'drupal_get_form', 'callback arguments' => array('scanner_confirm_form'), 'type' => MENU_CALLBACK, ); $items[] = array( 'path' => 'admin/content/scanner/undo/confirm', 'title' => t('Confirm Undo'), 'access' => user_access('perform search and replace'), 'callback' => 'drupal_get_form', 'callback arguments' => array('scanner_undo_confirm_form'), 'type' => MENU_CALLBACK, ); $items[] = array( // Shows up on scanner page as tab. 'path' => 'admin/content/scanner/settings', 'callback' => 'drupal_get_form', 'callback arguments' => array('scanner_admin_form'), 'access' => user_access('administer scanner settings'), 'type' => MENU_LOCAL_TASK, 'title' => t('Settings'), 'weight' => 1, ); $items[] = array( // Shows up on scanner page as tab. 'path' => 'admin/content/scanner/undo', 'callback' => 'scanner_undo_page', 'access' => user_access('perform search and replace'), 'type' => MENU_LOCAL_TASK, 'title' => t('Undo'), ); $items[] = array( // Shows up on admin page. 'path' => 'admin/settings/scanner', 'callback' => 'drupal_get_form', 'callback arguments' => array('scanner_admin_form'), 'access' => user_access('administer scanner settings'), 'title' => t('Search and Replace Scanner'), ); } return $items; } /** * Implementation of hook_perm(). */ function scanner_perm() { return array('administer scanner settings', 'perform search and replace'); } /** * Menu callback; presents the scan form and results. */ function scanner_view() { //using set_html_head because it seems unecessary to load a separate css // file for just two simple declarations: drupal_set_html_head(' '); //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; }); }); ", 'inline'); $search = $_SESSION['scanner_search']; $status = $_SESSION['scanner_status']; if (!is_NULL($search) && $status >= SCANNER_STATUS_GO_SEARCH) { if ($status == SCANNER_STATUS_GO_CONFIRM) { drupal_goto('admin/content/scanner/scan/confirm'); } else if ($status == SCANNER_STATUS_GO_REPLACE) { $resulttxt = ''. t('Replacement Results'); $results = scanner_execute('replace'); } else { $resulttxt = t('Search Results'); $results = scanner_execute('search'); } if ($results) { $results = ''. theme('box', $resulttxt, $results); } else { $results = theme('box', t('Your scan yielded no results'), NULL); } $output = drupal_get_form('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_regex']); unset($_SESSION['scanner_terms']); //clear old status: unset($_SESSION['scanner_status']); return $output; } return $output . drupal_get_form('scanner_form'); } /** * The search and replace form. * * @param str $search - regex to search for. * @param str $replace - string to substitute. * @return $form */ function scanner_form() { $form = array(); $search = $_SESSION['scanner_search']; $replace = $_SESSION['scanner_replace']; $preceded = $_SESSION['scanner_preceded']; $followed = $_SESSION['scanner_followed']; $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); $terms = $_SESSION['scanner_terms']; $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, ); $form['submit_replace'] = array( '#type' => 'submit', '#value' => t('Replace'), ); $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... $form['options']['surrounding']['notpreceded'] = array( '#type' => 'checkbox', '#title' => t('NOT preceded by the text above'), '#default_value' => $notpreceded, ); */ $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... $form['options']['surrounding']['notfollowed'] = array( '#type' => 'checkbox', '#title' => t('NOT followed by the text above'), '#default_value' => $notfollowed, ); */ $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 not 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())); 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; } /** * Validate form input. */ function scanner_form_validate($form_id, $form_values) { $search = trim($form_values['search']); if ($search == '') { form_set_error('search', t('Please enter some keywords.')); } } /** * Handles submission of the search and replace form. * * @param $form_id * @param $form_values * @return the new path that will be goto'ed. */ function scanner_form_submit($form_id, $form_values) { //save form input: $_SESSION['scanner_search'] = $form_values['search']; $_SESSION['scanner_preceded'] = $form_values['preceded']; //$_SESSION['scanner_notpreceded'] = $form_values['notpreceded']; $_SESSION['scanner_followed'] = $form_values['followed']; //$_SESSION['scanner_notfollowed'] = $form_values['notfollowed']; $_SESSION['scanner_mode'] = $form_values['mode']; $_SESSION['scanner_wholeword'] = $form_values['wholeword']; $_SESSION['scanner_published'] = $form_values['published']; $_SESSION['scanner_regex'] = $form_values['regex']; $_SESSION['scanner_terms'] = $form_values['terms']; $_SESSION['scanner_replace'] = $form_values['replace']; if ($form_values['op'] == 'Replace') { $_SESSION['scanner_status'] = SCANNER_STATUS_GO_CONFIRM; } else { $_SESSION['scanner_status'] = SCANNER_STATUS_GO_SEARCH; } return 'admin/content/scanner'; } /** * Scanner confirmation form to prevent people from accidentally * replacing things they don't intend to. */ function scanner_confirm_form() { //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 {...} drupal_set_html_head(' '); //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('

Replacing items... please wait...

') $('.scanner-button-msg').click(function() { return false; }); return true; }); }); ", 'inline'); $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 = ( '

'. t('Are you sure you want to make the following replacement?') .'

'. '
'. ' ['. check_plain($search) .']'. '
' ); if ($preceded) { $msg .= ( '
'. ' ['. check_plain($preceded) .']'. '
' ); } if ($followed) { $msg .= ( '
'. ' ['. check_plain($followed) .']'. '
' ); } $msg .= ( '
'. ' ['. check_plain($replace) .']' ); if ($replace === '') { $msg .= ' This will delete any occurences of the search terms!'; } $msg .= ( '
'. '
'. ' '. $modetxt . '
' ); if ($wholeword) { $msg .= ( '
'. ' '. t('Yes') . '
' ); } if ($regex) { $msg .= ( '
'. ' '. t('Yes') . '
' ); } $form['warning'] = array( '#type' => 'markup', '#value' => $msg, ); $form['confirm'] = array( '#type' => 'submit', '#value' => t('Yes, Continue'), '#prefix' => '
', //see suffix in cancel button element ); $form['cancel'] = array( '#type' => 'submit', '#value' => t('No, Cancel'), '#suffix' => '
', //see prefix in confirm button element ); return $form; } /** * Submission handling for scanner confirmation form. */ function scanner_confirm_form_submit($form_id, $form_values) { if ($form_values['op'] == t('Yes, Continue')) { $_SESSION['scanner_status'] = SCANNER_STATUS_GO_REPLACE; } else { unset($_SESSION['scanner_status']); } return 'admin/content/scanner'; } function scanner_undo_page() { $header = array(t('Date'), t('Searched'), t('Replaced'), t('Count'), t('Operation')); $sandrs = db_query('SELECT undo_id, time, searched, replaced, count, undone FROM {scanner} ORDER BY undo_id DESC'); while ($sandr = db_fetch_object($sandrs)) { $query = 'undo_id='. $sandr->undo_id; if ($sandr->undone) { $operation = l('Redo', 'admin/content/scanner/undo/confirm', array(), $query); } else { $operation = l('Undo', 'admin/content/scanner/undo/confirm', array(), $query); } $rows[] = array( format_date($sandr->time), check_plain($sandr->searched), check_plain($sandr->replaced), $sandr->count, $operation, ); } return theme('table', $header, $rows, NULL, 'Prior Search and Replace Events'); } function scanner_undo_confirm_form() { $undo_id = $_GET['undo_id']; if ($undo_id > 0) { $undo = db_fetch_object(db_query('SELECT undo_id, searched, replaced FROM {scanner} WHERE undo_id = %d', $undo_id)); } if ($undo->undo_id > 0) { $form['info'] = array( '#value' => '

'. t('Do you want to undo:') .'

'. '

'. t('Searched for:') .'

'. '

['. check_plain($undo->searched) .']

'. '

'. t('Replaced with:') .'

'. '

['. check_plain($undo->replaced) .']

', ); $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' => '

'. t('No undo event was found') .'

', ); } return $form; } function scanner_undo_confirm_form_submit($form_id, $form) { if ($form['op'] == t('Yes, Continue')) { $undo = db_fetch_object(db_query('SELECT undo_data, undone FROM {scanner} WHERE undo_id = %d', $form['undo_id'])); $undos = unserialize($undo->undo_data); 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')); db_query('UPDATE {scanner} SET undone = %d WHERE undo_id = %d', $undone, $form['undo_id']); } else { drupal_set_message(t('Undo / Redo canceled')); } return 'admin/content/scanner/undo'; } /** * Handles the actual search and replace. * * @param str $searchtype - either 'search', or 'replace' * @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 = time(); $expanded = FALSE; // get process and undo data if saved from timeout $processed = variable_get('scanner_partially_processed', array()); $undo_data = variable_get('scanner_partial_undo', array()); unset($_SESSION['scanner_status']); $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']; $terms = $_SESSION['scanner_terms']; 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 $where = "CAST(t.%s AS BINARY) "; // BINARY to force case sensative. $flag = NULL; } else { // Case Insensitive $where = "t.%s "; $flag = 'i'; //ci flag for use in php preg_search and preg_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 .= "REGEXP '[[:<:]]%s[[:>:]]'"; $search_db = $preceded . $search . $followed; $search_php = '\b'. $preceded_php . $search . $followed_php .'\b'; } //Case 2: else if ($wholeword && !$regex) { $where .= "REGEXP '[[:<:]]%s[[:>:]]'"; $search_db = $preceded . addcslashes($search, SCANNER_REGEX_CHARS) . $followed; $search_php = '\b'. $preceded_php . addcslashes($search, SCANNER_REGEX_CHARS) . $followed_php .'\b'; } //Case 3: else if (!$wholeword && $regex) { $where .= "REGEXP '%s'"; $search_db = $preceded . $search . $followed; $search_php = $preceded_php . $search . $followed_php; } //Case 4: else { //!wholeword and !regex: $where .= "REGEXP '%s'"; $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; } $join = 'INNER JOIN {term_node} tn ON t.nid = tn.nid'; $where .= ' AND ('. implode(' OR ', $terms_where) .')'; } if ($published) { $where .= ' AND n.status = 1 '; } $tables_map = _scanner_get_selected_tables_map(); foreach ( $tables_map as $map ) { $table = $map['table']; $field = $map['field']; $type = $map['type']; $on = $map['on'] ? $map['on'] : 'vid'; $query_params = array($field, $table, $on, $on, $type, $field, $search_db); if (!empty($join)) { $query_params = array_merge($query_params, $terms_params); } $result = db_query(" SELECT t.%s as content, t.nid, n.title FROM {%s} t INNER JOIN {node} n ON t.%s = n.%s $join WHERE n.type = '%s' AND $where ", $query_params); while ($row = db_fetch_object($result)) { $content = $row->content; $matches = array(); $text = ''; // checking for possible timeout // if within 5 seconds of timeout - attempt to expand environment if (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; } // if expanded environment still running out of time - shutdown process else { $shutting_down = TRUE; variable_set('scanner_partially_processed', $processed); variable_set('scanner_partial_undo', $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; } } /* * 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] ) { $text .= '...'. htmlentities($match[1], ENT_COMPAT, 'UTF-8'); } $text .= ''. htmlentities($match[2], ENT_COMPAT, 'UTF-8') .''; if ( $match[3] ) { $text .= htmlentities($match[3], ENT_COMPAT, 'UTF-8') .'...'; } } } else { $text = "
" . t("Can't display search result due to conflict between search term and internal preg_match_all function.") .'
'; } $results[] = array( 'title' => $row->title, 'type' => $type, 'count' => $hits, 'field' => $field, 'nid' => $row->nid, 'text' => $text, ); } /* * REPLACE * + check to see if already processed */ else if (!isset($processed[$field][$row->nid])) { $hits = 0; $newcontent = preg_replace("/$search_php/$flag", $replace, $content, -1, $hits); $thenode = node_load(array('nid' => $row->nid)); //see if we're dealing with a CCK text field and therefore need to strip the // "_value" off the end: preg_match('/(.+)_value$/', $field, $matches); if (empty($matches[0])) { //if not CCK text field: $thenode->$field = $newcontent; } else { //Is this the best way to copy the new content back into the node's CCK field??? $tmpstr = ('$thenode->'. $matches[1] .'[0]["value"] = $newcontent;'); eval($tmpstr); } // NOTE: a revision only created for the first change of the node. // subsequent changes of the same node do not generate additional revisions: if (!isset($undo_data[$thenode->nid]['new_vid'])) { $thenode->revision = TRUE; $thenode->log = t('@name replaced %search with %replace via Scanner Search and Replace module.', array('@name' => $user->name, '%search' => $search, '%replace' => $replace)); $undo_data[$thenode->nid]['old_vid'] = $thenode->vid; } if (variable_get('scanner_rebuild_teasers', 1)) { $thenode->teaser = node_teaser($thenode->body, $thenode->format); } node_save($thenode); // array to log completed fields in case of shutdown $processed[$field][$row->nid] = TRUE; // undo data construction $undo_data[$thenode->nid]['new_vid'] = $thenode->vid; //now set to updated vid after node_save() $results[] = array( 'title' => $thenode->title, 'type' => $thenode->type, 'count' => $hits, 'field' => $field, 'nid' => $thenode->nid, ); } } //end while } //end foreach // if completed if(!$shutting_down) { variable_del('scanner_partially_processed'); variable_del('scanner_partial_undo'); } if ($searchtype == 'search') { return theme('scanner_results', $results); } else { //searchtype == 'replace' if (count($undo_data) && !$shutting_down) { $undo_id = db_next_id('{scanner}_undo_id'); db_query('INSERT INTO {scanner} (undo_id, undo_data, undone, searched, replaced, count, time) VALUES (%d, "%s", %d, "%s", "%s", %d, %d)', $undo_id, serialize($undo_data), 0, $search, $replace, count($undo_data), time()); } return theme('scanner_replace_results', $results); } } // *************************************************************************** // Settings ****************************************************************** // *************************************************************************** /** * Search and Replace Settings form. * * @return $form */ function scanner_admin_form() { drupal_set_title('Scanner Settings'); $table_map = _scanner_get_selected_tables_map(); sort($table_map); foreach ($table_map as $item) { $output .= '
  • '. $item['type'] .': '. $item['field']; if ( $item['on'] ) { $output .= t('on !on', array('!on' => $item['on'])); } $output .= '
  • '; } $form['selected'] = array( '#type' => 'fieldset', '#title' => t('Current Settings'), '#collapsible' => TRUE, ); $form['selected']['info']['#value'] = '

    Fields that will be searched (in [nodetype: fieldname] order):

    '; $form['settings'] = array( '#type' => 'fieldset', '#title' => t('Scanner Options'), '#collapsible' => TRUE, ); $form['settings']['scanner_mode'] = array( '#type' => 'checkbox', '#title' => t('Default: Case Sensitive Search Mode'), '#default_value' => variable_get('scanner_mode', 0), ); $form['settings']['scanner_wholeword'] = array( '#type' => 'checkbox', '#title' => t('Default: Match Whole Word'), '#default_value' => variable_get('scanner_wholeword', 0), ); $form['settings']['scanner_regex'] = array( '#type' => 'checkbox', '#title' => t('Default: Regular Expression Search'), '#default_value' => variable_get('scanner_regex', 0), ); $form['settings']['scanner_published'] = array( '#type' => 'checkbox', '#title' => t('Default: Search Published Nodes Only'), '#default_value' => variable_get('scanner_published', 1), ); $form['settings']['scanner_rebuild_teasers'] = array( '#type' => 'checkbox', '#title' => t('Rebuild Teasers on Replace'), '#default_value' => variable_get('scanner_rebuild_teasers', 1), '#description' => t('If this box is checked: The teasers for any nodes that are modified in a search-and-replace action will be rebuilt to reflect the replacements in other fields; you do not need to check any teaser fields for nodes in the "Fields" section below. If this box is unchecked: Teasers will remain untouched; you can select specific teaser fields below to include in search-and-replaces.'), ); if (module_exists('taxonomy')) { $vocabularies = taxonomy_get_vocabularies(); if (count($vocabularies)) { $options = array(); foreach ($vocabularies as $vocabulary) { $options[$vocabulary->vid] = $vocabulary->name; } $form['settings']['scanner_vocabulary'] = array( '#type' => 'checkboxes', '#title' => t("Allow restrictions by terms in a vocabulary"), '#options' => $options, '#default_value' => variable_get('scanner_vocabulary', array()), ); } } $form['tables'] = array( '#type' => 'fieldset', '#title' => t('Fields that can be searched'), '#description' => t('Fields are listed in [nodetype: fieldname] order:'), '#collapsible' => TRUE, ); $table_map = _scanner_get_all_tables_map(); sort($table_map); foreach ($table_map as $item) { $key = 'scanner_'. $item['field'] .'_'. $item['table'] .'_'. $item['type']; $form['tables'][$key] = array( '#type' => 'checkbox', '#title' => ''. $item['type'] .': '. $item['field'], '#default_value' => variable_get($key, FALSE), // default to not checked ); } $form['scanner_custom'] = array( '#type' => 'textarea', '#title' => t('Custom Fields'), '#default_value' => variable_get('scanner_custom', NULL), '#description' => "one per row, field in table of type nodetype on vid or nid", ); return system_settings_form($form); } // *************************************************************************** // Internal Utility Functions ************************************************ // *************************************************************************** /** * Get all text fields. * This is all very fragle based on how CCK stores fields. * Works for CCK 1.6. * * @return map of fields and tables. */ function _scanner_get_all_tables_map() { //note, each array in the multidim array that is returned should be in the // following order: type, field, table. //this ensures that we can use the sort() function to easily sort the array // based on the nodetype. //build list of title, body, teaser fields for all node types: $ntypes = node_get_types(); foreach ($ntypes as $type) { if ($type->has_title) { $tables_map[] = array('type' => $type->type, 'field' => 'title', 'table' => 'node_revisions'); } if ($type->has_body) { $tables_map[] = array('type' => $type->type, 'field' => 'body', 'table' => 'node_revisions'); } $tables_map[] = array('type' => $type->type, 'field' => 'teaser', 'table' => 'node_revisions'); } //now build list of CCK-based text fields: $results = db_query("SELECT nfi.field_name, nfi.type_name, nf.db_storage ". "FROM {node_field_instance} nfi INNER JOIN {node_field} nf USING (field_name) ". "WHERE nfi.widget_type='text'"); while ($field=db_fetch_array($results)) { if ( $field['db_storage'] ) { $table = 'content_type_'. $field['type_name']; } else { $table = 'content_'. $field['field_name']; } $tables_map[] = array( // Modify to match current CCK storage rules. 'type' => $field['type_name'], 'field' => $field['field_name'] .'_value', 'table' => $table, ); } 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, FALSE)) { unset($tables_map[$i]); } } $custom = variable_get('scanner_custom', NULL); preg_match_all( '/(.*) in (.*) of type (.*) on (.*)/', $custom, $matches, PREG_SET_ORDER ); foreach ($matches as $match) { $tables_map[] = array('type' => trim($match[3]), 'field' => trim($match[1]), 'table' => trim($match[2]), 'on' => trim($match[4])); } return $tables_map; } /** * Attempt to stretch the amount of time available for processing so * that timeouts don't interrupt search and replace actions. * * This only works in hosting environments where changing PHP and * Apache settings on the fly is allowed. */ function _scanner_change_env($setting, $value, $verbose) { $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; } } } // *************************************************************************** // Theme Functions *********************************************************** // *************************************************************************** /** * The the search results. * * @param map $results * @return html str. */ function theme_scanner_results($results) { if (is_array($results)) { $total = count($results); drupal_set_message('Found matches in '. $total .' fields. See below for details.'); $output = '

    Found matches in '. $total .' fields:

    '; $output .= '
      '; foreach ($results as $item) { $output .= theme('scanner_item', $item); } $output .= '
    '; //TO DO: use pager to split up results } else { drupal_set_message('Sorry, we found no matches.'); } return $output; } /** * Theme each search result hit. * * @param map $item. * @return html str. */ function theme_scanner_item($item) { $item['count'] = $item['count'] > 0 ? $item['count'] : 'One or more'; $output .= '
  • '; $output .= ''. l($item['title'], 'node/' . $item['nid']) .'
    '; $output .= '['. $item['count'] .' matches in '. $item['type'] .' '. $item['field'] .'field:]
    '; $output .= ''. $item['text'] .''; $output .= '
  • '; return $output; } function theme_scanner_replace_results($results) { if (is_array($results)) { drupal_set_message('Replaced items in '. count($results) .' fields. See below for details.'); $output = '

    Replaced items in '. count($results) .' fields:

    '; $output .= '
      '; foreach ($results as $item) { $output .= theme('scanner_replace_item', $item); } $output .= '
    '; //TO DO: use pager to split up results } else { drupal_set_message('Sorry, we found 0 matches.'); } return $output; } function theme_scanner_replace_item($item) { $item['count'] = $item['count'] > 0 ? $item['count'] : 'One or more'; $output .= '
  • '; $output .= ''. l($item['title'], 'node/'. $item['nid']) .'
    '; $output .= '['. $item['count'] .' replacements in '. $item['type'] .' '. $item['field'] .' field]'; $output .= '
  • '; return $output; }