primary_key ) ) {
return $row;
}
$row = sprintf(
'
|
%s
%s
|
%s
|
%d
|
%d
|
',
esc_attr( $item->primary_key ),
esc_attr( $item->primary_column ),
esc_attr( $item->primary_type ),
esc_attr( $item->table ),
esc_attr( $item->column ),
$item->stringresult,
( ! current_user_can( String_Locator::$default_capability ) ? '' : sprintf(
'%2$s',
esc_url( $item->editurl ),
// translators: The row-action edit link label.
esc_html__( 'Edit', 'string-locator' )
) ),
( ! current_user_can( String_Locator::$default_capability ) ? $item->filename : sprintf(
'%s',
esc_url( $item->editurl ),
esc_html( $item->filename )
) ),
esc_html( $item->primary_key ),
esc_html( $item->linepos )
);
return $row;
}
/**
* Add SQL items as search options.
*
* @param string $searchers The markup for the existing search options.
* @param string $search_location The currently selected search option, when restoring a search.
*
* @return string
*/
public function add_search_options( $searchers, $search_location ) {
ob_start();
?>
get_param( 'data' ) );
if ( 'sql' === $data->directory ) {
$store = (object) array(
'type' => 'sql',
'search' => $data->search,
'directory' => 'sql',
'chunks' => 1,
'regex' => $data->regex,
);
set_transient( 'string-locator-search-overview', $store );
update_option( 'string-locator-search-history', array(), false );
return array(
'success' => true,
'data' => array(
'chunks' => 1,
'current' => 0,
'regex' => $data->regex,
'total' => 1,
),
);
}
return $short_circuit;
}
/**
* Conditionally override the search handler.
*
* @param mixed $handler The currently active class for handling the search request.
* @param \WP_REST_Request $request The request received by the REST API handler.
*
* @return $this
*/
public function maybe_perform_sql_search( $handler, $request ) {
$search_data = get_transient( 'string-locator-search-overview' );
if ( empty( $search_data ) || ! isset( $search_data->type ) || 'sql' !== $search_data->type ) {
return $handler;
}
return $this;
}
/**
* Run the search.
*
* @param int $filenum An integer representing where in the line you are when doing batch searches.
*
* @return array
*/
public function run( $filenum ) {
global $wpdb;
$response = array(
'search' => array(),
'filenum' => absint( $filenum ),
'current' => 0,
'total' => 0,
'type' => 'sql',
);
$scan_data = get_transient( 'string-locator-search-overview' );
$is_regex = false;
if ( isset( $scan_data->regex ) ) {
$is_regex = String_Locator::absbool( $scan_data->regex );
}
if ( $is_regex ) {
if ( false === @preg_match( $scan_data->search, '' ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
wp_send_json_error(
array(
'continue' => false,
'message' => sprintf(
/* translators: %s: The search string used. */
__( 'Your search string, %s, is not a valid pattern, and the search has been aborted.', 'string-locator' ),
esc_html( $scan_data->search )
),
)
);
}
}
// Get al ist of all available tables to search through.
$tables = $wpdb->get_results( 'SHOW TABLES' );
$identifier_name = 'Tables_in_' . DB_NAME;
if ( ! validate_sql_fields( $identifier_name ) ) {
wp_send_json_error(
array(
'continue' => false,
'message' => sprintf(
/* translators: %s: The search string used. */
__( 'The table identifier, combined with your database name, %s, is not a valid SQL pattern, and the search has been aborted.', 'string-locator' ),
esc_html( $identifier_name )
),
)
);
}
$match_count = 0;
$search_results = array();
foreach ( $tables as $table ) {
$table_name = $table->{ $identifier_name };
$columns = $wpdb->get_results( 'DESCRIBE ' . $table_name ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The table name is validated earlier for SQLi, and needs to be dynamic due to relying on `DB_NAME`.
$primary_column = null;
$primary_type = null;
// Initial loop only gets primary data.
foreach ( $columns as $column ) {
if ( 'PRI' === $column->Key ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Object property name is returned by the MySQL database.
$primary_column = $column->Field; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Object property name is returned by the MySQL database.
$primary_type = ( stristr( $column->Type, 'int' ) ? 'int' : 'str' ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Object property name is returned by the MySQL database.
}
}
foreach ( $columns as $column ) {
$column_name = $column->Field; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Object property name is returned by the MySQL database.
if ( $is_regex ) {
$matches = $wpdb->get_results(
$wpdb->prepare(
'SELECT ' . $column_name . ' AS column_name, ' . $primary_column . ' as primary_column FROM ' . $table_name . ' WHERE ' . $column_name . ' REGEXP %s', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- It is not possible to prepare a table or column name, but these are instead validated in `/includes/Search/class-sql.php` before reaching this point.
$scan_data->search
)
);
} else {
$matches = $wpdb->get_results(
$wpdb->prepare(
'SELECT ' . $column_name . ' AS column_name, ' . $primary_column . ' as primary_column FROM ' . $table_name . ' WHERE ' . $column_name . ' LIKE %s', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- It is not possible to prepare a table or column name, but these are instead validated in `/includes/Search/class-sql.php` before reaching this point.
'%' . $wpdb->esc_like( $scan_data->search ) . '%'
)
);
}
if ( is_wp_error( $matches ) ) {
wp_send_json_error(
array(
'continue' => false,
'message' => sprintf(
/* translators: 1: The search string used. 2: The error received */
__( 'Your search for %1$s led to an SQL error, and the search has been aborted. The error encountered was: %2$s', 'string-locator' ),
esc_html( $scan_data->search ),
esc_html( $matches->get_error_message() )
),
)
);
}
foreach ( $matches as $match ) {
$match_count++;
$string = $scan_data->search;
$string_preview = $match->column_name;
$string_location = 0;
$string_preview = String_Locator::create_preview( $string_preview, $string, $is_regex );
$editurl = $this->create_edit_link( $table_name, $column_name, $primary_column, $primary_type, $match );
$search_results[] = array(
'ID' => $match_count,
'table' => $table_name,
'column' => $column_name,
'primary_key' => $match->primary_column,
'primary_type' => $primary_type,
'primary_column' => $primary_column,
'filename' => sprintf(
'`%s`.`%s`',
$table_name,
$column_name
),
'filename_raw' => sprintf(
'`%s`.`%s`',
$table_name,
$column_name
),
'editurl' => ( current_user_can( String_Locator::$default_capability ) ? $editurl : false ),
'stringresult' => $string_preview,
'linepos' => $string_location,
'linenum' => 0,
);
}
}
}
if ( ! empty( $search_results ) ) {
$history = get_option( 'string-locator-search-history', array() );
$history = array_merge( $history, $search_results );
update_option( 'string-locator-search-history', $history, false );
}
$response['search'] = array(
$search_results,
);
return $response;
}
/**
* Generate a link to the editor interface for a search result.
*
* @param string $table_name The table name where a match was found.
* @param string $column_name The column name where a match was found.
* @param string $primary_column The primary column from the table having a match.
* @param string $primary_type The type of the primary column.
* @param object $match An object containing details of the match.
*
* @return string
*/
public function create_edit_link( $table_name, $column_name, $primary_column, $primary_type, $match ) {
return add_query_arg(
array(
'page' => 'string-locator',
'edit-file' => true,
'file-type' => 'sql',
'file-reference' => sprintf(
'`%s`.`%s`',
$table_name,
$column_name
),
'sql-column' => $column_name,
'sql-table' => $table_name,
'sql-primary-column' => $primary_column,
'sql-primary-type' => $primary_type,
'sql-primary-key' => $match->primary_column,
),
admin_url( $this->path_to_use )
);
}
}
new Search();