PluginProbe
Custom 404 Pro / trunk
Custom 404 Pro vtrunk
3.15.2 3.15.4 3.15.5 3.15.6 3.16.0 3.15.1 3.15.0 3.14.1 3.14.0 3.13.0 trunk 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.1.5 1.1.6 1.2.0 1.3.10 1.3.12 1.3.5 All 100 releases
custom-404-pro / includes / class-activateclass.php

class-activateclass.php in Custom 404 Pro trunk, at includes/class-activateclass.php

172 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles plugin activation.
4 *
5 * @package Custom_404_Pro
6 */
7
8 /**
9 * Activation class.
10 */
11 class ActivateClass {
12
13 /**
14 * Creates the logs table and seeds default settings for a single site.
15 */
16 private static function run_activation() {
17 self::create_tables();
18 self::maybe_migrate_legacy_options();
19 self::initialize_options();
20
21 // Record the schema version so the plugins_loaded migration check can
22 // skip the SHOW TABLES query on every subsequent page load.
23 if ( defined( 'CUSTOM_404_PRO_VERSION' ) ) {
24 update_option( 'custom_404_pro_db_version', CUSTOM_404_PRO_VERSION );
25 }
26
27 // Schedule the daily log-pruning cron event if it is not already registered.
28 if ( ! wp_next_scheduled( 'custom_404_pro_prune_logs' ) ) {
29 wp_schedule_event( time(), 'daily', 'custom_404_pro_prune_logs' );
30 }
31 }
32
33 /**
34 * Runs on plugin activation, handling multisite if needed.
35 *
36 * No capability check here — WordPress core already enforces that only users
37 * with activate_plugins can trigger this hook, and the check would silently
38 * break activation via WP-CLI or automated deployment pipelines.
39 *
40 * @since 3.12.9
41 */
42 public static function activate() {
43 if ( is_multisite() ) {
44 $sites = get_sites( array( 'fields' => 'ids' ) );
45 foreach ( $sites as $blog_id ) {
46 switch_to_blog( $blog_id );
47 self::run_activation();
48 restore_current_blog();
49 }
50 } else {
51 self::run_activation();
52 }
53 }
54
55 /**
56 * Creates or upgrades the plugin logs table.
57 *
58 * The legacy options table is no longer created here — settings are stored
59 * in wp_options under the Helpers::OPTION_KEY key.
60 *
61 * Schema notes:
62 * - `id` is bigint because this table records one row per 404 hit. The
63 * previous mediumint(9) ran out of AUTO_INCREMENT values at 8,388,607
64 * rows, after which every insert failed silently on a busy site.
65 * - `created` is indexed because the retention policy added in 3.14.0
66 * both sorts and filters on it. Without the index each daily prune ran
67 * a full table scan.
68 * - dbDelta parses this string with a strict format: two spaces after
69 * PRIMARY KEY, and one field or key per line. Reformatting it will make
70 * dbDelta reissue ALTER statements on every run.
71 * - Column types are lowercase because WordPress 6.4 and earlier compare
72 * the declared type against MySQL's reported type case-sensitively.
73 * Declaring `TIMESTAMP` there made dbDelta report "changed type from
74 * timestamp to TIMESTAMP" forever and reissue the ALTER on every call.
75 * Newer core normalises the case; the older behaviour is still within
76 * our supported range, so match MySQL and stay lowercase.
77 *
78 * @since 3.12.9
79 * @since 3.16.0 Widened `id` to bigint and added an index on `created`.
80 * @return array Map of changes dbDelta applied. Empty when the schema was
81 * already up to date, which is what the idempotency test
82 * asserts — a non-empty result on a second call would mean
83 * the plugin reissues ALTER TABLE on every upgrade check.
84 */
85 public static function create_tables(): array {
86 global $wpdb;
87 $helpers = Helpers::singleton();
88 $table_logs = $wpdb->prefix . $helpers->table_logs;
89 $charset_collate = $wpdb->get_charset_collate();
90 $sql_logs = "CREATE TABLE $table_logs (
91 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
92 ip text,
93 path text,
94 referer text,
95 user_agent text,
96 created timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
97 updated timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
98 PRIMARY KEY (id),
99 KEY created (created)
100 ) $charset_collate;";
101 include_once ABSPATH . 'wp-admin/includes/upgrade.php';
102 return (array) dbDelta( $sql_logs );
103 }
104
105 /**
106 * Seeds default settings into wp_options if they have not been set yet.
107 *
108 * Uses add_option() which is a no-op when the key already exists, making
109 * this safe to call on every activation without overwriting saved settings.
110 *
111 * @since 3.12.9
112 */
113 public static function initialize_options() {
114 $helpers = Helpers::singleton();
115 add_option( Helpers::OPTION_KEY, $helpers->defaults() );
116 }
117
118 /**
119 * Migrates settings from the legacy custom_404_pro_options table to wp_options.
120 *
121 * If the legacy table does not exist this method returns immediately. After a
122 * successful migration the legacy table is dropped. Safe to call multiple
123 * times — subsequent calls are a no-op once the table is gone.
124 *
125 * Values are cast to their correct types (bool, int) using the defaults map
126 * so that string values from the old text columns are not carried forward.
127 *
128 * @since 3.12.9
129 */
130 public static function maybe_migrate_legacy_options() {
131 global $wpdb;
132 $legacy_table = $wpdb->prefix . 'custom_404_pro_options';
133
134 // Nothing to migrate if the legacy table does not exist.
135 $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $legacy_table ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
136 if ( ! $table_exists ) {
137 return;
138 }
139
140 // Already migrated — just clean up the legacy table.
141 if ( get_option( Helpers::OPTION_KEY ) ) {
142 $wpdb->query( 'DROP TABLE IF EXISTS ' . $legacy_table ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
143 return;
144 }
145
146 // Read legacy rows and build a settings array, casting each value to the
147 // correct type so that legacy text-column strings do not persist as strings
148 // where booleans or integers are expected.
149 $rows = $wpdb->get_results( 'SELECT name, value FROM ' . $legacy_table, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
150 if ( ! empty( $rows ) ) {
151 $defaults = ( new Helpers() )->defaults();
152 $settings = array();
153 foreach ( $rows as $row ) {
154 $key = $row['name'];
155 $value = $row['value'];
156 if ( array_key_exists( $key, $defaults ) ) {
157 if ( is_bool( $defaults[ $key ] ) ) {
158 $value = ! empty( $value ) && '0' !== $value;
159 } elseif ( is_int( $defaults[ $key ] ) ) {
160 $value = (int) $value;
161 }
162 }
163 $settings[ $key ] = $value;
164 }
165 update_option( Helpers::OPTION_KEY, $settings );
166 }
167
168 // Drop the legacy table.
169 $wpdb->query( 'DROP TABLE IF EXISTS ' . $legacy_table ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
170 }
171 }
172