'TINYINT',
'SMALLINT' => 'SMALLINT',
'MEDIUMINT' => 'MEDIUMINT',
'INT' => 'INT',
'BIGINT' => 'BIGINT',
'BIT' => 'BIT',
);
/**
* All MySQL float types.
*
* @property const array Holds the MySql float data types
*/
public const FLOAT_TYPES = array(
'DECIMAL' => 'DECIMAL',
'FLOAT' => 'FLOAT',
'DOUBLE' => 'DOUBLE',
);
/**
* Name of the table ID
*
* @var string
*
* @since 2.1.0
*/
private static $id = '';
/**
* Name of the real table ID
*
* @var string
*
* @since 2.1.0
*/
private static $real_id = '';
/**
* The name of the table
*
* @var string
*
* @since 2.1.0
*/
protected static $table_name = '';
/**
* Stores info for the table columns - that is extracted from the MySQL server.
*
* @var array
*
* @since 2.1.0
*/
protected static $columns_info = array();
/**
* Class cache that holds all of the tables in schema.
*
* @var array
*
* @since 2.2.0
*/
private static $tables = array();
/**
* Class cache that holds all core tables of WP.
*
* @var array
*
* @since 2.2.0
*/
private static $core_tables = array();
/**
* Holds the prepared options for speeding the proccess
*
* @var array
*
* @since 2.1.0
*/
protected static $admin_columns = array();
/**
* Class cache keeps the size of the table
*
* @var int
*
* @since 2.1.2
*/
private static $table_size = null;
/**
* Inner class cache to store the table status
*
* @var array
*
* @since 2.4.1
*/
private static $table_stat = array();
/**
* Inner class cache to store the table status
*
* @var array
*
* @since 5.2.0
*/
private static $tables_statuses = array();
/**
* Inits the class and sets the vars
*
* @param string $table_name - The name of the table to use.
*
* @return void
*
* @since 2.1.0
*/
public static function init( string $table_name ) {
self::$table_name = $table_name;
}
/**
* Returns the name of the table
*
* @return string
*
* @since 2.1.0
*/
public static function get_name(): string {
return static::$table_name;
}
/**
* Checks if give table exists
*
* @param string $table_name - The table to check for. If empty checks for the current table.
* @param \wpdb $connection - \wpdb connection to be used for name extraction.
*
* @return boolean
*
* @since 2.1.0
*/
public static function check_table_exists( string $table_name = '', $connection = null ): bool {
if ( null !== $connection ) {
if ( $connection instanceof \wpdb ) {
$_wpdb = $connection;
}
} else {
global $wpdb;
$_wpdb = $wpdb;
}
if ( '' === $table_name ) {
$table_name = static::get_name();
} else {
// Basic table name validation (letters, numbers + underscore only; dot disallowed to prevent cross-db reference).
if ( ! is_string( $table_name ) || ! preg_match( '/^[A-Za-z0-9_]+$/', $table_name ) ) {
new \WP_Error( 'invalid_table', 'Invalid table name.' );
return false;
}
}
// Escape LIKE wildcards in prefixes; avoid retrieving every table name.
$table = $_wpdb->get_var( $_wpdb->prepare( 'SHOW TABLES LIKE %s', $_wpdb->esc_like( $table_name ) ) );
return $table === $table_name;
}
/**
* Executes query.
*
* Important - query string is not checked nor validated, the calling script is responsible for that.
*
* @param string $query - The query which needs to be executed.
* @param \wpdb $connection - \wpdb connection to be used for name extraction.
* @param array|null $args - Optional arguments for the query, if provided the query will be prepared with those arguments before execution.
*
* @return array
*
* @since 2.1.0
*/
public static function execute_query( string $query, $connection = null, ?array $args = null ) {
if ( null !== $connection ) {
if ( $connection instanceof \wpdb ) {
$_wpdb = $connection;
} else {
global $wpdb;
$_wpdb = $wpdb;
}
} else {
global $wpdb;
$_wpdb = $wpdb;
}
if ( is_array( $args ) && ! empty( $args ) ) {
$prepared = $_wpdb->prepare( $query, $args );
return $_wpdb->query( $prepared );
}
return $_wpdb->query( $query );
}
/**
* Calls for the create table syntax and executes the query.
*
* @return void
*
* @since 2.1.0
*/
public static function create_table() {
// self::execute_query( static::get_create_table_sql() );
}
/**
* Drop the table from the DB.
*
* @param \WP_REST_Request $request - The request object.
* @param string $table_name - The name of the table, if one is not provided, the default will be used.
* @param \wpdb $connection - \wpdb connection to be used for name extraction.
*
* @return \WP_REST_Response|\WP_Error
*
* @since 2.1.0
*/
public static function drop_table( ?\WP_REST_Request $request = null, string $table_name = '', $connection = null ) {
// If coming from REST context, enforce capability check.
if ( null !== $request && ! \current_user_can( 'manage_options' ) ) {
return new \WP_Error(
'forbidden',
__( 'Sorry, you are not allowed to perform this action.', '0-day-analytics' ),
array( 'status' => 403 )
);
}
if ( null !== $connection ) {
if ( $connection instanceof \wpdb ) {
$_wpdb = $connection;
} else {
global $wpdb;
$_wpdb = $wpdb;
}
} else {
global $wpdb;
$_wpdb = $wpdb;
}
if ( null !== $request ) {
$table_name = $request->get_param( 'table_name' );
}
if ( '' === $table_name ) {
$table_name = static::get_name();
}
// Validate table name strictly.
if ( ! self::validate_table_name( $table_name ) ) {
return new \WP_Error(
'invalid_table',
__( 'Invalid table name.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
if ( ! \in_array( $table_name, self::get_wp_core_tables(), true )
&& \in_array( $table_name, self::get_tables( $_wpdb ), true ) ) {
// Use backticks around table name (already validated) to guard against edge cases.
self::execute_query( 'DROP TABLE IF EXISTS `' . $table_name . '`', $_wpdb );
} elseif ( null !== $request ) { // Call is coming from REST API.
return new \WP_Error(
'core_table',
__( 'Can not delete core table.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
if ( null !== $request ) { // Call is coming from REST API.
return \rest_ensure_response(
array(
'success' => true,
)
);
}
}
/**
* Truncates the table.
*
* @param \WP_REST_Request $request - The request object.
* @param string $table_name - The name of the table, if one is not provided, the default will be used.
* @param \wpdb $connection - \wpdb connection to be used for name extraction.
*
* @return \WP_REST_Response|\WP_Error
*
* @since 2.4.1
*/
public static function truncate_table( ?\WP_REST_Request $request = null, string $table_name = '', $connection = null ) {
// If coming from REST context, enforce capability check for destructive action.
if ( null !== $request && ! \current_user_can( 'manage_options' ) ) {
return new \WP_Error(
'forbidden',
__( 'Sorry, you are not allowed to perform this action.', '0-day-analytics' ),
array( 'status' => 403 )
);
}
if ( null !== $connection ) {
if ( $connection instanceof \wpdb ) {
$_wpdb = $connection;
} else {
global $wpdb;
$_wpdb = $wpdb;
}
} else {
global $wpdb;
$_wpdb = $wpdb;
}
if ( null !== $request ) {
$table_name = $request->get_param( 'table_name' );
}
if ( '' === $table_name ) {
$table_name = static::get_name();
}
// Validate table name strictly.
if ( ! self::validate_table_name( $table_name ) ) {
return new \WP_Error(
'invalid_table',
__( 'Invalid table name.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
if ( \in_array( $table_name, self::get_tables( $_wpdb ), true ) ) {
if ( ! \in_array( $table_name, self::get_wp_core_tables(), true ) ) {
// Use backticks around table name (already validated) to guard against edge cases.
self::execute_query( 'TRUNCATE TABLE `' . $table_name . '`', $_wpdb );
} else {
return new \WP_Error(
'truncate_table',
__( 'You are not allowed to truncate WP Core table.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
} elseif ( null !== $request ) { // Call is coming from REST API.
return new \WP_Error(
'truncate_table',
__( 'Can not truncate table.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
if ( null !== $request ) { // Call is coming from REST API.
return \rest_ensure_response(
array(
'success' => true,
)
);
}
}
/**
* Returns the name of the id column for the table
*
* @return string
*
* @since 2.1.0
*/
public static function get_id_name(): string {
return static::$id;
}
/**
* Returns the name of the real id column for the table - some times there are different ids - for internal use and for global use.
*
* @return string
*
* @since 2.1.0
*/
public static function get_real_id_name(): string {
if ( empty( self::$real_id ) ) {
global $wpdb;
// Ensure table name safety and quote it.
$table = self::get_name();
if ( ! self::validate_table_name( $table ) ) {
return '';
}
$sql = 'SHOW KEYS FROM `' . $table . '` WHERE Key_name = \'PRIMARY\'';
$result = $wpdb->get_results(
$sql,
ARRAY_A
);
if ( \is_array( $result ) && ! empty( $result ) && isset( $result[0]['Column_name'] ) ) {
static::$real_id = $result[0]['Column_name'];
} else {
$sql = 'SHOW INDEX FROM `' . $table . '`';
$result = $wpdb->get_results(
$sql,
ARRAY_A
);
if ( \is_array( $result ) && ! empty( $result ) && isset( $result[0]['Column_name'] ) ) {
static::$real_id = $result[0]['Column_name'];
}
if ( empty( static::$real_id ) ) {
$columns = self::get_column_names();
static::$real_id = reset( $columns );
}
}
}
return static::$real_id;
}
/**
* Checks for given column existence using custom connection.
*
* @param string $col_name - The name of the column.
* @param string $col_type - Type of the column.
* @param string $table_name - The name of the table.
* @param boolean|null $is_null - Is it null.
* @param mixed $key - Is it key.
* @param mixed $default - The default value of the column.
* @param mixed $extra - Extra parameters.
*
* @return boolean - True if the column exists and all given parameters are the same, false otherwise.
*
* @since 2.1.0
*/
public static function check_column(
string $col_name,
string $col_type,
string $table_name = '',
?bool $is_null = null,
$key = null,
$default = null,
$extra = null ): bool {
global $wpdb;
if ( '' === $table_name ) {
$table_name = static::get_name();
}
// Validate and quote table identifier to prevent injection.
if ( ! self::validate_table_name( $table_name ) ) {
return false;
}
$table_quoted = '`' . $table_name . '`';
$diffs = 0;
$results = $wpdb->get_results( 'DESC ' . $table_quoted ); // phpcs:ignore
foreach ( $results as $row ) {
if ( $row->Field === $col_name ) { // phpcs:ignore
// Got our column, check the params.
if ( ( null !== $col_type ) && ( strtolower( str_replace( ' ', '', $row->Type ) ) !== strtolower( str_replace( ' ', '', $col_type ) ) ) ) { // phpcs:ignore
++$diffs;
}
if ( ( null !== $is_null ) && ( $row->Null !== $is_null ) ) { // phpcs:ignore
++$diffs;
}
if ( ( null !== $key ) && ( $row->Key !== $key ) ) { // phpcs:ignore
++$diffs;
}
if ( ( null !== $default ) && ( $row->Default !== $default ) ) { // phpcs:ignore
++$diffs;
}
if ( ( null !== $extra ) && ( $row->Extra !== $extra ) ) { // phpcs:ignore
++$diffs;
}
if ( $diffs > 0 ) {
return false;
}
return true;
} // End if found our column.
}
return false;
}
/**
* Getter for the collected table column info
*
* @return array
*
* @since 3.2.0
*/
public static function get_columns_info(): array {
if ( empty( static::$columns_info ) ) {
global $wpdb;
$query = $wpdb->prepare(
'SHOW TABLES LIKE %s',
$wpdb->esc_like( self::get_name() )
);
if ( $wpdb->get_var( $query ) == self::get_name() ) {
static::$columns_info = $wpdb->get_results(
'DESC `' .
self::get_name() . '`',
ARRAY_A
);
}
}
return static::$columns_info;
}
/**
* Default find method
*
* @param array $data Must contains formats and data.
*
* @return int bool
*
* @since 3.8.0
*/
public static function find( array $data ) {
global $wpdb;
/**
* \WPDB has very powerful method called process_fields @see \WPDB::process_fields().
* Unfortunately this method is not accessible, because it is marked protected. The best solution at the moment is to clone the class, lower the visibility and use the method.
*
* That of course takes resources so possible solution is to add also caching to this method, so that is marked as todo below.
*
* TODO: Add caching functionality to the method.
*/
$wpdb_clone = new class() extends \wpdb {
public function __construct() {
$dbuser = defined( 'DB_USER' ) ? DB_USER : '';
$dbpassword = defined( 'DB_PASSWORD' ) ? DB_PASSWORD : '';
$dbname = defined( 'DB_NAME' ) ? DB_NAME : '';
$dbhost = defined( 'DB_HOST' ) ? DB_HOST : '';
parent::__construct( $dbuser, $dbpassword, $dbname, $dbhost );
}
public function process_fields( $name, $data, $formats ) {
return parent::process_fields( $name, $data, $formats );
}
};
$where_clause = $wpdb_clone->process_fields(
self::get_name(),
$data['data'],
$data['formats']
);
$where_data = self::prepare_full_where( $where_clause );
$conditions = $where_data['conditions'];
$values = $where_data['values'];
$wpdb->check_current_query = false;
$result = $wpdb->get_results(
$wpdb->prepare(
// phpcs:disable
'SELECT * from `' . self::get_name() . '` WHERE ' . $conditions,
$values
// phpcs:enable
),
ARRAY_A
);
self::check_error();
return $result;
}
/**
* Checks for errors and throws exception if any
*
* @throws \Exception Last wpdb error.
*
* @return void
*/
protected static function check_error() {
global $wpdb;
if ( '' !== $wpdb->last_error ) {
throw new \Exception( 'Error with query - check the logs' );
}
}
/**
* Prepares full where clause
*
* @param array $where_clause - Array prepared based on fields and values from the WP_DB.
* @param string $condition - The where clause condition - default AND.
* @param string|null $criteria - The criteria to check for.
* @param boolean|null $left_pref - For any starting value - partial where clause.
* @param boolean|null $right_pref - For any ending value - partial where clause.
*
* @return array
*
* @since 2.1.0
*/
public static function prepare_full_where( array $where_clause,
string $condition = ' AND ',
?string $criteria = ' = ',
?bool $left_pref = false,
?bool $right_pref = false
): array {
$conditions = array();
$values = array();
// Sanitize boolean connector and comparison operator.
$condition_sanitized = strtoupper( trim( (string) $condition ) );
if ( ! in_array( $condition_sanitized, array( 'AND', 'OR' ), true ) ) {
$condition_sanitized = 'AND';
}
$crit = strtoupper( trim( (string) $criteria ) );
$allowed_ops = array( '=', '!=', '<', '<=', '>', '>=', 'LIKE', 'NOT LIKE', 'REGEXP', 'IS', 'IS NOT' );
if ( ! in_array( $crit, $allowed_ops, true ) ) {
$crit = '=';
}
foreach ( $where_clause as $field => $value ) {
if ( is_null( $value['value'] ) ) {
$conditions[] = '`' . self::get_name() . '` . `' . $field . '` IS null';
continue;
}
$conditions[] = '`' . self::get_name() . '` . `' . $field . '` ' . $crit . ' ' .
$value['format'];
$values[] =
( ( $left_pref ) ? ' % ' : '' ) .
$value['value'] .
( ( $right_pref ) ? ' % ' : '' );
}
$conditions = implode( ' ' . $condition_sanitized . ' ', $conditions );
return array(
'conditions' => $conditions,
'values' => $values,
);
}
/**
* Collects table column names
*
* @return array
*
* @since 2.4.1
*/
public static function get_column_names(): array {
$array = array_column( self::get_columns_info(), 'Field', 'Field' );
return $array;
}
/**
* Returns the class shortname
*
* @return string
*
* @since 2.4.1
*/
public static function get_entity_name(): string {
return ( new \ReflectionClass( get_called_class() ) )->getShortName();
}
/**
* Default delete method
*
* @param integer $id - The real id of the table.
*
* @return int|bool
*
* @since 2.4.1
*/
public static function delete_by_id( int $id ) {
global $wpdb;
$result = $wpdb->delete(
self::get_name(),
array( self::get_real_id_name() => $id ),
array( '%d' )
);
self::check_error();
return $result;
}
/**
* Default delete method for API
*
* @param array $data - Array with key and value pair.
*
* @return int bool
*
* @since 2.4.1
*/
public static function delete_data( array $data ) {
global $wpdb;
if ( isset( $data['data'][ self::get_real_id_name() ] ) ) {
return self::delete_by_id( (int) $data['data'][ self::get_real_id_name() ] );
} else {
$where = $data['data'];
$where_format = $data['formats'];
$result = $wpdb->delete( self::get_name(), $where, $where_format );
self::check_error();
return $result;
}
}
/**
* Default update method
*
* @param array $data - Array with key and value pair.
*
* @return int bool
*
* @since 2.4.1
*/
public static function update_data( array $data ) {
global $wpdb;
$where_clause = self::extract_where( $data );
$where = $where_clause['where'];
$where_format = $where_clause['whereFormat'];
$result = $wpdb->update( self::get_name(), $data['data'], $where, $data['formats'], $where_format );
self::check_error();
return $result;
}
/**
* Default insert method
*
* @param array $data - Array with key and value pair.
*
* @return int|bool
*
* @since 2.4.1
*/
public static function insert_data( array $data ) {
global $wpdb;
$wpdb->insert(
self::get_name(),
$data['data'],
$data['formats']
);
self::check_error();
return $wpdb->insert_id;
}
/**
* Backups the table.
* Format is the table name + yearmonthday-hour:minute:second
*
* @return void
*
* @since 2.1.0
*/
public static function backup_table() {
global $wpdb;
// Validate base table name and assemble safe backup table identifier with timestamp.
if ( ! self::validate_table_name( self::get_name() ) ) {
return new \WP_Error( 'invalid_table', 'Invalid base table name.' );
}
$new_table = self::get_name() . gmdate( 'YmdHis' );
if ( ! preg_match( '/^[A-Za-z0-9_]+$/', $new_table ) ) {
$new_table = preg_replace( '/[^A-Za-z0-9_]/', '_', $new_table );
}
$sql = 'CREATE TABLE `' . $new_table . '` LIKE `' . self::get_name() . '`';
$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$sql = 'INSERT INTO `' . $new_table . '` SELECT * FROM `' . self::get_name() . '`';
$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Extracts array with where values / formats from prepared data array
*
* @param array $data - The array to extract the data from.
*
* @return array
*
* @since 2.4.1
*/
protected static function extract_where( array &$data ): array {
$where[ self::get_real_id_name() ] = $data['data'][ self::get_real_id_name() ];
unset( $data['data'][ self::get_real_id_name() ] );
$where_format[ self::get_real_id_name() ] = $data['formats'][ self::get_real_id_name() ];
unset( $data['formats'][ self::get_real_id_name() ] );
return array(
'where' => $where,
'whereFormat' => $where_format,
);
}
/**
* Returns the table CMS admin fields
*
* @return array
*
* @since 2.1.0
*/
public static function get_column_names_admin(): array {
$clmns = self::get_columns_info();
$array_clmns = array();
foreach ( $clmns as $column ) {
$array_clmns[ $column['Field'] ] = $column['Field'] . '
' . $column['Type'];
}
return $array_clmns;
}
/**
* Adds columns to the screen options screed
*
* @param array $columns - Array of column names.
*
* @return array
*
* @since 2.1.0
*/
public static function manage_columns( $columns ): array {
if ( empty( self::$admin_columns ) ) {
$screen_options = self::get_column_names_admin();
$table_columns = array(
'cb' => '', // to display the checkbox.
);
self::$admin_columns = \array_merge( $table_columns, $screen_options, $columns );
}
return self::$admin_columns;
}
/**
* Returns a list with all available tables.
*
* @param \wpdb $connection - \wpdb connection to be used for name extraction.
*
* @return array
*
* @since 2.1.0
*/
public static function get_tables( $connection = null ): array {
if ( empty( self::$tables ) ) {
if ( null !== $connection ) {
if ( $connection instanceof \wpdb ) {
$_wpdb = $connection;
}
} else {
global $wpdb;
$_wpdb = $wpdb;
}
$_wpdb->suppress_errors( true );
$results = $_wpdb->get_results(
// $wpdb->prepare(
// 'SELECT table_name FROM information_schema.tables WHERE table_schema = %s;',
'SHOW TABLES;',
// $wpdb->dbname
// ),
\ARRAY_A
);
if ( '' !== $_wpdb->last_error || null === $results ) {
self::$tables = self::get_wp_core_tables();
} else {
foreach ( $results as $table ) {
self::$tables[] = reset( $table );
}
}
$_wpdb->suppress_errors( false );
}
return self::$tables;
}
/**
* Returns all of the core WP tables.
*
* @return array
*
* @since 2.2.0
*/
public static function get_wp_core_tables() {
if ( empty( self::$core_tables ) ) {
global $wpdb;
self::$core_tables = $wpdb->tables( 'all' );
}
return self::$core_tables;
}
/**
* Returns the table size in Megabyte format
*
* @return int
*
* @since 2.1.2
*/
public static function get_table_size() {
if ( null === self::$table_size ) {
global $wpdb;
$sql = $wpdb->prepare(
'SELECT ROUND(((data_length + index_length)), 2) AS `Size (B)` FROM information_schema.TABLES WHERE table_schema = %s AND table_name = %s;',
$wpdb->dbname,
self::get_name()
);
$wpdb->suppress_errors( true );
$results = $wpdb->get_var( $sql );
if ( '' !== $wpdb->last_error || null === $results ) {
$results = array();
}
$wpdb->suppress_errors( false );
if ( $results ) {
self::$table_size = $results;
} else {
self::$table_size = 0;
}
}
return self::$table_size;
}
/**
* Extracts table information and returns it
*
* @return array
*
* @since 2.3.0
*/
public static function get_table_status(): array {
if ( empty( self::$table_stat ) ) {
global $wpdb;
// Parameterize SHOW TABLE STATUS LIKE query for consistency and safety.
$sql = $wpdb->prepare( 'SHOW TABLE STATUS FROM `' . $wpdb->dbname . '` LIKE %s;', self::get_name() );
$wpdb->suppress_errors( true );
$results = $wpdb->get_results( $sql, \ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( '' !== $wpdb->last_error || null === $results ) {
$results = array();
}
$wpdb->suppress_errors( false );
self::$table_stat = $results;
}
return self::$table_stat;
}
/**
* Extracts table information and returns it
*
* @return array
*
* @since 5.2.0
*/
public static function get_all_tables_status(): array {
if ( empty( self::$tables_statuses ) ) {
global $wpdb;
// Parameterize SHOW TABLE STATUS LIKE query for consistency and safety.
$sql = 'SHOW TABLE STATUS FROM `' . $wpdb->dbname . '`;';
$wpdb->suppress_errors( true );
$results = $wpdb->get_results( $sql, \ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( '' !== $wpdb->last_error || null === $results ) {
$results = array();
}
$wpdb->suppress_errors( false );
self::$tables_statuses = $results;
}
return self::$tables_statuses;
}
/**
* Returns the default table to show if none selected.
*
* @return string
*
* @since 2.4.1
*/
public static function get_default_table(): string {
global $wpdb;
return $wpdb->prefix . 'options';
}
/**
* Extracts single row data from given table and shows it in HTML format.
*
* @param \WP_REST_Request $request - The request object.
*
* @return \WP_REST_Response|\WP_Error
*
* @since 3.2.0
*/
public static function extract_row_data( \WP_REST_Request $request ) {
// Enforce capability check for data disclosure.
if ( ! \current_user_can( 'manage_options' ) ) {
return new \WP_Error(
'forbidden',
__( 'Sorry, you are not allowed to perform this action.', '0-day-analytics' ),
array( 'status' => 403 )
);
}
$table_name = $request->get_param( 'table_name' );
if ( '' === trim( $table_name ) ) {
return new \WP_Error(
'show_row',
__( 'Table name is not provided.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
if ( ! self::check_table_exists( $table_name ) ) {
return new \WP_Error(
'show_row',
__( 'Table does not exist.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
$id = $request->get_param( 'id' );
if ( empty( $id ) ) {
return new \WP_Error(
'show_row',
__( 'ID is not provided or wrong.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
self::init( $table_name );
global $wpdb;
$query = $wpdb->prepare(
'SELECT * FROM `' . self::get_name() . '` WHERE `' . self::get_real_id_name() . '` = %s;', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$id
);
$wpdb->suppress_errors( true );
$results = $wpdb->get_results( $query, \ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
if ( '' !== $wpdb->last_error || null === $results ) {
$results = array();
}
$wpdb->suppress_errors( false );
if ( ! empty( $results ) ) {
\ob_start();
?>
true,
'mail_body' => $message,
'table_name' => $table_name,
)
);
} else {
return new \WP_Error(
'empty_row',
__( 'No record found.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
}
/**
* Loads single row data..
*
* @param mixed $id - The ID of the row to load.
*
* @return array|\WP_Error
*
* @since 3.2.0
*/
public static function load_row_data( $id ) {
$table_name = self::get_name();
if ( '' === trim( $table_name ) ) {
return new \WP_Error(
'edit_row',
__( 'Table name is not provided.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
if ( ! self::check_table_exists( $table_name ) ) {
return new \WP_Error(
'edit_row',
__( 'Table does not exist.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
if ( empty( $id ) ) {
return new \WP_Error(
'edit_row',
__( 'ID is not provided or wrong.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
global $wpdb;
$query = $wpdb->prepare(
'SELECT * FROM `' . self::get_name() . '` WHERE `' . self::get_real_id_name() . '` = %s;', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$id
);
$wpdb->suppress_errors( true );
$results = $wpdb->get_results( $query, \ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
if ( '' !== $wpdb->last_error || null === $results ) {
$results = array();
}
$wpdb->suppress_errors( false );
if ( ! empty( $results ) ) {
return $results[0];
} else {
return new \WP_Error(
'empty_row',
__( 'No record found.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
}
/**
* Extracts single row data from given table and shows it in HTML format.
*
* @param \WP_REST_Request $request - The request object.
*
* @return \WP_REST_Response|\WP_Error
*
* @since 3.2.0
*/
public static function extract_fatals_row_data( \WP_REST_Request $request ) {
// Enforce capability check for data disclosure.
if ( ! \current_user_can( 'manage_options' ) ) {
return new \WP_Error(
'forbidden',
__( 'Sorry, you are not allowed to perform this action.', '0-day-analytics' ),
array( 'status' => 403 )
);
}
$table_name = $request->get_param( 'table_name' );
if ( '' === trim( $table_name ) ) {
return new \WP_Error(
'show_row',
__( 'Table name is not provided.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
if ( ! self::check_table_exists( $table_name ) ) {
return new \WP_Error(
'show_row',
__( 'Table does not exist.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
if ( WP_Fatals_Entity::get_table_name() !== $table_name ) {
return new \WP_Error(
'show_row',
__( 'Wrong call.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
$id = $request->get_param( 'id' );
if ( empty( $id ) ) {
return new \WP_Error(
'show_row',
__( 'ID is not provided or wrong.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
self::init( $table_name );
global $wpdb;
$query = $wpdb->prepare(
'SELECT * FROM `' . self::get_name() . '` WHERE `' . self::get_real_id_name() . '` = %s;', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$id
);
$wpdb->suppress_errors( true );
$results = $wpdb->get_results( $query, \ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
if ( '' !== $wpdb->last_error || null === $results ) {
$results = array();
}
$wpdb->suppress_errors( false );
if ( ! empty( $results ) ) {
\ob_start();
?>
|
|
|
$value ) {
if ( ! \in_array( $key, \array_keys( WP_Fatals_Entity::get_details_columns() ) ) ) {
continue;
}
?>
|
|
\wp_create_nonce( 'source-view' ),
'action' => 'log_source_view',
);
$query_array['error_file'] = $value;
$query_array['error_line'] = 1;
if ( isset( $results[0]['error_line'] ) && ! empty( $results[0]['error_line'] ) ) {
$query_array['error_line'] = $results[0]['error_line'];
}
$query_array['TB_iframe'] = 'true';
$view_url = \esc_url_raw(
\add_query_arg( $query_array, \admin_url( 'admin-ajax.php' ) )
);
$title = __( 'Viewing: ', '0-day-analytics' ) . $query_array['error_file'];
$anchor = '';
$anchor .= esc_html( $query_array['error_file'] . ':' . $query_array['error_line'] );
$anchor .= '
';
$value = ' ' . $anchor;
// return $source_link;
}
?>
|
format( 'Y-m-d' );
$tomorrow_local = ( new \DateTimeImmutable( 'tomorrow', $timezone_local ) )->format( 'Y-m-d' );
$yesterday_local = ( new \DateTimeImmutable( 'yesterday', $timezone_local ) )->format( 'Y-m-d' );
// If the offset of the date of the event is different from the offset of the site, add a marker.
if ( \get_date_from_gmt( $event_datetime_utc, 'P' ) !== get_date_from_gmt( 'now', 'P' ) ) {
$time_format .= ' (P)';
}
$event_time_local = \get_date_from_gmt( $event_datetime_utc, $time_format );
if ( $event_local === $today_local ) {
$date = sprintf(
/* translators: %s: Time */
__( 'Today at %s', '0-day-analytics' ),
$event_time_local,
);
} elseif ( $event_local === $tomorrow_local ) {
$date = sprintf(
/* translators: %s: Time */
__( 'Tomorrow at %s', '0-day-analytics' ),
$event_time_local,
);
} elseif ( $event_local === $yesterday_local ) {
$date = sprintf(
/* translators: %s: Time */
__( 'Yesterday at %s', '0-day-analytics' ),
$event_time_local,
);
} else {
$date = sprintf(
/* translators: 1: Date, 2: Time */
__( '%1$s at %2$s', '0-day-analytics' ),
\get_date_from_gmt( $event_datetime_utc, 'F jS' ),
$event_time_local,
);
}
$time = sprintf(
'',
\esc_attr( gmdate( 'c', $item['date_added'] ) ),
\esc_html( $date )
);
$until = $item['date_added'] - time();
if ( $until < 0 ) {
$ago = sprintf(
/* translators: %s: Time period, for example "8 minutes" */
__( '%s ago', '0-day-analytics' ),
WP_Helper::interval( abs( $until ) )
);
$value = sprintf(
' %s
%s',
\esc_html( $ago ),
$time,
);
} elseif ( 0 === $until ) {
$value = __( 'Now', '0-day-analytics' );
} else {
$value = sprintf(
/* translators: %s: Time period, for example "8 minutes" */
__( 'In %s', '0-day-analytics' ),
WP_Helper::interval( $until ),
);
}
// $value = sprintf(
// ' %s
%s',
// \esc_html( $in ),
// $time,
// );
?>
|
|
true,
'mail_body' => $message,
'table_name' => $table_name,
)
);
} else {
return new \WP_Error(
'empty_row',
__( 'No record found.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
}
/**
* Extracts Hook Capture record data for the REST API.
*
* @param \WP_REST_Request $request - The REST request object.
*
* @return \WP_REST_Response|\WP_Error
*
* @since 4.5.0
*/
public static function extract_hook_capture_row_data( \WP_REST_Request $request ) {
// Enforce capability check for data disclosure.
if ( ! \current_user_can( 'manage_options' ) ) {
return new \WP_Error(
'forbidden',
__( 'Sorry, you are not allowed to perform this action.', '0-day-analytics' ),
array( 'status' => 403 )
);
}
$table_name = $request->get_param( 'table_name' );
if ( '' === trim( $table_name ) ) {
return new \WP_Error(
'show_row',
__( 'Table name is not provided.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
if ( ! self::check_table_exists( $table_name ) ) {
return new \WP_Error(
'show_row',
__( 'Table does not exist.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
if ( Hooks_Capture_Entity::get_table_name() !== $table_name ) {
return new \WP_Error(
'show_row',
__( 'Wrong call.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
$id = $request->get_param( 'id' );
if ( empty( $id ) ) {
return new \WP_Error(
'show_row',
__( 'ID is not provided or wrong.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
self::init( $table_name );
global $wpdb;
$query = $wpdb->prepare(
'SELECT * FROM `' . self::get_name() . '` WHERE `' . self::get_real_id_name() . '` = %d;', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$id
);
$wpdb->suppress_errors( true );
$results = $wpdb->get_results( $query, \ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
if ( '' !== $wpdb->last_error || null === $results ) {
$results = array();
}
$wpdb->suppress_errors( false );
if ( ! empty( $results ) ) {
$hook_data = $results[0];
// Decode JSON fields.
$parameters = ! empty( $hook_data['parameters'] ) ? json_decode( $hook_data['parameters'], true ) : array();
$output = ! empty( $hook_data['output'] ) ? json_decode( $hook_data['output'], true ) : array();
$backtrace = ! empty( $hook_data['backtrace'] ) ? json_decode( $hook_data['backtrace'], true ) : array();
\ob_start();
?>
|
|
|
|
|
|
|
|
|
blogname . ' (' . $blog->domain . ')' ) : \esc_html( $hook_data['blog_id'] );
?>
|
|
|
|
' . \esc_html__( 'N/A', '0-day-analytics' ) . ''; ?> |
|
s |
|
|
|
' : ''; ?> |
';
foreach ( $backtrace_data as $frame ) {
$call_display = '';
if ( ! empty( $frame['class'] ) ) {
$call_display = \esc_html( $frame['class'] );
if ( ! empty( $frame['function'] ) ) {
$call_display .= '::' . \esc_html( $frame['function'] ) . '()';
}
} elseif ( ! empty( $frame['function'] ) ) {
$call_display = \esc_html( $frame['function'] ) . '()';
}
if ( ! empty( $call_display ) ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Already escaped above with esc_html().
echo '' . $call_display . '
';
}
if ( ! empty( $frame['full_path'] ) ) {
$line_number = ! empty( $frame['line'] ) ? (int) $frame['line'] : 1;
$query_array = array(
'_wpnonce' => \wp_create_nonce( 'source-view' ),
'action' => 'log_source_view',
'error_file' => $frame['full_path'],
'error_line' => $line_number,
'TB_iframe' => 'true',
);
$view_url = \esc_url_raw(
\add_query_arg( $query_array, \admin_url( 'admin-ajax.php' ) )
);
$title = __( 'Viewing: ', '0-day-analytics' ) . $frame['full_path'];
echo '';
} elseif ( ! empty( $frame['file'] ) || ! empty( $frame['line'] ) ) {
echo '';
echo ! empty( $frame['file'] ) ? \esc_html( $frame['file'] ) : '';
echo ! empty( $frame['line'] ) ? ':' . \esc_html( (string) $frame['line'] ) : '';
echo '
';
}
}
echo ' ';
} else {
echo '';
echo \esc_html( $backtrace );
echo '
';
}
?>
true,
'mail_body' => $message,
'table_name' => $table_name,
)
);
} else {
return new \WP_Error(
'empty_row',
__( 'No record found.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
}
/**
* Extracts URL record data for the REST API.
*
* @param \WP_REST_Request $request - The REST request object.
*
* @return \WP_REST_Response|\WP_Error
*
* @since 4.9.0
*/
public static function extract_url_record_data( \WP_REST_Request $request ) {
// Enforce capability check for data disclosure.
if ( ! \current_user_can( 'manage_options' ) ) {
return new \WP_Error(
'forbidden',
__( 'Sorry, you are not allowed to perform this action.', '0-day-analytics' ),
array( 'status' => 403 )
);
}
$table_name = $request->get_param( 'table_name' );
if ( '' === trim( $table_name ) ) {
return new \WP_Error(
'show_row',
__( 'Table name is not provided.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
if ( ! self::check_table_exists( $table_name ) ) {
return new \WP_Error(
'show_row',
__( 'Table does not exist.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
if ( Urls_Log_Entity::get_table_name() !== $table_name ) {
return new \WP_Error(
'show_row',
__( 'Wrong call.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
$id = $request->get_param( 'id' );
if ( empty( $id ) ) {
return new \WP_Error(
'show_row',
__( 'ID is not provided or wrong.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
self::init( $table_name );
global $wpdb;
$query = $wpdb->prepare(
'SELECT * FROM `' . self::get_name() . '` WHERE `' . self::get_real_id_name() . '` = %d;', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$id
);
$wpdb->suppress_errors( true );
$results = $wpdb->get_results( $query, \ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
if ( '' !== $wpdb->last_error || null === $results ) {
$results = array();
}
$wpdb->suppress_errors( false );
if ( ! empty( $results ) && isset( $results[0] ) ) {
$url_data = $results[0];
// Decode JSON fields.
$assets_data = ! empty( $url_data['assets_data'] ) ? json_decode( $url_data['assets_data'], true ) : array();
$pagespeed_data = ! empty( $url_data['pagespeed_data'] ) ? json_decode( $url_data['pagespeed_data'], true ) : array();
\ob_start();
?>
|
|
|
|
|
|
|
|
|
|
|
' . \esc_html__( 'N/A', '0-day-analytics' ) . ''; ?> |
|
' . \esc_html__( 'N/A', '0-day-analytics' ) . ''; ?> |
|
|
|
0 ) {
if ( in_array( $page_type, array( 'single_post', 'page', 'front_page' ), true ) ) {
$url = \admin_url( 'post.php?post=' . $page_id . '&action=edit' );
echo '' . $page_id . '';
} elseif ( 'category' === $page_type ) {
$url = \admin_url( 'term.php?taxonomy=category&tag_ID=' . $page_id );
echo '' . $page_id . '';
} elseif ( 'tag' === $page_type ) {
$url = \admin_url( 'edit-tags.php?taxonomy=post_tag&tag_ID=' . $page_id );
echo '' . $page_id . '';
} elseif ( 'taxonomy' === $page_type ) {
$term = \get_term( $page_id );
if ( $term && ! is_wp_error( $term ) ) {
$url = \admin_url( 'edit-tags.php?taxonomy=' . $term->taxonomy . '&tag_ID=' . $page_id );
echo '' . $page_id . '';
} else {
echo \esc_html( $page_id );
}
} elseif ( 'attachment' === $page_type ) {
$url = \admin_url( 'post.php?post=' . $page_id . '&action=edit' );
echo '' . $page_id . '';
} else {
echo \esc_html( $page_id );
}
} else {
echo \esc_html( $page_id );
}
?>
|
|
|
|
|
true,
'mail_body' => $message,
'table_name' => $table_name,
'assets_data' => $assets_data,
'pagespeed_data' => array(
'mobile_data' => $mobile_data,
'desktop_data' => $desktop_data,
),
'url_data' => $url_data,
)
);
} else {
return new \WP_Error(
'empty_row',
__( 'No record found.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
}
/**
* Collect assets for a URL record.
*
* @param \WP_REST_Request $request REST request object.
*
* @return \WP_REST_Response|\WP_Error
*
* @since 4.9.0
*/
public static function collect_assets( \WP_REST_Request $request ) {
// Capability check.
if ( ! \current_user_can( 'manage_options' ) ) {
return new \WP_Error(
'forbidden',
__( 'Sorry, you are not allowed to perform this action.', '0-day-analytics' ),
array( 'status' => 403 )
);
}
$id = (int) $request->get_param( 'id' );
if ( ! $id ) {
return new \WP_Error(
'invalid_id',
__( 'Invalid ID provided.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
// Get URL record.
$record = Urls_Log_Entity::load( 'id=%d', array( $id ) );
if ( ! $record ) {
return new \WP_Error(
'not_found',
__( 'URL record not found.', '0-day-analytics' ),
array( 'status' => 404 )
);
}
$url = $record['url'];
// Fetch the URL content.
$response = \wp_remote_get(
$url,
array(
'timeout' => 30,
'redirection' => 5,
'user-agent' => 'Advanced Analytics Asset Collector/1.0',
)
);
if ( \is_wp_error( $response ) ) {
return new \WP_Error(
'fetch_failed',
__( 'Failed to fetch URL: ', '0-day-analytics' ) . $response->get_error_message(),
array( 'status' => 500 )
);
}
$html = \wp_remote_retrieve_body( $response );
// Parse HTML to extract assets.
$assets = self::parse_assets_from_html( $html, $url );
// Update the record.
$updated = Urls_Log_Entity::update_assets_data( $id, $assets );
if ( ! $updated ) {
return new \WP_Error(
'update_failed',
__( 'Failed to update assets data.', '0-day-analytics' ),
array( 'status' => 500 )
);
}
return \rest_ensure_response(
array(
'success' => true,
'message' => __( 'Assets collected successfully.', '0-day-analytics' ),
'data' => $assets,
)
);
}
/**
* Parse assets from HTML content.
*
* @param string $html HTML content.
* @param string $base_url Base URL for resolving relative paths.
*
* @return array
*
* @since 4.9.0
*/
private static function parse_assets_from_html( string $html, string $base_url ): array {
$assets = array(
'js' => array(),
'css' => array(),
'media' => array(),
);
// Suppress warnings from malformed HTML.
libxml_use_internal_errors( true );
$dom = new \DOMDocument();
$dom->loadHTML( $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
libxml_clear_errors();
// Extract JavaScript files.
$scripts = $dom->getElementsByTagName( 'script' );
foreach ( $scripts as $script ) {
$src = $script->getAttribute( 'src' );
if ( $src ) {
$full_url = self::resolve_url( $src, $base_url );
$size = self::get_remote_file_size( $full_url );
$assets['js'][] = array(
'url' => $full_url,
'size' => $size,
);
}
}
// Extract CSS files.
$links = $dom->getElementsByTagName( 'link' );
foreach ( $links as $link ) {
$rel = $link->getAttribute( 'rel' );
$href = $link->getAttribute( 'href' );
if ( 'stylesheet' === $rel && $href ) {
$full_url = self::resolve_url( $href, $base_url );
$size = self::get_remote_file_size( $full_url );
$assets['css'][] = array(
'url' => $full_url,
'size' => $size,
);
}
}
// Extract media files (img, video, audio).
$images = $dom->getElementsByTagName( 'img' );
foreach ( $images as $img ) {
$src = $img->getAttribute( 'src' );
if ( $src ) {
$full_url = self::resolve_url( $src, $base_url );
$size = self::get_remote_file_size( $full_url );
$assets['media'][] = array(
'type' => 'image',
'url' => $full_url,
'size' => $size,
);
}
}
$videos = $dom->getElementsByTagName( 'video' );
foreach ( $videos as $video ) {
$src = $video->getAttribute( 'src' );
if ( $src ) {
$full_url = self::resolve_url( $src, $base_url );
$size = self::get_remote_file_size( $full_url );
$assets['media'][] = array(
'type' => 'video',
'url' => $full_url,
'size' => $size,
);
}
// Also check source tags within video.
$sources = $video->getElementsByTagName( 'source' );
foreach ( $sources as $source ) {
$src = $source->getAttribute( 'src' );
if ( $src ) {
$full_url = self::resolve_url( $src, $base_url );
$size = self::get_remote_file_size( $full_url );
$assets['media'][] = array(
'type' => 'video',
'url' => $full_url,
'size' => $size,
);
}
}
}
return $assets;
}
/**
* Resolve a relative URL to an absolute URL.
*
* @param string $url URL to resolve.
* @param string $base_url Base URL.
* @return string
*
* @since 4.9.0
*/
private static function resolve_url( string $url, string $base_url ): string {
// Already absolute.
if ( parse_url( $url, PHP_URL_SCHEME ) !== null ) {
return $url;
}
// Protocol-relative URL.
if ( strpos( $url, '//' ) === 0 ) {
return parse_url( $base_url, PHP_URL_SCHEME ) . ':' . $url;
}
// Absolute path.
if ( strpos( $url, '/' ) === 0 ) {
$base_parts = parse_url( $base_url );
return $base_parts['scheme'] . '://' . $base_parts['host'] . $url;
}
// Relative path.
return rtrim( $base_url, '/' ) . '/' . ltrim( $url, '/' );
}
/**
* Get remote file size.
*
* For assets hosted on this site the URL is mapped to the local filesystem
* so no HTTP request is needed. For external assets a HEAD request is tried
* first; if the server omits Content-Length a Range request is used as a
* fallback — most servers populate Content-Range even when HEAD does not
* include the size.
*
* @param string $url URL to check.
* @return int|null File size in bytes, or null if unavailable.
*
* @since 4.9.0
*/
private static function get_remote_file_size( string $url ) {
// ── Local assets ────────────────────────────────────────────────────
// Map same-site URLs directly to the filesystem — zero HTTP overhead
// and guaranteed accuracy.
$site_url = \site_url();
if ( strpos( $url, $site_url ) === 0 ) {
$relative = str_replace( $site_url, '', $url );
// Strip query strings / fragments before path resolution.
$relative = strtok( $relative, '?#' );
$local_path = ABSPATH . ltrim( $relative, '/' );
$real_path = realpath( $local_path );
$abspath = realpath( ABSPATH );
// Guard against path-traversal: the resolved path must stay inside ABSPATH.
if (
false !== $real_path
&& false !== $abspath
&& strpos( $real_path, $abspath ) === 0
&& is_file( $real_path )
) {
return filesize( $real_path );
}
}
// ── External assets — HEAD request ───────────────────────────────
$head_response = \wp_remote_head( $url, array( 'timeout' => 5 ) );
if ( ! \is_wp_error( $head_response ) ) {
$headers = \wp_remote_retrieve_headers( $head_response );
if ( isset( $headers['content-length'] ) ) {
return (int) $headers['content-length'];
}
}
// ── External assets — Range fallback ─────────────────────────────
// Request only the first byte; servers that support Range return a
// Content-Range header of the form "bytes 0-0/TOTAL" even when they
// don't include Content-Length on HEAD responses.
$range_response = \wp_remote_get(
$url,
array(
'timeout' => 5,
'headers' => array( 'Range' => 'bytes=0-0' ),
)
);
if ( ! \is_wp_error( $range_response ) ) {
$range_headers = \wp_remote_retrieve_headers( $range_response );
// e.g. "Content-Range: bytes 0-0/123456".
if (
isset( $range_headers['content-range'] )
&& preg_match( '/\/\s*(\d+)$/', $range_headers['content-range'], $m )
) {
return (int) $m[1];
}
// Server ignored the Range header but still sent Content-Length.
if ( isset( $range_headers['content-length'] ) ) {
return (int) $range_headers['content-length'];
}
}
return null;
}
/**
* Collect PageSpeed data for a URL record.
*
* @param \WP_REST_Request $request REST request object.
*
* @return \WP_REST_Response|\WP_Error
*
* @since 4.9.0
*/
public static function collect_pagespeed( \WP_REST_Request $request ) {
// Capability check.
if ( ! \current_user_can( 'manage_options' ) ) {
return new \WP_Error(
'forbidden',
__( 'Sorry, you are not allowed to perform this action.', '0-day-analytics' ),
array( 'status' => 403 )
);
}
$id = (int) $request->get_param( 'id' );
if ( ! $id ) {
return new \WP_Error(
'invalid_id',
__( 'Invalid ID provided.', '0-day-analytics' ),
array( 'status' => 400 )
);
}
// Get URL record.
$record = Urls_Log_Entity::load( 'id=%d', array( $id ) );
if ( ! $record ) {
return new \WP_Error(
'not_found',
__( 'URL record not found.', '0-day-analytics' ),
array( 'status' => 404 )
);
}
$url = $record['url'];
// Fetch PageSpeed data using the existing logic.
$pagespeed_data = self::fetch_pagespeed_for_url( $url );
if ( ! $pagespeed_data ) {
return new \WP_Error(
'fetch_failed',
__( 'Failed to fetch PageSpeed data. Check API key and try again.', '0-day-analytics' ),
array( 'status' => 500 )
);
}
// Update the record.
$updated = Urls_Log_Entity::update_pagespeed_data( $id, $pagespeed_data );
if ( ! $updated ) {
return new \WP_Error(
'update_failed',
__( 'Failed to update PageSpeed data.', '0-day-analytics' ),
array( 'status' => 500 )
);
}
// Detect per-strategy errors so the front-end can surface them to the user.
$pagespeed_errors = array();
foreach ( array( 'mobile_data', 'desktop_data' ) as $strategy_key ) {
if (
isset( $pagespeed_data[ $strategy_key ]['error'] ) &&
$pagespeed_data[ $strategy_key ]['error'] &&
! empty( $pagespeed_data[ $strategy_key ]['error_message'] )
) {
$device = 'mobile_data' === $strategy_key ? 'mobile' : 'desktop';
$pagespeed_errors[ $device ] = $pagespeed_data[ $strategy_key ]['error_message'];
}
}
return \rest_ensure_response(
array(
'success' => true,
'message' => empty( $pagespeed_errors )
? __( 'PageSpeed data collected successfully.', '0-day-analytics' )
: __( 'PageSpeed collection completed with errors.', '0-day-analytics' ),
'data' => $pagespeed_data,
'pagespeed_errors' => $pagespeed_errors,
)
);
}
/**
* Fetch PageSpeed data for a specific URL.
*
* @param string $url URL to analyze.
* @return array|false
*
* @since 4.9.0
*/
public static function fetch_pagespeed_for_url( string $url ) {
$api = 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed';
$api_key = Settings::get_option( 'pagespeed_api_key' );
if ( empty( $api_key ) ) {
return false;
}
$parameters = array(
'url' => rawurlencode( $url ),
'key' => $api_key,
'category' => array(
'ACCESSIBILITY',
'BEST_PRACTICES',
'PERFORMANCE',
'PWA',
'SEO',
),
);
$query = $api . '?';
foreach ( $parameters as $key => $value ) {
if ( 'category' === $key ) {
foreach ( $value as $v ) {
$query .= "$key=$v&";
}
continue;
}
$query .= "$key=$value&";
}
$queries['mobile'] = $query . 'strategy=MOBILE';
$queries['desktop'] = $query . 'strategy=DESKTOP';
$args = array(
'timeout' => 300,
);
$response['mobile'] = \wp_remote_get( $queries['mobile'], $args );
$response['desktop'] = \wp_remote_get( $queries['desktop'], $args );
if ( \is_wp_error( $response['mobile'] ) || \is_wp_error( $response['desktop'] ) ) {
return false;
}
$raw_response['mobile'] = \wp_remote_retrieve_body( $response['mobile'] );
$raw_response['desktop'] = \wp_remote_retrieve_body( $response['desktop'] );
$mobile_response = json_decode( $raw_response['mobile'], true );
$desktop_response = json_decode( $raw_response['desktop'], true );
$mobile_data = PageSpeed_List::prepare_pagespeed_api_response( $mobile_response );
$desktop_data = PageSpeed_List::prepare_pagespeed_api_response( $desktop_response );
// Inject the correct formFactor so the front-end can target the right
// container even when the response is an error with no configSettings.
if ( ! empty( $mobile_data['error'] ) ) {
$mobile_data['lighthouseResult']['configSettings']['formFactor'] = 'mobile';
}
if ( ! empty( $desktop_data['error'] ) ) {
$desktop_data['lighthouseResult']['configSettings']['formFactor'] = 'desktop';
}
return array(
'mobile_data' => $mobile_data,
'desktop_data' => $desktop_data,
);
}
/**
* Formats the value for HTML output
*
* @param mixed $value - The value to be formatted.
*
* @return string
*
* @since 3.8.0
*/
public static function format_value_for_html( $value ) {
// Try to decode JSON if it's a string.
if ( is_string( $value ) ) {
$decoded = json_decode( $value, true );
// Handle valid JSON (objects or arrays).
if ( json_last_error() === JSON_ERROR_NONE ) {
$value = $decoded;
} elseif ( preg_match( '/^[aOs]:[0-9]+:/', $value ) ) { // Try unserialize if not valid JSON but looks like serialized PHP.
$unserialized = @unserialize( $value, array( 'allowed_classes' => false ) );
if ( false !== $unserialized || 'b:0;' === $value ) {
$value = $unserialized;
}
}
}
// Format by type.
if ( is_array( $value ) ) {
// Pretty print arrays or objects.
$formatted = '' . \esc_html( print_r( $value, true ) ) . '
';
} elseif ( is_object( $value ) ) {
$formatted = '' . \esc_html( print_r( $value, true ) ) . '
';
} elseif ( is_bool( $value ) ) {
$formatted = $value ? 'true' : 'false';
} elseif ( is_null( $value ) ) {
$formatted = 'null';
} elseif ( is_numeric( $value ) ) {
$formatted = \esc_html( (string) $value );
} else {
// Fallback to escaped plain string.
$formatted = \esc_html( (string) $value );
}
return $formatted;
}
/**
* Smart insert/update that respects the destination table's column types.
*
* @param string $table_name Full table name (may include $wpdb->prefix).
* @param array $data Associative array column => value (raw values).
* @param array $where Optional associative array column=>value used for update condition. If provided, function performs update; otherwise insert.
*
* @return int|bool|WP_Error Insert ID on insert, true on successful update, WP_Error on failure.
*
* @since 3.9.4
*/
public static function insert_row_record( $table_name, array $data, ?array $where = null ) {
if ( ! self::check_table_exists( $table_name ) ) {
return new \WP_Error( 'table_not_found', 'Table not found.' );
}
self::init( $table_name );
// Fetch column metadata.
$columns = self::get_columns_info();
// Build a map: column_name => column_meta.
$colmap = array();
foreach ( $columns as $col ) {
$colmap[ $col['Field'] ] = $col; // ['Type'], ['Null'], ['Key'], ['Default'], ['Extra']
}
// sanitize incoming data: only columns that exist and have safe names.
$prepared_data = array();
$formats = array();
foreach ( $data as $col => $val ) {
if ( ! is_string( $col ) || ! isset( $colmap[ $col ] ) ) {
// skip columns that do not exist in table.
continue;
}
$ctype = $colmap[ $col ]['Type']; // e.g. "int(11) unsigned", "varchar(255)", "enum('a','b')", "json".
$null_allowed = ( 'YES' === $colmap[ $col ]['Null'] );
// Normalize value to column type.
$normalized = null;
$use_format = '%s'; // default for wpdb insert format.
// Helper to extract base type and extra info.
$lower_type = strtolower( $ctype );
// INT types.
if ( preg_match( '/^(tinyint|smallint|mediumint|int|bigint)\b/', $lower_type, $m ) ) {
// treat boolean-like tinyint(1) as integer 0/1 if scalar boolean or numeric.
if ( is_bool( $val ) ) {
$normalized = $val ? 1 : 0;
} elseif ( is_numeric( $val ) ) {
$normalized = intval( $val );
} elseif ( is_null( $val ) && $null_allowed ) {
$normalized = null;
} else {
// attempt cast from string.
$normalized = intval( $val );
}
$use_format = '%d';
} elseif ( preg_match( '/^(float|double|decimal)\b/', $lower_type, $m ) ) { // FLOAT/DOUBLE/DECIMAL.
if ( is_numeric( $val ) ) {
$normalized = floatval( $val );
} elseif ( is_null( $val ) && $null_allowed ) {
$normalized = null;
} else {
// try numeric cast.
$normalized = floatval( $val );
}
$use_format = '%f';
} elseif ( preg_match( '/\b(date|datetime|timestamp|time|year)\b/', $lower_type, $m ) ) {// DATE / DATETIME / TIMESTAMP / TIME / YEAR.
if ( is_null( $val ) && $null_allowed ) {
$normalized = null;
} elseif ( $val instanceof \DateTime ) {
$type = $m[1];
switch ( $type ) {
case 'date':
$normalized = $val->format( 'Y-m-d' );
break;
case 'datetime':
case 'timestamp':
$normalized = $val->format( 'Y-m-d H:i:s' );
break;
case 'time':
$normalized = $val->format( 'H:i:s' );
break;
case 'year':
$normalized = $val->format( 'Y' );
break;
}
} elseif ( is_numeric( $val ) ) {
// treat as unix timestamp.
$type = $m[1];
$ts = intval( $val );
if ( 'date' === $type ) {
$normalized = gmdate( 'Y-m-d', $ts );
} elseif ( 'time' === $type ) {
$normalized = gmdate( 'H:i:s', $ts );
} elseif ( 'year' === $type ) {
$normalized = gmdate( 'Y', $ts );
} else {
$normalized = gmdate( 'Y-m-d H:i:s', $ts );
}
} elseif ( is_string( $val ) ) {
// try to parse common formats; be conservative: try strtotime.
$ts = strtotime( $val );
if ( false !== $ts ) {
$type = $m[1];
if ( 'date' === $type ) {
$normalized = gmdate( 'Y-m-d', $ts );
} elseif ( 'time' === $type ) {
$normalized = gmdate( 'H:i:s', $ts );
} elseif ( 'year' === $type ) {
$normalized = gmdate( 'Y', $ts );
} else {
$normalized = gmdate( 'Y-m-d H:i:s', $ts );
}
} else {
// if can't parse, leave raw string (caller must ensure correctness).
$normalized = (string) $val;
}
} else {
$normalized = (string) $val;
}
$use_format = '%s';
} elseif ( preg_match( '/\bjson\b/', $lower_type ) ) { // JSON column (MySQL JSON type) -- store JSON string.
$json = wp_json_encode( $val );
if ( false !== $json && json_last_error() === JSON_ERROR_NONE ) {
$normalized = $json;
} else {
// fallback to string cast.
$normalized = is_scalar( $val ) || is_null( $val ) ? $val : wp_json_encode( (string) $val );
}
$use_format = '%s';
} elseif ( preg_match( '/^enum\((.*)\)$/', $lower_type, $m ) || preg_match( '/^set\((.*)\)$/', $lower_type, $m ) ) { // ENUM / SET -> try to validate against allowed values.
$raw = $m[1];
// parse allowed values 'a','b','c'.
$allowed = array();
if ( preg_match_all( "/'((?:[^'\\\\]|\\\\.)*)'/", $raw, $vals ) ) {
foreach ( $vals[1] as $v ) {
$allowed[] = stripslashes( $v );
}
}
// if value is array for SET, join by comma.
if ( is_array( $val ) ) {
$candidate = implode( ',', array_map( 'strval', $val ) );
} else {
$candidate = (string) $val;
}
// validate: every value in candidate must be allowed (for SET split by comma).
$valid = true;
if ( strpos( $candidate, ',' ) !== false ) {
$parts = array_map( 'trim', explode( ',', $candidate ) );
} else {
$parts = array( $candidate );
}
foreach ( $parts as $p ) {
if ( '' === $p ) {
continue;
}
if ( ! in_array( $p, $allowed, true ) ) {
$valid = false;
break;
}
}
if ( $valid ) {
$normalized = $candidate;
} else {
// invalid enum/set value: reject.
return new \WP_Error( 'invalid_enum', "Invalid value for {$col}." );
}
$use_format = '%s';
} elseif ( preg_match( '/\b(blob|binary|varbinary)\b/', $lower_type ) ) { // BINARY / BLOB types => store binary safely; if non-scalar convert to base64(serialized).
if ( is_null( $val ) && $null_allowed ) {
$normalized = null;
} elseif ( is_string( $val ) ) {
$normalized = $val;
} else {
// encode complex structure to base64 serialized.
$normalized = base64_encode( serialize( $val ) );
}
$use_format = '%s';
} else { // TEXT / CHAR / VARCHAR and default fallback
// If scalar string/number/null use as-is (cast to string for safety).
if ( is_null( $val ) && $null_allowed ) {
$normalized = null;
} elseif ( is_scalar( $val ) ) {
$normalized = (string) $val;
} else {
// try JSON encode for arrays/objects; prefer JSON so other apps can recognize JSON if expected.
$json = wp_json_encode( $val );
if ( false !== $json && json_last_error() === JSON_ERROR_NONE ) {
$normalized = $json;
} else {
// fallback to base64(serialized).
$normalized = base64_encode( serialize( $val ) );
}
}
$use_format = '%s';
}
// Enforce maximum length if column has a length (varchar(n)).
if ( preg_match( '/^(?:var)?char\((\d+)\)/', $lower_type, $m ) || preg_match( '/^varchar\((\d+)\)/', $lower_type, $m ) ) {
$max = intval( $m[1] );
if ( is_string( $normalized ) && strlen( $normalized ) > $max ) {
// truncate to max length (preserve valid utf8 boundary).
$normalized = mb_strcut( $normalized, 0, $max, 'UTF-8' );
}
}
$prepared_data[ $col ] = $normalized;
$formats[ $col ] = $use_format;
}
if ( empty( $prepared_data ) ) {
return new \WP_Error( 'no_valid_columns', 'No valid columns provided.' );
}
// Format array for wpdb insert/update: an ordered array of format values corresponding to values.
$format_ordered = array();
foreach ( $prepared_data as $k => $v ) {
$format_ordered[] = isset( $formats[ $k ] ) ? $formats[ $k ] : '%s';
}
global $wpdb;
// If $where is provided -> update, else insert.
if ( ! empty( $where ) && is_array( $where ) ) {
// sanitize where keys and values similarly.
$prepared_where = array();
$where_formats = array();
foreach ( $where as $col => $val ) {
if ( ! is_string( $col ) || ! isset( $colmap[ $col ] ) ) {
continue;
}
// reuse a simple normalization: cast to scalar or string.
if ( is_null( $val ) ) {
$prepared_where[ $col ] = null;
$where_formats[] = '%s';
} elseif ( is_numeric( $val ) && preg_match( '/int|tinyint|smallint|mediumint|bigint/', strtolower( $colmap[ $col ]['Type'] ) ) ) {
$prepared_where[ $col ] = intval( $val );
$where_formats[] = '%d';
} else {
$prepared_where[ $col ] = (string) $val;
$where_formats[] = '%s';
}
}
if ( empty( $prepared_where ) ) {
return new \WP_Error( 'invalid_where', 'Invalid WHERE clause.' );
}
$updated = $wpdb->update(
$table_name,
$prepared_data,
$prepared_where,
array_values( $format_ordered ),
$where_formats
);
if ( false === $updated ) {
return new \WP_Error( 'update_failed', 'Database update failed.' );
}
// $updated may be 0 if no rows changed; return true to indicate success
return true;
} else {
$inserted = $wpdb->replace(
$table_name,
$prepared_data,
array_values( $format_ordered )
);
if ( false === $inserted ) {
return new \WP_Error( 'insert_failed', 'Database insert failed.' );
}
return (int) $wpdb->insert_id;
}
}
/**
* Get detailed table information including structure, indexes, foreign keys, and health.
*
* @param string $table_name The table name to get information for.
* @param \wpdb $connection Optional database connection.
*
* @return array|\WP_Error Array of table information or WP_Error on failure.
*
* @since 4.7.0
*/
public static function get_table_info( string $table_name, $connection = null ) {
if ( null !== $connection ) {
if ( $connection instanceof \wpdb ) {
$_wpdb = $connection;
} else {
global $wpdb;
$_wpdb = $wpdb;
}
} else {
global $wpdb;
$_wpdb = $wpdb;
}
// Validate table name.
if ( ! self::validate_table_name( $table_name ) ) {
return new \WP_Error( 'invalid_table', 'Invalid table name.' );
}
// Check if table exists.
if ( ! self::check_table_exists( $table_name, $_wpdb ) ) {
return new \WP_Error( 'table_not_found', 'Table does not exist.' );
}
$table_info = array();
try {
// Get table structure (columns) - using DESC command like other methods.
$table_info['structure'] = $_wpdb->get_results(
$_wpdb->prepare( 'DESC %i', $table_name ),
ARRAY_A
);
// Get indexes - using SHOW INDEX command.
$table_info['indexes'] = $_wpdb->get_results(
$_wpdb->prepare( 'SHOW INDEX FROM %i', $table_name ),
ARRAY_A
);
// Get foreign keys from information_schema - using prepared query.
$table_info['foreign_keys'] = $_wpdb->get_results(
$_wpdb->prepare(
'SELECT
kcu.constraint_name,
kcu.column_name,
kcu.referenced_table_name,
kcu.referenced_column_name
FROM information_schema.key_column_usage kcu
JOIN information_schema.table_constraints tc
ON kcu.constraint_name = tc.constraint_name
AND kcu.table_schema = tc.table_schema
WHERE kcu.table_schema = %s
AND kcu.table_name = %s
AND tc.constraint_type = %s',
$_wpdb->dbname,
$table_name,
'FOREIGN KEY'
),
ARRAY_A
);
// Get table health information from information_schema - using prepared query.
$health_info = $_wpdb->get_row(
$_wpdb->prepare(
'SELECT
table_rows as row_count,
data_length as data_size,
index_length as index_size,
data_free as fragmentation
FROM information_schema.tables
WHERE table_schema = %s AND table_name = %s',
$_wpdb->dbname,
$table_name
),
ARRAY_A
);
$table_info['health'] = array(
'row_count' => $health_info ? (int) $health_info['row_count'] : 0,
'data_size' => $health_info ? (int) $health_info['data_size'] : 0,
'index_size' => $health_info ? (int) $health_info['index_size'] : 0,
'needs_optimization' => $health_info && (int) $health_info['fragmentation'] > 0,
);
} catch ( \Exception $e ) {
return new \WP_Error( 'database_error', 'Database error: ' . $e->getMessage() );
}
return $table_info;
}
/**
* Optimize a database table.
*
* Runs MySQL OPTIMIZE TABLE to reclaim unused space and defragment the table.
*
* @param string $table_name The name of the table to optimize.
* @param \wpdb $connection Optional wpdb connection.
*
* @return array|\WP_Error Result array with status and message, or WP_Error on failure.
*
* @since 5.1.0
*/
public static function optimize_table( string $table_name, $connection = null ) {
if ( null !== $connection ) {
if ( $connection instanceof \wpdb ) {
$_wpdb = $connection;
} else {
global $wpdb;
$_wpdb = $wpdb;
}
} else {
global $wpdb;
$_wpdb = $wpdb;
}
// Validate table name.
if ( ! self::validate_table_name( $table_name ) ) {
return new \WP_Error( 'invalid_table', __( 'Invalid table name.', '0-day-analytics' ) );
}
// Check if table exists.
if ( ! self::check_table_exists( $table_name, $_wpdb ) ) {
return new \WP_Error( 'table_not_found', __( 'Table does not exist.', '0-day-analytics' ) );
}
try {
$result = $_wpdb->get_results(
$_wpdb->prepare( 'OPTIMIZE TABLE %i', $table_name ),
ARRAY_A
);
if ( empty( $result ) ) {
return new \WP_Error( 'optimize_failed', __( 'Optimization returned no result.', '0-day-analytics' ) );
}
$last_row = end( $result );
$status = isset( $last_row['Msg_type'] ) ? strtolower( $last_row['Msg_type'] ) : '';
$msg_text = isset( $last_row['Msg_text'] ) ? $last_row['Msg_text'] : '';
if ( 'error' === $status ) {
return new \WP_Error( 'optimize_error', $msg_text );
}
return array(
'status' => $status,
'message' => $msg_text,
);
} catch ( \Exception $e ) {
return new \WP_Error( 'database_error', $e->getMessage() );
}
}
}
}