class-hustle-410-migration.php
5 months ago
class-hustle-430-migration.php
3 years ago
class-hustle-441-migration.php
3 years ago
class-hustle-410-migration.php
465 lines
| 1 | <?php |
| 2 | /** |
| 3 | * File for Hustle_410_Migration class. |
| 4 | * |
| 5 | * @package Hustle |
| 6 | * @since 4.1.0 |
| 7 | */ |
| 8 | |
| 9 | /** |
| 10 | * Class Hustle_410_Migration. |
| 11 | * |
| 12 | * This class handles the migration when going from 4.0.x to 4.1.x |
| 13 | * We adjusted the modules' visibility conditions, and we need the |
| 14 | * conditions from 4.0.x to remain compatible to how we're handling them from 4.1.x on. |
| 15 | * Note this won't make the modules behave as they used to. This just makes the conditions that |
| 16 | * changed, such as Source of Arrival, Posts/Pages/Tags/Categories all/none options, and so on, |
| 17 | * compatible with their expected values in 4.1.x. |
| 18 | * |
| 19 | * @since 4.1.0 |
| 20 | */ |
| 21 | class Hustle_410_Migration { |
| 22 | |
| 23 | /** |
| 24 | * Flag name to mark the migration as "done". |
| 25 | * |
| 26 | * @since 4.1.0 |
| 27 | */ |
| 28 | const MIGRATION_FLAG = 'hustle_40_migrated'; |
| 29 | |
| 30 | /** |
| 31 | * Meta key for the visibility backup. |
| 32 | * |
| 33 | * @since 4.1.0 |
| 34 | */ |
| 35 | const VISIBILITY_BACKUP_META = 'visibility_backup_40x'; |
| 36 | |
| 37 | /** |
| 38 | * Instance of the wpdb class. |
| 39 | * |
| 40 | * @since 4.1.0 |
| 41 | * @var object |
| 42 | */ |
| 43 | private $wpdb; |
| 44 | |
| 45 | /** |
| 46 | * Array of metas to be inserted. |
| 47 | * |
| 48 | * @since 4.1.0 |
| 49 | * @var array |
| 50 | */ |
| 51 | private $backup_metas = array(); |
| 52 | |
| 53 | /** |
| 54 | * Hustle_401_Migration class constructor. |
| 55 | */ |
| 56 | public function __construct() { |
| 57 | |
| 58 | global $wpdb; |
| 59 | $this->wpdb = $wpdb; |
| 60 | |
| 61 | if ( $this->is_migrating() ) { |
| 62 | add_action( 'init', array( $this, 'do_migration' ) ); |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | /** |
| 67 | * Checks whether we should run da migration. |
| 68 | * |
| 69 | * @since 4.1.0 |
| 70 | * |
| 71 | * @return bool |
| 72 | */ |
| 73 | private function is_migrating() { |
| 74 | |
| 75 | // If migration is being forced, do it. |
| 76 | if ( filter_input( INPUT_GET, 'run_41_migration', FILTER_VALIDATE_BOOLEAN ) ) { |
| 77 | return true; |
| 78 | } |
| 79 | |
| 80 | // If migration was already done, skip. |
| 81 | if ( Hustle_Migration::is_migrated( self::MIGRATION_FLAG ) ) { |
| 82 | $prev_version = Hustle_Migration::get_previous_installed_version(); |
| 83 | if ( $prev_version && version_compare( $prev_version, '4.1', '>=' ) ) { |
| 84 | Hustle_Notifications::add_dismissed_notification( '41_visibility_behavior_update' ); |
| 85 | } |
| 86 | return false; |
| 87 | } |
| 88 | |
| 89 | return ! self::is_fresh(); |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * Checks if it's a fresh 4.1 installation or not |
| 94 | * |
| 95 | * @since 4.1.0 |
| 96 | * |
| 97 | * @return bool |
| 98 | */ |
| 99 | private static function is_fresh() { |
| 100 | $is_fresh = Hustle_Db::$is_fresh_install; |
| 101 | |
| 102 | if ( $is_fresh ) { |
| 103 | Hustle_Migration::migration_passed( self::MIGRATION_FLAG ); |
| 104 | } |
| 105 | |
| 106 | return $is_fresh; |
| 107 | } |
| 108 | |
| 109 | /** |
| 110 | * Does the migration from 4.0.x to 4.1.x. |
| 111 | * |
| 112 | * @since 4.1.0 |
| 113 | */ |
| 114 | public function do_migration() { |
| 115 | |
| 116 | $limit = apply_filters( 'hustle_40_migration_limit', 100 ); |
| 117 | |
| 118 | // Restore the 4.0.x metas if they exist. |
| 119 | // Avoid duplicated backup metas and running migration on already migrated settings. |
| 120 | if ( $this->is_backup_created() ) { |
| 121 | $this->restore( false ); |
| 122 | } |
| 123 | |
| 124 | do { |
| 125 | $offset = get_option( 'hustle_40_migration_offset', 0 ); |
| 126 | $m2_modules = get_option( 'hustle_notice_stop_support_m2', array() ); |
| 127 | $conditions = $this->get_all_hustle_module_conditions( $limit, $offset ); |
| 128 | |
| 129 | foreach ( $conditions as $meta ) { |
| 130 | |
| 131 | // Let's keep aside this meta to save them all together at the end. |
| 132 | $this->queue_meta_to_backup( $meta ); |
| 133 | |
| 134 | $meta_id = $meta->meta_id; |
| 135 | $value = json_decode( $meta->meta_value, true ); |
| 136 | |
| 137 | if ( empty( $value['conditions'] ) || ! is_array( $value['conditions'] ) || 1 > count( $value['conditions'] ) ) { |
| 138 | |
| 139 | $group_id = substr( md5( wp_rand() ), 0, 10 ); |
| 140 | |
| 141 | $value['conditions'] = array( |
| 142 | $group_id => array( |
| 143 | 'filter_type' => 'all', |
| 144 | 'group_id' => $group_id, |
| 145 | ), |
| 146 | ); |
| 147 | } |
| 148 | |
| 149 | foreach ( $value['conditions'] as $group_id => $conds ) { |
| 150 | |
| 151 | if ( ! empty( $conds['ms_membership'] ) || ! empty( $conds['ms_membership-n'] ) ) { |
| 152 | $m2_modules[] = $meta_id; |
| 153 | } |
| 154 | |
| 155 | unset( $conds['group_id'], $conds['filter_type'] ); |
| 156 | $count_conds = count( $conds ); |
| 157 | |
| 158 | if ( ! $count_conds || ! empty( $conds['page_404'] ) ) { |
| 159 | |
| 160 | // Hide on 404 page according old behavior. |
| 161 | $filter_type = ! $count_conds ? 'except' : 'only'; |
| 162 | $conds['wp_conditions'] = array( |
| 163 | 'wp_conditions' => array( 'is_404' ), |
| 164 | 'filter_type' => $filter_type, |
| 165 | ); |
| 166 | |
| 167 | // By default, we start showing modules on 404 page. |
| 168 | unset( $conds['page_404'] ); |
| 169 | } |
| 170 | |
| 171 | if ( ! empty( $conds['source_of_arrival']['source_external'] ) |
| 172 | && 'true' === $conds['source_of_arrival']['source_external'] ) { |
| 173 | $conds['source_of_arrival']['source_direct'] = 'true'; |
| 174 | } |
| 175 | |
| 176 | // Remove 'all' values. |
| 177 | $post_types = Opt_In_Utils::get_post_types(); |
| 178 | $cpts = array_keys( $post_types ); |
| 179 | $types = array_merge( array( 'posts', 'pages', 'tags', 'categories' ), $cpts ); |
| 180 | |
| 181 | foreach ( $types as $type ) { |
| 182 | if ( |
| 183 | ( |
| 184 | ! empty( $conds[ $type ][ $type ] ) && |
| 185 | in_array( 'all', $conds[ $type ][ $type ], true ) |
| 186 | ) || |
| 187 | ( |
| 188 | ! empty( $conds[ $type ]['selected_cpts'] ) && |
| 189 | in_array( 'all', $conds[ $type ]['selected_cpts'], true ) |
| 190 | ) |
| 191 | ) { |
| 192 | unset( $conds[ $type ][ $type ], $conds[ $type ]['selected_cpts'] ); |
| 193 | $conds[ $type ]['filter_type'] = empty( $conds[ $type ]['filter_type'] ) || 'only' !== $conds[ $type ]['filter_type'] ? 'only' : 'except'; |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | // Transform condition rules according new logic. |
| 198 | $and_rules = array( |
| 199 | 'visitor_logged_in_status', |
| 200 | 'visitor_device', |
| 201 | 'from_referrer', |
| 202 | 'source_of_arrival', |
| 203 | 'on_url', |
| 204 | 'visitor_commented', |
| 205 | 'visitor_country', |
| 206 | 'shown_less_than', |
| 207 | ); |
| 208 | |
| 209 | $or_rules = array_diff_key( $conds, array_flip( $and_rules ) ); |
| 210 | |
| 211 | // Get "AND" conditions. |
| 212 | $and_conds = array_intersect_key( $conds, array_flip( $and_rules ) ); |
| 213 | |
| 214 | if ( isset( $or_rules['pages'] ) && 1 < count( $or_rules ) ) { |
| 215 | $this->add_new_group( |
| 216 | $value, |
| 217 | array_merge( $and_conds, array( 'pages' => $conds['pages'] ) ) |
| 218 | ); |
| 219 | unset( $conds['pages'] ); |
| 220 | unset( $or_rules['pages'] ); |
| 221 | } |
| 222 | |
| 223 | $post_group = array_intersect_key( $or_rules, array_flip( array( 'posts', 'tags', 'categories' ) ) ); |
| 224 | if ( ! empty( $post_group ) && count( $post_group ) < count( $or_rules ) ) { |
| 225 | $this->add_new_group( $value, array_merge( $and_conds, $post_group ) ); |
| 226 | |
| 227 | unset( $conds['posts'], $conds['tags'], $conds['categories'] ); |
| 228 | unset( $or_rules['posts'], $or_rules['tags'], $or_rules['categories'] ); |
| 229 | } |
| 230 | |
| 231 | foreach ( $or_rules as $name => $args ) { |
| 232 | if ( 2 > count( $or_rules ) ) { |
| 233 | break; |
| 234 | } |
| 235 | $this->add_new_group( |
| 236 | $value, |
| 237 | array_merge( $and_conds, array( $name => $args ) ) |
| 238 | ); |
| 239 | unset( $conds[ $name ] ); |
| 240 | unset( $or_rules[ $name ] ); |
| 241 | } |
| 242 | |
| 243 | // Add new show/hide option. |
| 244 | $conds['show_or_hide_conditions'] = 'show'; |
| 245 | $conds['filter_type'] = 'all'; |
| 246 | $conds['group_id'] = $group_id; |
| 247 | $value['conditions'][ $group_id ] = $conds; |
| 248 | } |
| 249 | |
| 250 | // Save transformed conditions. |
| 251 | $this->wpdb->update( |
| 252 | Hustle_Db::modules_meta_table(), |
| 253 | array( 'meta_value' => wp_json_encode( $value ) ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value |
| 254 | array( 'meta_id' => $meta_id ) |
| 255 | ); |
| 256 | |
| 257 | wp_cache_delete( $meta->module_id, 'hustle_module_meta' ); |
| 258 | } |
| 259 | |
| 260 | $count_conditions = count( $conditions ); |
| 261 | $offset += $limit; |
| 262 | |
| 263 | update_option( 'hustle_40_migration_offset', $offset ); |
| 264 | update_option( 'hustle_notice_stop_support_m2', $m2_modules ); |
| 265 | |
| 266 | // Store the backup metas. |
| 267 | $this->insert_backup_metas(); |
| 268 | |
| 269 | } while ( $count_conditions === $limit ); |
| 270 | |
| 271 | Hustle_Migration::migration_passed( self::MIGRATION_FLAG ); |
| 272 | delete_option( 'hustle_40_migration_offset' ); |
| 273 | } |
| 274 | |
| 275 | /** |
| 276 | * Gets all the stored visibility metas. |
| 277 | * |
| 278 | * @since 4.1.0 |
| 279 | * |
| 280 | * @param int $limit Query limit. |
| 281 | * @param int $offset Query offset. |
| 282 | * @return array |
| 283 | */ |
| 284 | private function get_all_hustle_module_conditions( $limit, $offset ) { |
| 285 | |
| 286 | $modules_table = Hustle_Db::modules_meta_table(); |
| 287 | |
| 288 | // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 289 | $query = $this->wpdb->prepare( "SELECT meta_id, module_id, meta_value FROM {$modules_table} WHERE meta_key = 'visibility' LIMIT %d OFFSET %d", intval( $limit ), intval( $offset ) ); |
| 290 | $results = $this->wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
| 291 | |
| 292 | return $results; |
| 293 | } |
| 294 | |
| 295 | /** |
| 296 | * Adds a new group within the passed visibility settings. |
| 297 | * |
| 298 | * @since 4.1.0 |
| 299 | * |
| 300 | * @param array $conditions Reference to the settings to which this group will be added. |
| 301 | * @param array $args Group's properties and conditions. |
| 302 | */ |
| 303 | private function add_new_group( &$conditions, $args ) { |
| 304 | |
| 305 | $new_group_id = substr( md5( wp_rand() ), 0, 10 ); |
| 306 | |
| 307 | if ( ! isset( $args['filter_type'] ) ) { |
| 308 | $args['filter_type'] = 'all'; |
| 309 | $args['group_id'] = $new_group_id; |
| 310 | } |
| 311 | |
| 312 | // Add new show/hide option. |
| 313 | $args['show_or_hide_conditions'] = 'show'; |
| 314 | |
| 315 | $conditions['conditions'][ $new_group_id ] = $args; |
| 316 | } |
| 317 | |
| 318 | /** |
| 319 | * Checks if there's any backup meta already created. |
| 320 | * |
| 321 | * These should be deleted when rolling back, so if a single one exists, |
| 322 | * it's likely that we have them all and creating new ones isn't needed. |
| 323 | * |
| 324 | * @since 4.1.0 |
| 325 | * |
| 326 | * @return bool |
| 327 | */ |
| 328 | public function is_backup_created() { |
| 329 | |
| 330 | $modules_table = Hustle_Db::modules_meta_table(); |
| 331 | |
| 332 | $query = $this->wpdb->prepare( "SELECT meta_id FROM {$modules_table} WHERE meta_key = %s LIMIT 1", self::VISIBILITY_BACKUP_META ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 333 | |
| 334 | $results = $this->wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
| 335 | |
| 336 | $is_backup_created = 0 < count( $results ); |
| 337 | |
| 338 | return $is_backup_created; |
| 339 | } |
| 340 | |
| 341 | /** |
| 342 | * Adds the old meta to be inserted later on. |
| 343 | * |
| 344 | * Store the old 4.0.x meta formatted for INSERT in the class property, |
| 345 | * to be saved in the db later on. Used when looping through the metas. |
| 346 | * |
| 347 | * @since 4.1.0 |
| 348 | * |
| 349 | * @param object $meta The original 4.0.x visibility meta retrieved from the db. |
| 350 | */ |
| 351 | private function queue_meta_to_backup( $meta ) { |
| 352 | |
| 353 | // Format this ready for IMPORT. |
| 354 | $row = $this->wpdb->prepare( '(%d, %s, %s)', $meta->module_id, self::VISIBILITY_BACKUP_META, $meta->meta_value ); |
| 355 | |
| 356 | $this->backup_metas[] = $row; |
| 357 | } |
| 358 | |
| 359 | /** |
| 360 | * Inserts the backup visibility metas into the db. |
| 361 | * |
| 362 | * @since 4.1.0 |
| 363 | */ |
| 364 | private function insert_backup_metas() { |
| 365 | |
| 366 | // Skip if there isn't any meta queued. |
| 367 | if ( empty( $this->backup_metas ) ) { |
| 368 | return; |
| 369 | } |
| 370 | |
| 371 | $modules_meta_table = Hustle_Db::modules_meta_table(); |
| 372 | $backup_values = implode( ', ', $this->backup_metas ); |
| 373 | |
| 374 | // Build the query with the already queued metas. |
| 375 | $sql = "INSERT INTO {$modules_meta_table} (module_id, meta_key, meta_value) VALUES {$backup_values}"; |
| 376 | |
| 377 | // Do the insert. |
| 378 | $this->wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
| 379 | |
| 380 | // Empty the property. |
| 381 | $this->backup_metas = array(); |
| 382 | } |
| 383 | |
| 384 | /* |
| 385 | ---------- Deactivation stuff ---------- |
| 386 | */ |
| 387 | |
| 388 | /** |
| 389 | * Deletes the 4.1.x visibility metas and restore the ones for 4.0.x. |
| 390 | * |
| 391 | * This will allow admins to find their old visibility settings |
| 392 | * when they activate 4.0.x again. |
| 393 | * |
| 394 | * @since 4.1.0 |
| 395 | * |
| 396 | * @param bool $check_if_exists Skip check for existing backup. False only if already checked. |
| 397 | * |
| 398 | * @throws Exception When there's no data to restore or the restore failed. |
| 399 | * @return bool |
| 400 | */ |
| 401 | private function restore( $check_if_exists = true ) { |
| 402 | |
| 403 | try { |
| 404 | |
| 405 | // If there's nothing to restore, abort. |
| 406 | if ( $check_if_exists && ! $this->is_backup_created() ) { |
| 407 | throw new Exception( __( "There's no backup to restore.", 'hustle' ) ); |
| 408 | } |
| 409 | |
| 410 | $modules_meta_table = Hustle_Db::modules_meta_table(); |
| 411 | |
| 412 | // Get the meta id and module id of the modules with backups. |
| 413 | // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 414 | $backup_sql = $this->wpdb->prepare( "SELECT meta_id, module_id FROM {$modules_meta_table} WHERE meta_key = %s", self::VISIBILITY_BACKUP_META ); |
| 415 | $backup_result = $this->wpdb->get_results( $backup_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
| 416 | |
| 417 | $backup_modules_id = array_column( $backup_result, 'module_id' ); |
| 418 | |
| 419 | if ( empty( $backup_modules_id ) ) { |
| 420 | throw new Exception( __( "There's no backup to restore.", 'hustle' ) ); |
| 421 | } |
| 422 | |
| 423 | // Delete the visibility conditions created for 4.1.x migration. |
| 424 | $modules_id_holder = implode( ', ', array_fill( 0, count( $backup_modules_id ), '%s' ) ); |
| 425 | |
| 426 | // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 427 | $delete_sql = $this->wpdb->prepare( "DELETE FROM {$modules_meta_table} WHERE module_id IN ($modules_id_holder) AND meta_key = 'visibility'", $backup_modules_id ); |
| 428 | $deleted = $this->wpdb->query( $delete_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
| 429 | |
| 430 | // Check if the metas were successfully deleted. |
| 431 | if ( false === $deleted ) { |
| 432 | throw new Exception( __( "The 4.1.x visibility metas couldn't be deleted.", 'hustle' ) ); |
| 433 | } |
| 434 | |
| 435 | // Restore the visibility conditions created in 4.0.x. |
| 436 | $backup_metas_id = array_column( $backup_result, 'meta_id' ); |
| 437 | |
| 438 | $metas_id_holder = implode( ', ', array_fill( 0, count( $backup_metas_id ), '%s' ) ); |
| 439 | |
| 440 | // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 441 | $rename_sql = $this->wpdb->prepare( "UPDATE {$modules_meta_table} SET meta_key = 'visibility' WHERE meta_id IN ($metas_id_holder)", $backup_metas_id ); |
| 442 | $renamed = $this->wpdb->query( $rename_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
| 443 | |
| 444 | // Check if the old metas were successfully restored. |
| 445 | if ( false === $renamed ) { |
| 446 | throw new Exception( __( "The 4.0.x visibility metas couldn't be restored.", 'hustle' ) ); |
| 447 | } |
| 448 | } catch ( Exception $e ) { |
| 449 | |
| 450 | $message = Opt_In_Utils::maybe_log( __METHOD__, $e->getMessage() ); |
| 451 | return false; |
| 452 | } |
| 453 | |
| 454 | // Clear caches. |
| 455 | foreach ( $backup_modules_id as $id ) { |
| 456 | wp_cache_delete( $id, 'hustle_module_meta' ); |
| 457 | } |
| 458 | |
| 459 | // Reset the migration flag so migration runs next time 4.1.x is enabled. |
| 460 | Hustle_Migration::remove_migration_passed_flag( self::MIGRATION_FLAG ); |
| 461 | |
| 462 | return true; |
| 463 | } |
| 464 | } |
| 465 |