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();