avatar-privacy/includes/avatar-privacy/components/class-setup.php

486 lines
14 KiB
PHP

<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
* Copyright 2012-2013 Johannes Freudendahl.
*
* 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\Components;
use Avatar_Privacy\Component;
use Avatar_Privacy\Core\Comment_Author_Fields;
use Avatar_Privacy\Core\Settings;
use Avatar_Privacy\Core\User_Fields;
use Avatar_Privacy\Components\Image_Proxy;
use Avatar_Privacy\Data_Storage\Database\Hashes_Table;
use Avatar_Privacy\Data_Storage\Database\Table; // phpcs:ignore ImportDetection.Imports.RequireImports.Import -- needed for type annotations.
use Avatar_Privacy\Data_Storage\Network_Options;
use Avatar_Privacy\Data_Storage\Options;
use Avatar_Privacy\Data_Storage\Site_Transients;
use Avatar_Privacy\Data_Storage\Transients;
use Avatar_Privacy\Tools\Multisite;
/**
* Handles plugin activation and deactivation.
*
* @since 1.0.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @phpstan-import-type SettingsFields from \Avatar_Privacy\Core\Settings
*
* @phpstan-type ObsoleteSettingsFields array<self::OBSOLETE_SETTINGS_FILES, mixed>
*/
class Setup implements Component {
/**
* Obsolete settings keys.
*
* @var string[]
*/
const OBSOLETE_SETTINGS = [
'mode_optin',
'use_gravatar',
'mode_checkforgravatar',
'default_show',
'checkbox_default',
];
/**
* Obsolete avatar defaults and replacement values.
*
* @var string[]
*/
const OBSOLETE_AVATAR_DEFAULTS = [
'comment' => 'bubble',
'im-user-offline' => 'bowling-pin',
'view-media-artist' => 'silhouette',
];
/**
* The options handler.
*
* @var Options
*/
private Options $options;
/**
* The options handler.
*
* @var Network_Options
*/
private Network_Options $network_options;
/**
* The transients handler.
*
* @var Transients
*/
private Transients $transients;
/**
* The site transients handler.
*
* @var Site_Transients
*/
private Site_Transients $site_transients;
/**
* The database table handlers.
*
* @var Table[]
*/
private array $tables;
/**
* The multisite tools.
*
* @var Multisite
*/
private Multisite $multisite;
/**
* The settings API.
*
* @since 2.4.0
*
* @var Settings
*/
private Settings $settings;
/**
* The user fields API.
*
* @since 2.4.0
*
* @var User_Fields
*/
private User_Fields $registered_user;
/**
* The comment author fields API.
*
* @since 2.4.0
*
* @var Comment_Author_Fields
*/
private Comment_Author_Fields $comment_author;
/**
* Creates a new Setup instance.
*
* @since 2.1.0 Parameter $plugin_file removed.
* @since 2.4.0 Parameters $settings, $registered_user, $comment_author, and
* $tables added, parameters $core and $database removed.
*
* @param Settings $settings The settings API.
* @param User_Fields $registered_user The user fields API.
* @param Comment_Author_Fields $comment_author The comment author fields API.
* @param Transients $transients The transients handler.
* @param Site_Transients $site_transients The site transients handler.
* @param Options $options The options handler.
* @param Network_Options $network_options The network options handler.
* @param Table[] $tables The database table handlers, indexed by their base name.
* @param Multisite $multisite The the multisite handler.
*/
public function __construct( Settings $settings, User_Fields $registered_user, Comment_Author_Fields $comment_author, Transients $transients, Site_Transients $site_transients, Options $options, Network_Options $network_options, array $tables, Multisite $multisite ) {
$this->settings = $settings;
$this->registered_user = $registered_user;
$this->comment_author = $comment_author;
$this->transients = $transients;
$this->site_transients = $site_transients;
$this->options = $options;
$this->network_options = $network_options;
$this->tables = $tables;
$this->multisite = $multisite;
}
/**
* Sets up the various hooks for the plugin component.
*
* @return void
*/
public function run() {
// Register deactivation hook. Activation is handled by the update check instead.
\register_deactivation_hook( \AVATAR_PRIVACY_PLUGIN_FILE, [ $this, 'deactivate' ] );
// Update settings and database if necessary.
\add_action( 'plugins_loaded', [ $this, 'update_check' ] );
// Clean up uploaded files after deleting the refering metadata.
\add_action( 'deleted_user_meta', [ $this->registered_user, 'remove_orphaned_local_avatar' ], 10, 4 );
}
/**
* Checks if the default settings or database schema need to be upgraded.
*
* @return void
*/
public function update_check() {
// Force reading the settings from the DB, but do not cache the result.
$current_settings = $this->settings->get_all_settings( true );
// We can ignore errors here, just carry on as if for a new installation.
if ( ! empty( $current_settings[ Options::INSTALLED_VERSION ] ) ) {
$installed_version = $current_settings[ Options::INSTALLED_VERSION ];
} elseif ( ! empty( $current_settings ) && ! isset( $current_settings[ Options::INSTALLED_VERSION ] ) ) {
// Plugin releases before 1.0 did not store the installed version.
$installed_version = '0.4-or-earlier';
} else {
// The plugins was not installed previously.
$installed_version = '';
}
// Check if the plugin data needs to be updated.
$version = $this->settings->get_version();
$update_needed = $version !== $installed_version;
$new_install = empty( $installed_version );
if ( $update_needed ) {
if ( ! $new_install ) {
// Update plugin settings if necessary.
$current_settings = $this->update_settings( $installed_version, $current_settings );
// Preserve previous multisite behavior.
// This needs to happen before the database tables are set up.
if ( \is_multisite() && \version_compare( $installed_version, '0.5', '<' ) ) {
$this->network_options->set( Network_Options::USE_GLOBAL_TABLE, true );
}
}
// Clear transients.
$this->transients->invalidate();
$this->site_transients->invalidate();
// To be safe, let's always flush the rewrite rules if there has been an update.
$this->flush_rewrite_rules_soon();
}
// Check if our database tables need to created or updated.
// This also sets up the `$wpdb->avatar_privacy*` properties, so we have
// to check on every page load.
foreach ( $this->tables as $table ) {
$table->setup( $installed_version );
}
// Run additional upgrade routines now that the tables are set up, but
// only if this plugin has been previously installed.
if ( $update_needed && ! $new_install ) {
$this->update_plugin_data( $installed_version );
}
// Update installed version.
$current_settings[ Options::INSTALLED_VERSION ] = $version;
$this->options->set( Settings::OPTION_NAME, $current_settings );
}
/**
* Updates the plugin settings.
*
* @since 2.1.0 Visibility changed to protected.
* @since 2.4.0 Renamed to update_settings. Parameter $settings passed by value
* and returned as the result of of the function.
*
* @param string $previous_version The version we are upgrading from.
* @param array $settings The settings array.
*
* @return array The updated settings array.
*
* @phpstan-param ObsoleteSettingsFields $settings
* @phpstan-return mixed[]
*/
protected function update_settings( $previous_version, array $settings ) {
// Upgrade from version 0.4 or lower.
if ( \version_compare( $previous_version, '0.5', '<' ) ) {
// Drop old settings.
foreach ( self::OBSOLETE_SETTINGS as $key ) {
unset( $settings[ $key ] );
}
}
return $settings;
}
/**
* Upgrades existing plugin data.
*
* @since 2.4.0
*
* @param string $previous_version The version we are upgrading from.
*
* @return void
*/
protected function update_plugin_data( $previous_version ) {
// Upgrade from version 0.4 or lower.
if ( \version_compare( $previous_version, '0.5', '<' ) ) {
$this->maybe_update_user_hashes();
}
// Upgrade from anything below 1.0-rc.1.
if ( \version_compare( $previous_version, '1.0-rc.1', '<' ) ) {
$this->upgrade_old_avatar_defaults();
}
// Upgrade from anything below 2.1.0-alpha.3.
if ( \version_compare( $previous_version, '2.1.0-alpha.3', '<' ) ) {
$this->prefix_usermeta_keys();
}
// Upgrade from anything below 2.4.0.
if ( \version_compare( $previous_version, '2.4.0', '<' ) ) {
$this->maybe_add_email_hashes();
}
}
/**
* Handles plugin deactivation.
*
* @since 2.1.0 Parameter `$network_wide` added.
*
* @param bool $network_wide A flag indicating if the plugin was network-activated.
*
* @return void
*/
public function deactivate( $network_wide ) {
if ( ! $network_wide ) {
// We've only been activated on this site, all good.
$this->deactivate_plugin();
} elseif ( ! \wp_is_large_network() ) {
// This is a "small" multisite network, so get WordPress to rebuild the rewrite rules.
$this->multisite->do_for_all_sites_in_network( [ $this, 'deactivate_plugin' ] );
} else {
// OK, let's try not to break anything.
$this->multisite->do_for_all_sites_in_network(
// We still need to disable our cron jobs, though.
function() {
\wp_unschedule_hook( Image_Proxy::CRON_JOB_ACTION );
}
);
}
}
/**
* Triggers a rebuild of the rewrite rules on the next page load.
*
* @since 2.1.0
*
* @return void
*/
public function flush_rewrite_rules_soon() {
// Deleting the option forces a rebuild in the proper context on the next load.
$this->options->delete( 'rewrite_rules', true );
}
/**
* Adds a prefix to the GRAVATAR_USE_META_KEY.
*
* This migration method will not work if the standard `wp_usermeta` table is
* replaced with something else, but there does not seem to be a good way to
* use the `get_user_metadata` filter hook and fulfill the goal of not breaking
* future Core use of `use_gravatar` as a meta key.
*
* @since 2.1.0
*
* @global wpdb $wpdb The WordPress Database Access Abstraction.
*
* @return void
*/
public function prefix_usermeta_keys() {
global $wpdb;
// Get all users with the `use_gravatar` meta key.
$affected_users = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s", 'use_gravatar' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
if ( \count( $affected_users ) > 0 ) {
// Update the database table.
$rows = $wpdb->update( $wpdb->usermeta, [ 'meta_key' => User_Fields::GRAVATAR_USE_META_KEY ], [ 'meta_key' => 'use_gravatar' ] ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
// If there were any keys to update, we also have to clear the user_meta cache group.
if ( false !== $rows && $rows > 0 ) {
// Clear user_meta cache for all affected users.
foreach ( $affected_users as $user_id ) {
\wp_cache_delete( $user_id, 'user_meta' );
}
}
}
}
/**
* The deactivation tasks for a single site.
*
* @since 2.1.0
*
* @return void
*/
public function deactivate_plugin() {
// Disable cron jobs.
\wp_unschedule_hook( Image_Proxy::CRON_JOB_ACTION );
// Reset avatar defaults.
$this->options->reset_avatar_default();
// Flush rewrite rules on next page load.
$this->flush_rewrite_rules_soon();
}
/**
* Tries to upgrade the `avatar_defaults` option.
*
* @since 2.1.0 Visibility changed to protected, $options parameter removed.
*
* @return void
*/
protected function upgrade_old_avatar_defaults() {
$obsolete_avatar_defaults = self::OBSOLETE_AVATAR_DEFAULTS;
$old_default = $this->options->get( 'avatar_default', 'mystery', true );
if ( ! empty( $obsolete_avatar_defaults[ $old_default ] ) ) {
$this->options->set( 'avatar_default', $obsolete_avatar_defaults[ $old_default ], true, true );
}
}
/**
* Updates user hashes where they don't exist yet.
*
* @since 2.1.0 Visibility changed to protected.
*
* @return void
*/
protected function maybe_update_user_hashes() {
$args = [
'meta_key' => User_Fields::EMAIL_HASH_META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'meta_compare' => 'NOT EXISTS',
];
foreach ( \get_users( $args ) as $user ) {
// Ensure that there is a user hash - retrieving the hash updates the meta field.
$this->registered_user->get_hash( $user->ID );
}
}
/**
* Adds hashes for stored e-mail addresses if necessary.
*
* @since 2.4.0
*
* @global \wpdb $wpdb The WordPress Database Access Abstraction.
*
* @return int The number of upgraded rows.
*/
protected function maybe_add_email_hashes() {
global $wpdb;
// Add hashes when they are missing.
$emails = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnquotedComplexPlaceholder -- DB and column name.
'SELECT c.email FROM `%1$s` c LEFT OUTER JOIN `%2$s` h ON c.email = h.identifier AND h.type = "comment" AND h.hash IS NULL',
$wpdb->avatar_privacy,
$wpdb->avatar_privacy_hashes
)
);
$email_count = \count( $emails );
if ( $email_count > 0 ) {
// Add hashes for all retrieved rows.
$rows = [];
foreach ( $emails as $email ) {
$rows[] = [
'identifier' => $email,
'hash' => $this->comment_author->get_hash( $email ),
'type' => 'comment',
];
}
return (int) $this->tables[ Hashes_Table::TABLE_BASENAME ]->insert_or_update( [ 'identifier', 'hash', 'type' ], $rows );
}
return 0;
}
}