Commit de4972c0 authored by Tao Starbow's avatar Tao Starbow
Browse files

Got CCK field seaching working.

Only modifiying most recent revision in node_revisions table.
Added title search/replace.
Added case insensitive searching.
Got custom fields working.
parent bcbccad1
...@@ -4,6 +4,27 @@ ...@@ -4,6 +4,27 @@
* @file * @file
* Search and Replace Scanner - works on all nodes text content. * Search and Replace Scanner - works on all nodes text content.
* *
* The Search and Replace Scanner can do regular expression matches
* against the body and CCK text content fields on all nodes in your system.
* This is useful for finding html strings that Drupal's normal search will
* ignore. And it can replace the matched text. Very handy if you are changing
* the name of your company, or are changing the URL of a link included
* multiple times in multiple nodes.
*
* The module allow you to configure which fields and tables to work with,
* and also to add in custom tables and fields for modules that don't use CCK.
*
* Limitations:
* Only works with Mysql
*
* Warning:
* This is a very powerful tool, and as such is very dangerous. You can
* easy distroy your entire site with it. Be sure to backup your database
* before using it. No, really.
*
* Todo:
* * Add more warning text.
* * Deal with lots of results and possible timeouts.
*/ */
/** /**
...@@ -20,11 +41,25 @@ function scanner_menu($may_cache) { ...@@ -20,11 +41,25 @@ function scanner_menu($may_cache) {
'access' => ($user->uid == 1), 'access' => ($user->uid == 1),
); );
$items[] = array( $items[] = array(
'path' => 'admin/content/scanner/scan',
'title' => t('Search'),
'access' => ($user->uid == 1),
'type' => MENU_DEFAULT_LOCAL_TASK,
);
$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->uid == 1),
'type' => MENU_LOCAL_TASK,
'title' => t('Settings'),
);
$items[] = array( // Shows up on admin page.
'path' => 'admin/settings/scanner', 'path' => 'admin/settings/scanner',
'callback' => 'drupal_get_form', 'callback' => 'drupal_get_form',
'callback arguments' => array('scanner_admin_form'), 'callback arguments' => array('scanner_admin_form'),
'type' => MENU_CALLBACK,
'access' => ($user->uid == 1), 'access' => ($user->uid == 1),
'title' => t('Search and Replace'),
); );
} }
return $items; return $items;
...@@ -134,29 +169,39 @@ function scanner_form_submit($form_id, $form_values) { ...@@ -134,29 +169,39 @@ function scanner_form_submit($form_id, $form_values) {
* @return The themed results. * @return The themed results.
*/ */
function scanner_search( $search ) { function scanner_search( $search ) {
drupal_set_message( "Looking for $search." ); $mode = variable_get('scanner_mode', 0);
drupal_set_message( "Looking for $search. Mode = $mode." );
$tables_map = _scanner_get_selected_tables_map(); $tables_map = _scanner_get_selected_tables_map();
if( $mode == 'cs' ) { // case sensitive
$flag = null;
$where = "CAST(t.%s AS BINARY) REGEXP '%s'"; // BINARY to force case sensative.
}
else { // case insensitive
$flag = 'i';
$where = "t.%s REGEXP '%s'";
}
foreach( $tables_map as $map ) { foreach( $tables_map as $map ) {
$table = $map['table']; $table = $map['table'];
$field = $map['field']; $field = $map['field'];
$on = $map['on'] ? $map['on'] : 'vid';
$result = db_query( "SELECT t.%s as content, t.nid, n.title FROM {%s} t ". $result = db_query( "SELECT t.%s as content, t.nid, n.title FROM {%s} t ".
" INNER JOIN {node} n ON t.nid = n.nid ". " INNER JOIN {node} n ON t.%s = n.%s ".
" WHERE CAST(%s AS BINARY) REGEXP '.*%s.*'", // BINARY to force case sensative. " WHERE $where",
$field, $table, $field, $search); $field, $table, $on, $on, $field, $search);
drupal_set_message("Scanning $field in $table."); drupal_set_message("Scanning $field in $table on $on.");
while( $obj = db_fetch_object($result) ){ while( $obj = db_fetch_object($result) ){
$content = $obj->content; $content = $obj->content;
$matches = array(); $matches = array();
$text = ''; $text = '';
$hits = preg_match_all("/(.{0,8})($search)(.{0,8})/", $content, $matches, PREG_SET_ORDER); $hits = preg_match_all("/(.{0,8})($search)(.{0,8})/$flag", $content, $matches, PREG_SET_ORDER);
if ($hits > 0) { if ($hits > 0) {
foreach( $matches as $match ) { foreach( $matches as $match ) {
if( $match[1] ) { if( $match[1] ) {
$text .= '...'. htmlentities($match[1]); $text .= '...'. htmlentities($match[1]);
} }
$text .= '<b>'. htmlentities($match[2]) .'</b>'; $text .= '<b>'. htmlentities($match[2]) .'</b>'; // Sadly destroys unicode, but needed to show html.
if( $match[3] ) { if( $match[3] ) {
$text .= htmlentities($match[3]) .'... '; $text .= htmlentities($match[3]) .'... ';
} }
...@@ -176,7 +221,7 @@ function scanner_search( $search ) { ...@@ -176,7 +221,7 @@ function scanner_search( $search ) {
} }
/** /**
* do a search and replace in the db * Do a search and replace in the db.
* *
* @param str $search - regexp. * @param str $search - regexp.
* @param str $replace - replacement text. * @param str $replace - replacement text.
...@@ -188,17 +233,33 @@ function scanner_replace($search, $replace) { ...@@ -188,17 +233,33 @@ function scanner_replace($search, $replace) {
foreach( $tables_map as $map ) { foreach( $tables_map as $map ) {
$table = $map['table']; $table = $map['table'];
$field = $map['field']; $field = $map['field'];
$sql = "UPDATE {$table} SET $field=REPLACE($field, '$search', '$replace') ". // Only set most recent version.
"WHERE CAST($field AS BINARY) REGEXP ('$search')"; $sql = "UPDATE {".$table."} t, {node} n SET t.$field=REPLACE(t.$field, '$search', '$replace') ".
//$output .= "<p>$sql</p>"; "WHERE n.vid = t.vid AND CAST(t.$field AS BINARY) REGEXP ('$search')";
// $output .= "<p>$sql</p>";
$output .= "Updating $field in $table.<br/>"; $output .= "Updating $field in $table.<br/>";
db_query( $sql ); if( db_query( $sql ) ) {
$updated += db_affected_rows(); $updated += db_affected_rows();
// Special case for node_revisions.title, sync node.title.
if ($field == 'title' && $table == 'node_revisions') {
db_query("UPDATE {node} n SET n.title=REPLACE(n.title, '%s', '%s') ".
"WHERE CAST(n.title AS BINARY) REGEXP ('%s')",
$search, $replace, $search );
}
}
else {
$output .= "<p>Bad SQL: $sql</p>";
}
} }
$output .= "<p>Updated $updated rows.</p>"; $output .= "<p>Updated $updated rows.</p>";
return $output; return $output;
} }
// ***************************************************************************
// Settings ******************************************************************
// ***************************************************************************
/** /**
* Search and Replace Settings form. * Search and Replace Settings form.
* *
...@@ -213,13 +274,27 @@ function scanner_admin_form() { ...@@ -213,13 +274,27 @@ function scanner_admin_form() {
); );
$table_map = _scanner_get_selected_tables_map(); $table_map = _scanner_get_selected_tables_map();
foreach($table_map as $item) { foreach($table_map as $item) {
$output .= '<li><b>'. $item['field'] .'</b> in <b>'. $item['table'] .'</b></li>'; $output .= '<li><b>'. $item['field'] .'</b> in <b>'. $item['table'] .'*** </b>';
if( $item['on'] ) {
$output .= 'on <b>'. $item['on'] .'</b>';
}
$output .= '</li>';
} }
$form['settings']['info']['#value'] = '<p>Fields that will be searched.</p><ul>'. $output .'</ul>'; $form['settings']['info']['#value'] = '<p>Fields that will be searched.</p><ul>'. $output .'</ul>';
$form['settings']['scanner_mode'] = array(
'#type' => 'radios',
'#title' => t('Search Mode'),
'#options' => array(
'cs' => t('Case Sensitive'),
'ci' => t('Case Insensitive'),
),
'#default_value' => variable_get('scanner_mode', 'cs'),
);
$form['tables'] = array( $form['tables'] = array(
'#type' => 'fieldset', '#type' => 'fieldset',
'#title' => t('Fields that could be searched'), '#title' => t('Fields that can be searched'),
'#collapsible' => TRUE, '#collapsible' => TRUE,
); );
$table_map = _scanner_get_all_tables_map(); $table_map = _scanner_get_all_tables_map();
...@@ -236,7 +311,7 @@ function scanner_admin_form() { ...@@ -236,7 +311,7 @@ function scanner_admin_form() {
'#type' => 'textarea', '#type' => 'textarea',
'#title' => t('Custom Fields'), '#title' => t('Custom Fields'),
'#default_value' => variable_get('scanner_custom', NULL), '#default_value' => variable_get('scanner_custom', NULL),
'#description' => "one per row, <i>field</i> in <i>table</i>", '#description' => "one per row, <i>field</i> in <i>table</i> on <i>vid or nid</i>",
); );
return system_settings_form($form); return system_settings_form($form);
...@@ -257,16 +332,30 @@ function scanner_admin_form_validate() { ...@@ -257,16 +332,30 @@ function scanner_admin_form_validate() {
/** /**
* Get all text fields. * 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. * @return map of fields and tables.
*/ */
function _scanner_get_all_tables_map() { function _scanner_get_all_tables_map() {
$tables_map[] = array('table' => 'node_revisions', 'field' => 'title');
$tables_map[] = array('table' => 'node_revisions', 'field' => 'body'); $tables_map[] = array('table' => 'node_revisions', 'field' => 'body');
$tables_map[] = array('table' => 'node_revisions', 'field' => 'teaser'); $tables_map[] = array('table' => 'node_revisions', 'field' => 'teaser');
$results = db_query("SELECT field_name, type_name FROM {node_field_instance} WHERE widget_type='text'"); $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)){ while($field=db_fetch_array($results)){
$tables_map[] = array('table'=>$field['type_name'], 'field'=>$field['field_name']); 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.
'table' => $table,
'field' => $field['field_name'] .'_value',
);
} }
return $tables_map; return $tables_map;
} }
...@@ -285,9 +374,9 @@ function _scanner_get_selected_tables_map() { ...@@ -285,9 +374,9 @@ function _scanner_get_selected_tables_map() {
} }
} }
$custom = variable_get('scanner_custom', NULL); $custom = variable_get('scanner_custom', NULL);
preg_match_all( '/(.*) in (.*)/', $custom, $matches, PREG_SET_ORDER ); preg_match_all( '/(.*) in (.*) on (.*)/', $custom, $matches, PREG_SET_ORDER );
foreach($matches as $match){ foreach($matches as $match){
$tables_map[] = array('table'=>$match[1], 'field'=>$match[2]); $tables_map[] = array('table'=>trim($match[2]), 'field'=>trim($match[1]), 'on'=>trim($match[3]));
} }
return $tables_map; return $tables_map;
} }
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment