new. Table: {prefix}rafgrid_audit. */ class Audit { public static function create_table() { global $wpdb; $table = $wpdb->prefix . 'rafgrid_audit'; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $charset = $wpdb->get_charset_collate(); dbDelta( "CREATE TABLE {$table} ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, post_id BIGINT UNSIGNED NOT NULL, column_key VARCHAR(191) NOT NULL DEFAULT '', old_value LONGTEXT NULL, new_value LONGTEXT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY post_id (post_id), KEY user_id (user_id) ) {$charset};" ); } /** * @param int $post_id Post ID. * @param string $column Column key. * @param mixed $old Old value. * @param mixed $new New value. */ private static ?Settings $settings_cache = null; public static function log( $post_id, $column, $old, $new ) { if ( null === self::$settings_cache ) { self::$settings_cache = new Settings(); } if ( ! self::$settings_cache->get( 'enable_history', 1 ) ) { return; } if ( self::stringify( $old ) === self::stringify( $new ) ) { return; } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->insert( $wpdb->prefix . 'rafgrid_audit', array( 'user_id' => get_current_user_id(), 'post_id' => absint( $post_id ), 'column_key' => substr( sanitize_text_field( $column ), 0, 191 ), 'old_value' => substr( self::stringify( $old ), 0, 2000 ), 'new_value' => substr( self::stringify( $new ), 0, 2000 ), 'created_at' => current_time( 'mysql' ), ), array( '%d', '%d', '%s', '%s', '%s', '%s' ) ); } /** * @param mixed $v Value. */ public static function stringify( $v ): string { if ( is_array( $v ) ) { return implode( ',', array_map( static fn( $x ) => is_array( $x ) ? wp_json_encode( $x ) : (string) $x, $v ) ); } if ( is_bool( $v ) ) { return $v ? '1' : '0'; } return null === $v ? '' : (string) $v; } /** * Recent entries. * * @param int $limit Limit. * @return array */ public static function recent( $limit = 50 ) { global $wpdb; $table = $wpdb->prefix . 'rafgrid_audit'; // Administrators see the whole log, everyone else only their own changes. if ( current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin's own table; live data that must not be served from cache. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY id DESC LIMIT %d', $table, $limit ), ARRAY_A ); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin's own table; live data that must not be served from cache. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE user_id = %d ORDER BY id DESC LIMIT %d', $table, get_current_user_id(), $limit ), ARRAY_A ); } return is_array( $rows ) ? $rows : array(); } /** * Roll back one audit entry (restore old value). * * @param int $id Audit ID. * @return true|\WP_Error */ public static function rollback( $id ) { global $wpdb; $table = $wpdb->prefix . 'rafgrid_audit'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin's own table; live data that must not be served from cache. $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $table, absint( $id ) ) ); if ( ! $row ) { return new \WP_Error( 'rafgrid_no_audit', __( 'Audit entry not found.', 'rafgrid' ) ); } if ( ! current_user_can( 'manage_options' ) && (int) $row->user_id !== get_current_user_id() ) { return new \WP_Error( 'rafgrid_forbidden', __( 'You cannot edit this item.', 'rafgrid' ) ); } // save_cell checks edit rights on the item and logs the rollback itself. return Saving::save_cell( (int) $row->post_id, $row->column_key, $row->old_value ); } }