455 lines
14 KiB
PHP
455 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* This file is part of Avatar Privacy.
|
|
*
|
|
* Copyright 2018-2022 Peter Putzer.
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
* ***
|
|
*
|
|
* @package mundschenk-at/avatar-privacy
|
|
* @license http://www.gnu.org/licenses/gpl-2.0.html
|
|
*/
|
|
|
|
namespace Avatar_Privacy\Data_Storage\Database;
|
|
|
|
use Avatar_Privacy\Data_Storage\Database\Table;
|
|
use Avatar_Privacy\Data_Storage\Network_Options;
|
|
|
|
/**
|
|
* The database table used for storing (anonymous) comment author data.
|
|
*
|
|
* @since 2.4.0 Extracted from Avatar_Privacy\Data_Storage\Database\Table.
|
|
*
|
|
* @author Peter Putzer <github@mundschenk.at>
|
|
*/
|
|
class Comment_Author_Table extends Table {
|
|
|
|
/**
|
|
* The table basename without the prefix.
|
|
*
|
|
* @var string
|
|
*/
|
|
const TABLE_BASENAME = 'avatar_privacy';
|
|
|
|
/**
|
|
* The minimum version not needing a table update.
|
|
*
|
|
* @var string
|
|
*/
|
|
const LAST_UPDATED = '2.4.0';
|
|
|
|
/**
|
|
* A column/field to placeholder mapping.
|
|
*
|
|
* @since 2.3.0
|
|
*
|
|
* @var string[]
|
|
*/
|
|
const COLUMN_FORMATS = [
|
|
'id' => '%d',
|
|
'email' => '%s',
|
|
'use_gravatar' => '%d',
|
|
'last_updated' => '%s',
|
|
'log_message' => '%s',
|
|
];
|
|
|
|
/**
|
|
* A list auto-update columns (e.g. date-/timestamps).
|
|
*
|
|
* @since 2.6.0
|
|
*
|
|
* @var string[]
|
|
*/
|
|
const AUTO_UPDATE_COLS = [
|
|
'last_updated',
|
|
];
|
|
|
|
/**
|
|
* The options handler.
|
|
*
|
|
* @var Network_Options
|
|
*/
|
|
private $network_options;
|
|
|
|
/**
|
|
* Creates a new instance.
|
|
*
|
|
* @since 2.3.0 Parameter $core added.
|
|
* @since 2.4.0 Parameter $core removed.
|
|
*
|
|
* @param Network_Options $network_options The network options handler.
|
|
*/
|
|
public function __construct( Network_Options $network_options ) {
|
|
parent::__construct( self::TABLE_BASENAME, self::LAST_UPDATED, self::COLUMN_FORMATS, self::AUTO_UPDATE_COLS );
|
|
|
|
$this->network_options = $network_options;
|
|
}
|
|
|
|
/**
|
|
* Sets up the table, including necessary data upgrades. The method is called
|
|
* on every page load.
|
|
*
|
|
* @since 2.4.0
|
|
*
|
|
* @param string $previous_version The previously installed plugin version.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function setup( $previous_version ) {
|
|
parent::setup( $previous_version );
|
|
|
|
// The table is set up correctly, but maybe we need to migrate some data
|
|
// from the global table on network installations.
|
|
$this->maybe_prepare_migration_queue();
|
|
$this->maybe_migrate_from_global_table();
|
|
}
|
|
|
|
/**
|
|
* Determines whether this (multisite) installation uses the global table.
|
|
* Result is ignored for single-site installations.
|
|
*
|
|
* @since 2.3.0 Visibility changed to public.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function use_global_table() {
|
|
$global_table = (bool) $this->network_options->get( Network_Options::USE_GLOBAL_TABLE, false );
|
|
|
|
/**
|
|
* Filters whether a global table should be enabled for multisite installations.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @param bool $enable Default false, unless this is a multisite installation
|
|
* upgraded from version 0.4 or earlier.
|
|
*/
|
|
return \apply_filters( 'avatar_privacy_enable_global_table', $global_table );
|
|
}
|
|
|
|
/**
|
|
* Retrieves the CREATE TABLE definition formatted for use by \db_delta(),
|
|
* without the charset collate clause.
|
|
*
|
|
* Example:
|
|
* `CREATE TABLE some_table (
|
|
* id mediumint(9) NOT NULL AUTO_INCREMENT,
|
|
* some_column varchar(100) NOT NULL,
|
|
* PRIMARY KEY (id)
|
|
* )`
|
|
*
|
|
* @since 2.4.0
|
|
*
|
|
* @param string $table_name The table name including any prefixes.
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function get_table_definition( $table_name ) {
|
|
return "CREATE TABLE {$table_name} (
|
|
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
|
email varchar(100) NOT NULL,
|
|
use_gravatar tinyint(1) DEFAULT NULL,
|
|
last_updated datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
|
|
log_message varchar(255),
|
|
PRIMARY KEY (id),
|
|
UNIQUE KEY email (email)
|
|
)";
|
|
}
|
|
|
|
/**
|
|
* Tries set up the migration queue if the trigger is set.
|
|
*
|
|
* @since 2.4.0 Moved to class Comment_Author_Table.
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function maybe_prepare_migration_queue() {
|
|
$queue = $this->network_options->get( Network_Options::START_GLOBAL_TABLE_MIGRATION );
|
|
|
|
if ( \is_array( $queue ) && $this->network_options->lock( Network_Options::GLOBAL_TABLE_MIGRATION ) ) {
|
|
if ( ! empty( $queue ) ) {
|
|
// Store new queue, overwriting any existing queue (since this per network
|
|
// and we already got all sites currently in the network).
|
|
$this->network_options->set( Network_Options::GLOBAL_TABLE_MIGRATION, $queue );
|
|
} else {
|
|
// The "start queue" is empty, which means we should cease the migration efforts.
|
|
$this->network_options->delete( Network_Options::GLOBAL_TABLE_MIGRATION );
|
|
}
|
|
|
|
// Unlock queue and delete trigger.
|
|
$this->network_options->unlock( Network_Options::GLOBAL_TABLE_MIGRATION );
|
|
$this->network_options->delete( Network_Options::START_GLOBAL_TABLE_MIGRATION );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tries to migrate global table data if the current site is queued.
|
|
*
|
|
* @since 2.4.0 Moved to class Comment_Author_Table.
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function maybe_migrate_from_global_table() {
|
|
if (
|
|
// The plugin is not network-activated (or not on a multisite installation).
|
|
! \is_plugin_active_for_network( \plugin_basename( \AVATAR_PRIVACY_PLUGIN_FILE ) ) ||
|
|
// The queue is empty.
|
|
! $this->network_options->get( Network_Options::GLOBAL_TABLE_MIGRATION ) ||
|
|
// The queue is locked. Try again next time.
|
|
! $this->network_options->lock( Network_Options::GLOBAL_TABLE_MIGRATION )
|
|
) {
|
|
// Nothing to see here.
|
|
return;
|
|
}
|
|
|
|
// Check if we are scheduled to migrate data from the global table.
|
|
$site_id = \get_current_blog_id();
|
|
$queue = $this->network_options->get( Network_Options::GLOBAL_TABLE_MIGRATION, [] );
|
|
|
|
if ( \is_array( $queue ) && ! empty( $queue[ $site_id ] ) ) {
|
|
// Migrate the data.
|
|
$this->migrate_from_global_table( $site_id );
|
|
|
|
// Mark this site as done.
|
|
unset( $queue[ $site_id ] );
|
|
|
|
if ( ! empty( $queue ) ) {
|
|
// Save the new queue.
|
|
$this->network_options->set( Network_Options::GLOBAL_TABLE_MIGRATION, $queue );
|
|
} else {
|
|
// Delete it.
|
|
$this->network_options->delete( Network_Options::GLOBAL_TABLE_MIGRATION );
|
|
}
|
|
}
|
|
|
|
// Unlock the queue again.
|
|
$this->network_options->unlock( Network_Options::GLOBAL_TABLE_MIGRATION );
|
|
}
|
|
|
|
/**
|
|
* Migrates data from the global database to the given site database.
|
|
*
|
|
* @since 2.4.0 Parameter $site_id made mandatory.
|
|
*
|
|
* @global \wpdb $wpdb The WordPress Database Access Abstraction.
|
|
*
|
|
* @param int|null $site_id The site ID. Null means the current $blog_id.
|
|
*
|
|
* @return int|false The number of migrated rows or false on error.
|
|
*/
|
|
public function migrate_from_global_table( $site_id ) {
|
|
global $wpdb;
|
|
|
|
// Get table names.
|
|
$global_table_name = $this->get_table_name( \get_main_site_id() );
|
|
$site_table_name = $this->get_table_name( $site_id );
|
|
|
|
// Either we are on the main site or the "use global table" option is enabled.
|
|
if ( $global_table_name === $site_table_name ) {
|
|
return false;
|
|
}
|
|
|
|
// Select the rows to migrate.
|
|
|
|
/**
|
|
* Rows to delete indexed by the ID column in the global table.
|
|
*
|
|
* @var \stdClass[]
|
|
*/
|
|
$rows_to_delete = [];
|
|
/**
|
|
* Rows to migrate indexed by the ID column in the global table.
|
|
*
|
|
* @var \stdClass[]
|
|
*/
|
|
$rows_to_migrate = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->prepare(
|
|
"SELECT * FROM `{$global_table_name}` WHERE log_message LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
"set with comment % (site: %, blog: {$wpdb->esc_like( $site_id )})"
|
|
),
|
|
\OBJECT_K
|
|
);
|
|
|
|
// Check for existing rows for the same email addresses.
|
|
$emails = \wp_list_pluck( $rows_to_migrate, 'email', 'id' );
|
|
$emails_to_ids = \array_flip( $emails );
|
|
$existing_rows = (array) $wpdb->get_results( $this->prepare_email_query( $emails, $site_table_name ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
|
|
foreach ( $existing_rows as $row ) {
|
|
$global_row_id = $emails_to_ids[ $row->email ];
|
|
|
|
if ( ! empty( $rows_to_migrate[ $global_row_id ] ) ) {
|
|
$global_row = $rows_to_migrate[ $global_row_id ];
|
|
|
|
if ( (int) \strtotime( $row->last_updated ) >= (int) \strtotime( $global_row->last_updated ) ) {
|
|
unset( $rows_to_migrate[ $global_row_id ] );
|
|
|
|
// Just delete this row.
|
|
$rows_to_delete[ $global_row_id ] = $global_row;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Migrated rows need to be deleted, too.
|
|
$rows_to_delete = $rows_to_delete + $rows_to_migrate;
|
|
|
|
// Do INSERTs and UPDATEs in one query.
|
|
$migrated = $this->insert_or_update( [ 'email', 'hash', 'use_gravatar', 'last_updated', 'log_message' ], $rows_to_migrate, $site_id );
|
|
if ( false !== $migrated ) {
|
|
// Do DELETEs in one query.
|
|
$deleted = 0;
|
|
$delete_query = $this->prepare_delete_query( \array_keys( $rows_to_delete ), $global_table_name );
|
|
if ( false !== $delete_query ) {
|
|
$deleted = $wpdb->query( $delete_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
|
|
}
|
|
|
|
if ( false !== $deleted ) {
|
|
// Count the deleted rows if they were not included in the migrated
|
|
// rows because they were too old.
|
|
return \max( $migrated, $deleted );
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Prepares the query for selecting existing rows by email.
|
|
*
|
|
* @global \wpdb $wpdb The WordPress Database Access Abstraction.
|
|
*
|
|
* @param string[] $emails An array of email adresses.
|
|
* @param string $table The table name.
|
|
*
|
|
* @return string|false The prepared query, or false.
|
|
*/
|
|
protected function prepare_email_query( array $emails, $table ) {
|
|
global $wpdb;
|
|
|
|
if ( empty( $emails ) || empty( $table ) ) {
|
|
return false;
|
|
}
|
|
|
|
$placeholders = \join( ',', \array_fill( 0, \count( $emails ), '%s' ) );
|
|
|
|
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
|
return $wpdb->prepare( "SELECT * FROM `{$table}` WHERE email IN ({$placeholders})", $emails );
|
|
}
|
|
|
|
/**
|
|
* Prepares the query for deleting obsolete rows from the database.
|
|
*
|
|
* @global \wpdb $wpdb The WordPress Database Access Abstraction.
|
|
*
|
|
* @param int[] $ids_to_delete The IDs to delete.
|
|
* @param string $table The table name.
|
|
*
|
|
* @return string|false The prepared query, or false.
|
|
*/
|
|
protected function prepare_delete_query( array $ids_to_delete, $table ) {
|
|
global $wpdb;
|
|
|
|
if ( empty( $ids_to_delete ) || empty( $table ) ) {
|
|
return false;
|
|
}
|
|
|
|
$placeholders = \join( ',', \array_fill( 0, \count( $ids_to_delete ), '%d' ) );
|
|
|
|
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
|
return $wpdb->prepare( "DELETE FROM `{$table}` WHERE id IN ({$placeholders})", $ids_to_delete );
|
|
}
|
|
|
|
/**
|
|
* Fixes the table schema when dbDelta cannot cope with the changes.
|
|
*
|
|
* The table itself is already guaranteed to exist.
|
|
*
|
|
* @since 2.4.0
|
|
*
|
|
* @param string $previous_version The previously installed plugin version.
|
|
*
|
|
* @return bool True if the schema was modified, false otherwise.
|
|
*/
|
|
public function maybe_upgrade_schema( $previous_version ) {
|
|
$result = false;
|
|
|
|
if ( \version_compare( $previous_version, '2.4.0', '<' ) ) {
|
|
$result = $this->maybe_drop_hash_column() || $result; // @phpstan-ignore-line -- to make copy & paste less error prone.
|
|
$result = $this->maybe_fix_last_updated_column_default() || $result;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Drops the obsolete 'hash' column from the table (if it exists).
|
|
*
|
|
* @since 2.4.0
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function maybe_drop_hash_column() {
|
|
global $wpdb;
|
|
|
|
$table_name = $this->get_table_name();
|
|
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQLPlaceholders
|
|
if ( 'hash' === $wpdb->get_var( $wpdb->prepare( 'SHOW COLUMNS FROM `%1$s` LIKE \'hash\'', $table_name ) ) ) {
|
|
return (bool) $wpdb->query( $wpdb->prepare( 'ALTER TABLE `%1$s` DROP COLUMN hash', $table_name ) );
|
|
}
|
|
// phpcs:enable WordPress.DB
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Drops the obsolete 'hash' column from the table.
|
|
*
|
|
* @since 2.4.0
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function maybe_fix_last_updated_column_default() {
|
|
global $wpdb;
|
|
|
|
$table_name = $this->get_table_name();
|
|
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQLPlaceholders
|
|
$column_definition = $wpdb->get_row( $wpdb->prepare( 'SHOW COLUMNS FROM `%1$s` LIKE \'last_updated\'', $table_name ), \ARRAY_A );
|
|
if ( 'CURRENT_TIMESTAMP' !== $column_definition['Default'] ) {
|
|
return (bool) $wpdb->query( $wpdb->prepare( 'ALTER TABLE `%1$s` MODIFY COLUMN `last_updated` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL', $table_name ) );
|
|
}
|
|
// phpcs:enable WordPress.DB
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Upgrades the table data if necessary.
|
|
*
|
|
* @since 2.3.0
|
|
* @since 2.4.0 Renamed to maybe_upgrade_data. Parameter $previous_version added.
|
|
*
|
|
* @global \wpdb $wpdb The WordPress Database Access Abstraction.
|
|
*
|
|
* @param string $previous_version The previously installed plugin version.
|
|
*
|
|
* @return int The number of upgraded rows.
|
|
*/
|
|
public function maybe_upgrade_data( $previous_version ) {
|
|
return 0;
|
|
}
|
|
}
|