| @@ -39,15 +39,18 @@ | ||
| 39 | 39 | /** |
| 40 | 40 | * Initialize the settings page. |
| 41 | 41 | */ |
| 42 | 42 | public function init() { |
| 43 | - add_action( 'init', [ $this, 'hooks' ] ); | |
| 43 | + add_action( 'admin_init', [ $this, 'hooks' ] ); | |
| 44 | 44 | add_action( 'wp_ajax_content_control_upgrades', [ $this, 'ajax_handler' ] ); |
| 45 | 45 | add_filter( 'content_control/settings-page_localized_vars', [ $this, 'localize_vars' ] ); |
| 46 | + add_action( 'content_control/update_version', '\\ContentControl\\maybe_force_v2_migrations' ); | |
| 46 | 47 | } |
| 47 | 48 | |
| 48 | 49 | /** |
| 49 | 50 | * Hook into relevant WP actions. |
| 51 | + * | |
| 52 | + * @return void | |
| 50 | 53 | */ |
| 51 | 54 | public function hooks() { |
| 52 | 55 | if ( is_admin() && current_user_can( 'manage_options' ) ) { |
| 53 | 56 | add_action( 'admin_notices', [ $this, 'admin_notices' ] ); |
| @@ -63,8 +66,9 @@ | ||
| 63 | 66 | */ |
| 64 | 67 | public function all_upgrades() { |
| 65 | 68 | return [ |
| 66 | 69 | // Version 2 upgrades. |
| 70 | + '\ContentControl\Upgrades\Backup_2', | |
| 67 | 71 | '\ContentControl\Upgrades\PluginMeta_2', |
| 68 | 72 | '\ContentControl\Upgrades\Settings_2', |
| 69 | 73 | '\ContentControl\Upgrades\UserMeta_2', |
| 70 | 74 | '\ContentControl\Upgrades\Restrictions_2', |
| @@ -76,9 +80,9 @@ | ||
| 76 | 80 | * |
| 77 | 81 | * @return boolean |
| 78 | 82 | */ |
| 79 | 83 | public function has_upgrades() { |
| 80 | - return count( $this->get_required_upgrades() ); | |
| 84 | + return (bool) count( $this->get_required_upgrades() ); | |
| 81 | 85 | } |
| 82 | 86 | |
| 83 | 87 | /** |
| 84 | 88 | * Get a list of required upgrades. |
| @@ -87,42 +91,40 @@ | ||
| 87 | 91 | * |
| 88 | 92 | * @return \ContentControl\Base\Upgrade[] |
| 89 | 93 | */ |
| 90 | 94 | public function get_required_upgrades() { |
| 91 | - static $required_upgrades = null; | |
| 95 | + $required_upgrades = []; | |
| 92 | 96 | |
| 93 | - if ( null === $required_upgrades ) { | |
| 94 | - $all_upgrades = $this->all_upgrades(); | |
| 95 | - $required_upgrades = []; | |
| 97 | + $all_upgrades = $this->all_upgrades(); | |
| 98 | + $required_upgrades = []; | |
| 96 | 99 | |
| 97 | - foreach ( $all_upgrades as $upgrade_class ) { | |
| 98 | - if ( ! class_exists( $upgrade_class ) ) { | |
| 99 | - continue; | |
| 100 | - } | |
| 100 | + foreach ( $all_upgrades as $upgrade_class ) { | |
| 101 | + if ( ! class_exists( $upgrade_class ) ) { | |
| 102 | + continue; | |
| 103 | + } | |
| 101 | 104 | |
| 102 | - /** | |
| 103 | - * Upgrade class instance. | |
| 104 | - * | |
| 105 | - * @var \ContentControl\Base\Upgrade $upgrade | |
| 106 | - */ | |
| 107 | - $upgrade = new $upgrade_class(); | |
| 105 | + /** | |
| 106 | + * Upgrade class instance. | |
| 107 | + * | |
| 108 | + * @var \ContentControl\Base\Upgrade $upgrade | |
| 109 | + */ | |
| 110 | + $upgrade = new $upgrade_class(); | |
| 108 | 111 | |
| 109 | - // Check if required, and if so, add it to the list. | |
| 110 | - if ( is_upgrade_complete( $upgrade ) ) { | |
| 111 | - continue; | |
| 112 | - } elseif ( ! $upgrade->is_required() ) { | |
| 113 | - // If its not required, mark it as done. | |
| 114 | - mark_upgrade_complete( $upgrade ); | |
| 115 | - continue; | |
| 116 | - } | |
| 117 | - | |
| 118 | - $required_upgrades[] = $upgrade; | |
| 112 | + // Check if required, and if so, add it to the list. | |
| 113 | + if ( is_upgrade_complete( $upgrade ) ) { | |
| 114 | + continue; | |
| 115 | + } elseif ( ! $upgrade->is_required() ) { | |
| 116 | + // If its not required, mark it as done. | |
| 117 | + mark_upgrade_complete( $upgrade ); | |
| 118 | + continue; | |
| 119 | 119 | } |
| 120 | 120 | |
| 121 | - // Sort the required upgrades based on prerequisites. | |
| 122 | - $required_upgrades = $this->sort_upgrades_by_prerequisites( $required_upgrades ); | |
| 121 | + $required_upgrades[] = $upgrade; | |
| 123 | 122 | } |
| 124 | 123 | |
| 124 | + // Sort the required upgrades based on prerequisites. | |
| 125 | + $required_upgrades = $this->sort_upgrades_by_prerequisites( $required_upgrades ); | |
| 126 | + | |
| 125 | 127 | return $required_upgrades; |
| 126 | 128 | } |
| 127 | 129 | |
| 128 | 130 | /** |
| @@ -133,15 +135,20 @@ | ||
| 133 | 135 | * @return \ContentControl\Base\Upgrade[] |
| 134 | 136 | */ |
| 135 | 137 | private function sort_upgrades_by_prerequisites( $upgrades ) { |
| 136 | 138 | // Build the graph of upgrades and their dependencies. |
| 137 | - $graph = []; | |
| 139 | + $graph = []; | |
| 140 | + // Build a lookup table of upgrades by name. | |
| 138 | 141 | $upgrade_by_name = []; |
| 142 | + | |
| 143 | + // Build the graph & lookup tables. | |
| 139 | 144 | foreach ( $upgrades as $upgrade ) { |
| 140 | 145 | $updgrade_name = get_upgrade_name( $upgrade ); |
| 141 | 146 | |
| 147 | + // Add the upgrade to the graph with its dependencies. | |
| 142 | 148 | $graph[ $updgrade_name ] = $upgrade->get_dependencies(); |
| 143 | 149 | |
| 150 | + // Add the upgrade to the lookup table. | |
| 144 | 151 | $upgrade_by_name[ $updgrade_name ] = $upgrade; |
| 145 | 152 | } |
| 146 | 153 | |
| 147 | 154 | // Perform a topological sort on the graph. |
| @@ -146,26 +153,29 @@ | ||
| 146 | 153 | |
| 147 | 154 | // Perform a topological sort on the graph. |
| 148 | 155 | $sorted = $this->topological_sort( $graph ); |
| 149 | 156 | |
| 150 | - // Rebuild the list of upgrades in the sorted order. | |
| 151 | - foreach ( $sorted as $key => $value ) { | |
| 152 | - $sorted[ $key ] = $upgrade_by_name[ $value ]; | |
| 157 | + $sorted_upgrades = []; | |
| 158 | + | |
| 159 | + // Map the sorted ugprade list with the upgrade from the lookup table. | |
| 160 | + foreach ( $sorted as $upgrade_name ) { | |
| 161 | + $upgrade = isset( $upgrade_by_name[ $upgrade_name ] ) ? $upgrade_by_name[ $upgrade_name ] : null; | |
| 162 | + $sorted_upgrades[] = $upgrade; | |
| 153 | 163 | } |
| 154 | 164 | |
| 155 | 165 | // Remove null values, these are upgrades that have been marked as done. |
| 156 | - $sorted = array_filter( $sorted ); | |
| 166 | + $sorted_upgrades = array_filter( $sorted_upgrades ); | |
| 157 | 167 | |
| 158 | 168 | // Return the sorted upgrades. |
| 159 | - return $sorted; | |
| 169 | + return $sorted_upgrades; | |
| 160 | 170 | } |
| 161 | 171 | |
| 162 | 172 | /** |
| 163 | 173 | * Perform a topological sort on a graph. |
| 164 | 174 | * |
| 165 | - * @param array $graph Graph to sort. | |
| 175 | + * @param array<string,array<string>> $graph Graph to sort. | |
| 166 | 176 | * |
| 167 | - * @return array | |
| 177 | + * @return array<string> | |
| 168 | 178 | */ |
| 169 | 179 | private function topological_sort( $graph ) { |
| 170 | 180 | $visited = []; |
| 171 | 181 | $sorted = []; |
| @@ -179,12 +189,14 @@ | ||
| 179 | 189 | |
| 180 | 190 | /** |
| 181 | 191 | * Visit a node in the graph for topological sort. |
| 182 | 192 | * |
| 183 | - * @param mixed $node Node to visit. | |
| 184 | - * @param array $graph Graph to sort. | |
| 185 | - * @param array $visited List of visited nodes. | |
| 186 | - * @param array $sorted List of sorted nodes. | |
| 193 | + * @param string $node Node to visit. | |
| 194 | + * @param array<string,array<string>> $graph Graph to sort. | |
| 195 | + * @param array<string,bool> $visited List of visited nodes. | |
| 196 | + * @param array<string> $sorted List of sorted nodes. | |
| 197 | + * | |
| 198 | + * @return void | |
| 187 | 199 | */ |
| 188 | 200 | private function visit_node( $node, $graph, &$visited, &$sorted ) { |
| 189 | 201 | if ( isset( $visited[ $node ] ) ) { |
| 190 | 202 | // Node already visited, skip. |
| @@ -192,10 +204,12 @@ | ||
| 192 | 204 | } |
| 193 | 205 | |
| 194 | 206 | $visited[ $node ] = true; |
| 195 | 207 | |
| 196 | - foreach ( $graph[ $node ] as $dependency ) { | |
| 197 | - $this->visit_node( $dependency, $graph, $visited, $sorted ); | |
| 208 | + if ( isset( $graph[ $node ] ) && ! empty( $graph[ $node ] ) ) { | |
| 209 | + foreach ( $graph[ $node ] as $dependency ) { | |
| 210 | + $this->visit_node( $dependency, $graph, $visited, $sorted ); | |
| 211 | + } | |
| 198 | 212 | } |
| 199 | 213 | |
| 200 | 214 | $sorted[] = $node; |
| 201 | 215 | } |
| @@ -200,9 +214,11 @@ | ||
| 200 | 214 | $sorted[] = $node; |
| 201 | 215 | } |
| 202 | 216 | |
| 203 | 217 | /** |
| 204 | - * AJAX Handler | |
| 218 | + * AJAX Handler. | |
| 219 | + * | |
| 220 | + * @return void | |
| 205 | 221 | */ |
| 206 | 222 | public function ajax_handler() { |
| 207 | 223 | // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 208 | 224 | if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_REQUEST['nonce'] ), 'content_control_upgrades' ) ) { |
| @@ -213,35 +229,74 @@ | ||
| 213 | 229 | wp_send_json_error( __( 'You do not have permission to run upgrades.', 'content-control' ) ); |
| 214 | 230 | } |
| 215 | 231 | |
| 216 | 232 | try { |
| 217 | - $stream = new \ContentControl\Services\UpgradeStream( 'upgrades' ); | |
| 218 | 233 | $upgrades = $this->get_required_upgrades(); |
| 219 | 234 | $count = count( $upgrades ); |
| 235 | + $stream = new \ContentControl\Services\UpgradeStream( 'upgrades' ); | |
| 236 | + $stream->start(); | |
| 220 | 237 | |
| 221 | 238 | // First do/while loop starts the stream and breaks if connection aborted. |
| 222 | 239 | do { |
| 223 | - $stream->start(); | |
| 224 | 240 | $stream->start_upgrades( $count, __( 'Upgrades started', 'content-control' ) ); |
| 225 | 241 | |
| 226 | 242 | $failed_upgrades = []; |
| 227 | 243 | |
| 228 | 244 | // This second while loop runs the upgrades. |
| 229 | - while ( ! empty( $upgrades ) ) { | |
| 230 | - $upgrade = array_shift( $upgrades ); | |
| 245 | + while ( ! empty( $this->get_required_upgrades() ) ) { | |
| 246 | + $required_upgrades = $this->get_required_upgrades(); | |
| 247 | + $upgrade = array_shift( $required_upgrades ); | |
| 248 | + $upgrade_name = get_upgrade_name( $upgrade ); | |
| 231 | 249 | |
| 250 | + if ( ! isset( $failed_upgrades[ $upgrade_name ] ) ) { | |
| 251 | + $failed_upgrades[ $upgrade_name ] = 0; | |
| 252 | + } elseif ( $failed_upgrades[ $upgrade_name ] > 0 ) { | |
| 253 | + $stream->send_event( | |
| 254 | + 'task:retry', | |
| 255 | + [ | |
| 256 | + 'message' => __( 'Retrying upgrade', 'content-control' ), | |
| 257 | + 'data' => [ | |
| 258 | + 'key' => $upgrade_name, | |
| 259 | + 'label' => $upgrade->label(), | |
| 260 | + ], | |
| 261 | + ] | |
| 262 | + ); | |
| 263 | + } | |
| 264 | + | |
| 265 | + if ( $failed_upgrades[ $upgrade_name ] > 2 ) { | |
| 266 | + $stream->send_error( [ | |
| 267 | + 'message' => __( 'Some upgrades failed to complete.', 'content-control' ), | |
| 268 | + ] ); | |
| 269 | + | |
| 270 | + $stream->send_event( 'upgrades:error', [ | |
| 271 | + 'message' => __( 'Upgrade did not complete, see error logs above.', 'content-control' ), | |
| 272 | + 'data' => $failed_upgrades, | |
| 273 | + ] ); | |
| 274 | + return; | |
| 275 | + } | |
| 276 | + | |
| 232 | 277 | $result = $upgrade->stream_run( $stream ); |
| 233 | 278 | |
| 234 | 279 | if ( is_wp_error( $result ) ) { |
| 235 | - $stream->send_error( $result ); | |
| 280 | + $stream->send_error( [ | |
| 281 | + 'message' => sprintf( | |
| 282 | + // translators: %s: error message. | |
| 283 | + __( 'Some upgrades failed to complete: %s', 'content-control' ), | |
| 284 | + $result->get_error_message() | |
| 285 | + ), | |
| 286 | + 'data' => $result, | |
| 287 | + ] ); | |
| 236 | 288 | } elseif ( false !== $result ) { |
| 237 | 289 | mark_upgrade_complete( $upgrade ); |
| 238 | 290 | } else { |
| 239 | 291 | // False means the upgrade failed. |
| 240 | - $failed_upgrades[] = get_upgrade_name( $upgrade ); | |
| 292 | + ++$failed_upgrades[ $upgrade_name ]; | |
| 241 | 293 | } |
| 242 | 294 | } |
| 243 | 295 | |
| 296 | + // Filter out any upgrades that have fail counts of 0. | |
| 297 | + $failed_upgrades = array_filter( $failed_upgrades ); | |
| 298 | + | |
| 244 | 299 | if ( ! empty( $failed_upgrades ) ) { |
| 245 | 300 | $stream->send_error( [ |
| 246 | 301 | 'message' => __( 'Some upgrades failed to complete.', 'content-control' ), |
| 247 | 302 | 'data' => $failed_upgrades, |
| @@ -252,14 +307,20 @@ | ||
| 252 | 307 | $stream->complete_upgrades( __( 'Upgrades complete!', 'content-control' ) ); |
| 253 | 308 | } |
| 254 | 309 | } while ( ! $stream->should_abort() ); |
| 255 | 310 | } catch ( \Exception $e ) { |
| 256 | - $stream->send_error( $e ); | |
| 311 | + if ( isset( $stream ) ) { | |
| 312 | + $stream->send_error( $e ); | |
| 313 | + } else { | |
| 314 | + wp_send_json_error( $e ); | |
| 315 | + } | |
| 257 | 316 | } |
| 258 | 317 | } |
| 259 | 318 | |
| 260 | 319 | /** |
| 261 | - * AJAX Handler | |
| 320 | + * AJAX Handler. | |
| 321 | + * | |
| 322 | + * @return void | |
| 262 | 323 | */ |
| 263 | 324 | public function ajax_handler_demo() { |
| 264 | 325 | // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 265 | 326 | if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_REQUEST['nonce'] ), 'content_control_upgrades' ) ) { |
| @@ -310,17 +371,26 @@ | ||
| 310 | 371 | |
| 311 | 372 | $stream->complete_upgrades( __( 'Upgrades complete!', 'content-control' ) ); |
| 312 | 373 | } while ( ! $stream->should_abort() && ! empty( $upgrades ) ); |
| 313 | 374 | } catch ( \Exception $e ) { |
| 314 | - $stream->send_error( $e ); | |
| 375 | + if ( isset( $stream ) ) { | |
| 376 | + $stream->send_error( [ | |
| 377 | + 'message' => $e->getMessage(), | |
| 378 | + 'data' => $e, | |
| 379 | + ] ); | |
| 380 | + } else { | |
| 381 | + wp_send_json_error( $e ); | |
| 382 | + } | |
| 315 | 383 | } |
| 316 | 384 | } |
| 317 | 385 | |
| 318 | 386 | /** |
| 319 | 387 | * Render admin notices if available. |
| 388 | + * | |
| 389 | + * @return void | |
| 320 | 390 | */ |
| 321 | 391 | public function admin_notices() { |
| 322 | - if ( ! is_admin() ) { | |
| 392 | + if ( ! is_admin() || ! current_user_can( $this->container->get_permission( 'manage_settings' ) ) ) { | |
| 323 | 393 | return; |
| 324 | 394 | } |
| 325 | 395 | |
| 326 | 396 | if ( ! $this->has_upgrades() ) { |
| @@ -377,11 +447,9 @@ | ||
| 377 | 447 | </div> |
| 378 | 448 | |
| 379 | 449 | <div class="notice-content"> |
| 380 | 450 | <p> |
| 381 | - <strong> | |
| 382 | - <?php esc_html_e( 'Content Control has been updated and needs to run some database upgrades.', 'content-control' ); ?> | |
| 383 | - </strong> | |
| 451 | + <strong><?php esc_html_e( 'Content Control has been updated and needs to run some database upgrades.', 'content-control' ); ?></strong> | |
| 384 | 452 | </p> |
| 385 | 453 | <ul class="notice-actions"> |
| 386 | 454 | <li> |
| 387 | 455 | <a class="content-control-go-to-settings button button-tertiary" href="<?php echo esc_attr( admin_url( 'options-general.php?page=content-control-settings' ) ); ?>" data-reason="am_now"> |
| @@ -396,13 +464,17 @@ | ||
| 396 | 464 | |
| 397 | 465 | /** |
| 398 | 466 | * Add localized vars to settings page if there are upgrades to run. |
| 399 | 467 | * |
| 400 | - * @param array $vars Localized vars. | |
| 468 | + * @param array<string,mixed> $vars Localized vars. | |
| 401 | 469 | * |
| 402 | - * @return array | |
| 470 | + * @return array<string,mixed> | |
| 403 | 471 | */ |
| 404 | 472 | public function localize_vars( $vars ) { |
| 473 | + if ( ! is_admin() || ! current_user_can( $this->container->get_permission( 'manage_settings' ) ) ) { | |
| 474 | + return $vars; | |
| 475 | + } | |
| 476 | + | |
| 405 | 477 | $vars['hasUpgrades'] = false; |
| 406 | 478 | |
| 407 | 479 | if ( ! $this->has_upgrades() ) { |
| 408 | 480 | return $vars; |