|:');
//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'] = array_filter($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() {
$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?') .'
'.
''.
' '. t('Search for') .': ['. check_plain($search) .']'.
'
'
);
if ($preceded) {
$msg .= (
''.
' '. t('Preceded by') .': ['. check_plain($preceded) .']'.
'
'
);
}
if ($followed) {
$msg .= (
''.
' '. t('Followed by') .': ['. check_plain($followed) .']'.
'
'
);
}
$msg .= (
''.
' '. t('Replace with') .': ['. check_plain($replace) .']'
);
if ($replace === '') {
$msg .= ' This will delete any occurences of the search terms! ';
}
$msg .= (
'
'.
''.
' '. t('Mode') .': '. $modetxt .
'
'
);
if ($wholeword) {
$msg .= (
''.
' '. t('Match whole word') .': '. t('Yes') .
'
'
);
}
if ($regex) {
$msg .= (
''.
' '. t('Use regular expressions') .': '. t('Yes') .
'
'
);
}
$form['warning'] = array(
'#type' => 'markup',
'#value' => $msg,
);
$form['confirm'] = array(
'#type' => 'submit',
'#value' => t('Yes, Continue'),
);
$form['cancel'] = array(
'#type' => 'submit',
'#value' => t('No, Cancel'),
);
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_'. $user->uid, array());
$undo_data = variable_get('scanner_partial_undo_'. $user->uid, 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_'. $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;
}
}
/*
* 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_'. $user->uid);
variable_del('scanner_partial_undo_'. $user->uid);
}
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;
}