PluginProbe
Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More / 2.6.2
Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More v2.6.2
trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.10 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 All 47 releases
content-control / classes / Controllers / Admin / Upgrades.php

Upgrades.php in Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More 2.6.2, at classes/Controllers/Admin/Upgrades.php

512 lines 13.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Upgrades Controller Class.
4 *
5 * @package ContentControl
6 */
7
8 namespace ContentControl\Controllers\Admin;
9
10 use ContentControl\Base\Controller;
11
12 defined( 'ABSPATH' ) || exit;
13
14 use function __;
15 use function add_filter;
16 use function add_action;
17 use function esc_html_e;
18 use function esc_attr;
19 use function get_current_screen;
20 use function admin_url;
21 use function is_admin;
22 use function current_user_can;
23 use function wp_create_nonce;
24 use function wp_verify_nonce;
25 use function wp_send_json_error;
26 use function wp_unslash;
27 use function is_wp_error;
28 use function ContentControl\get_upgrade_name;
29 use function ContentControl\is_upgrade_complete;
30 use function ContentControl\mark_upgrade_complete;
31
32 /**
33 * Upgrades Controller.
34 *
35 * @package ContentControl\Admin
36 */
37 class Upgrades extends Controller {
38
39 /**
40 * Initialize the settings page.
41 */
42 public function init() {
43 add_action( 'admin_init', [ $this, 'hooks' ] );
44 add_action( 'wp_ajax_content_control_upgrades', [ $this, 'ajax_handler' ] );
45 add_filter( 'content_control/settings-page_localized_vars', [ $this, 'localize_vars' ] );
46 add_action( 'content_control/update_version', '\\ContentControl\\maybe_force_v2_migrations' );
47 }
48
49 /**
50 * Hook into relevant WP actions.
51 *
52 * @return void
53 */
54 public function hooks() {
55 if ( is_admin() && current_user_can( 'manage_options' ) ) {
56 add_action( 'admin_notices', [ $this, 'admin_notices' ] );
57 add_action( 'network_admin_notices', [ $this, 'admin_notices' ] );
58 add_action( 'user_admin_notices', [ $this, 'admin_notices' ] );
59 }
60 }
61
62 /**
63 * Get a list of all upgrades.
64 *
65 * @return string[]
66 */
67 public function all_upgrades() {
68 return [
69 // Version 2 upgrades.
70 '\ContentControl\Upgrades\Backup_2',
71 '\ContentControl\Upgrades\PluginMeta_2',
72 '\ContentControl\Upgrades\Settings_2',
73 '\ContentControl\Upgrades\UserMeta_2',
74 '\ContentControl\Upgrades\Restrictions_2',
75 ];
76 }
77
78 /**
79 * Check if there are any upgrades to run.
80 *
81 * @return boolean
82 */
83 public function has_upgrades() {
84 return (bool) count( $this->get_required_upgrades() );
85 }
86
87 /**
88 * Get a list of required upgrades.
89 *
90 * Uses a cached list of done upgrades to prevent extra processing.
91 *
92 * @return \ContentControl\Base\Upgrade[]
93 */
94 public function get_required_upgrades() {
95 $required_upgrades = [];
96
97 $all_upgrades = $this->all_upgrades();
98 $required_upgrades = [];
99
100 foreach ( $all_upgrades as $upgrade_class ) {
101 if ( ! class_exists( $upgrade_class ) ) {
102 continue;
103 }
104
105 /**
106 * Upgrade class instance.
107 *
108 * @var \ContentControl\Base\Upgrade $upgrade
109 */
110 $upgrade = new $upgrade_class();
111
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 }
120
121 $required_upgrades[] = $upgrade;
122 }
123
124 // Sort the required upgrades based on prerequisites.
125 $required_upgrades = $this->sort_upgrades_by_prerequisites( $required_upgrades );
126
127 return $required_upgrades;
128 }
129
130 /**
131 * Sort upgrades based on prerequisites using a graph-based approach.
132 *
133 * @param \ContentControl\Base\Upgrade[] $upgrades List of upgrades to sort.
134 *
135 * @return \ContentControl\Base\Upgrade[]
136 */
137 private function sort_upgrades_by_prerequisites( $upgrades ) {
138 // Build the graph of upgrades and their dependencies.
139 $graph = [];
140 // Build a lookup table of upgrades by name.
141 $upgrade_by_name = [];
142
143 // Build the graph & lookup tables.
144 foreach ( $upgrades as $upgrade ) {
145 $updgrade_name = get_upgrade_name( $upgrade );
146
147 // Add the upgrade to the graph with its dependencies.
148 $graph[ $updgrade_name ] = $upgrade->get_dependencies();
149
150 // Add the upgrade to the lookup table.
151 $upgrade_by_name[ $updgrade_name ] = $upgrade;
152 }
153
154 // Perform a topological sort on the graph.
155 $sorted = $this->topological_sort( $graph );
156
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;
163 }
164
165 // Remove null values, these are upgrades that have been marked as done.
166 $sorted_upgrades = array_filter( $sorted_upgrades );
167
168 // Return the sorted upgrades.
169 return $sorted_upgrades;
170 }
171
172 /**
173 * Perform a topological sort on a graph.
174 *
175 * @param array<string,array<string>> $graph Graph to sort.
176 *
177 * @return array<string>
178 */
179 private function topological_sort( $graph ) {
180 $visited = [];
181 $sorted = [];
182
183 foreach ( $graph as $node => $dependencies ) {
184 $this->visit_node( $node, $graph, $visited, $sorted );
185 }
186
187 return $sorted;
188 }
189
190 /**
191 * Visit a node in the graph for topological sort.
192 *
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
199 */
200 private function visit_node( $node, $graph, &$visited, &$sorted ) {
201 if ( isset( $visited[ $node ] ) ) {
202 // Node already visited, skip.
203 return;
204 }
205
206 $visited[ $node ] = true;
207
208 if ( isset( $graph[ $node ] ) && ! empty( $graph[ $node ] ) ) {
209 foreach ( $graph[ $node ] as $dependency ) {
210 $this->visit_node( $dependency, $graph, $visited, $sorted );
211 }
212 }
213
214 $sorted[] = $node;
215 }
216
217 /**
218 * AJAX Handler.
219 *
220 * @return void
221 */
222 public function ajax_handler() {
223 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
224 if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_REQUEST['nonce'] ), 'content_control_upgrades' ) ) {
225 wp_send_json_error( __( 'Invalid nonce.', 'content-control' ) );
226 }
227
228 if ( ! current_user_can( $this->container->get_permission( 'manage_settings' ) ) ) {
229 wp_send_json_error( __( 'You do not have permission to run upgrades.', 'content-control' ) );
230 }
231
232 try {
233 $upgrades = $this->get_required_upgrades();
234 $count = count( $upgrades );
235 $stream = new \ContentControl\Services\UpgradeStream( 'upgrades' );
236 $stream->start();
237
238 // First do/while loop starts the stream and breaks if connection aborted.
239 do {
240 $stream->start_upgrades( $count, __( 'Upgrades started', 'content-control' ) );
241
242 $failed_upgrades = [];
243
244 // This second while loop runs the 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 );
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
277 $result = $upgrade->stream_run( $stream );
278
279 if ( is_wp_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 ] );
288 } elseif ( false !== $result ) {
289 mark_upgrade_complete( $upgrade );
290 } else {
291 // False means the upgrade failed.
292 ++$failed_upgrades[ $upgrade_name ];
293 }
294 }
295
296 // Filter out any upgrades that have fail counts of 0.
297 $failed_upgrades = array_filter( $failed_upgrades );
298
299 if ( ! empty( $failed_upgrades ) ) {
300 $stream->send_error( [
301 'message' => __( 'Some upgrades failed to complete.', 'content-control' ),
302 'data' => $failed_upgrades,
303 ] );
304
305 $stream->complete_upgrades( __( 'Upgrades complete with errors.', 'content-control' ) );
306 } else {
307 $stream->complete_upgrades( __( 'Upgrades complete!', 'content-control' ) );
308 }
309 } while ( ! $stream->should_abort() );
310 } catch ( \Exception $e ) {
311 if ( isset( $stream ) ) {
312 $stream->send_error( $e );
313 } else {
314 wp_send_json_error( $e );
315 }
316 }
317 }
318
319 /**
320 * AJAX Handler.
321 *
322 * @return void
323 */
324 public function ajax_handler_demo() {
325 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
326 if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_REQUEST['nonce'] ), 'content_control_upgrades' ) ) {
327 wp_send_json_error( __( 'Invalid nonce.', 'content-control' ) );
328 }
329
330 if ( ! current_user_can( $this->container->get_permission( 'manage_settings' ) ) ) {
331 wp_send_json_error( __( 'You do not have permission to run upgrades.', 'content-control' ) );
332 }
333
334 try {
335 $upgrades = $this->get_required_upgrades();
336 $count = count( $upgrades ) * 2;
337
338 $stream = new \ContentControl\Services\UpgradeStream( 'upgrades' );
339
340 $stream->start();
341
342 $count = wp_rand( 3, 10 );
343
344 do {
345 $stream->start_upgrades( $count, __( 'Upgrades started', 'content-control' ) );
346
347 // test loop of 1000 upgrades.
348 $test_delay = 60000;
349 for ( $i = 0; $i < $count; $i++ ) {
350 usleep( $test_delay );
351
352 $task_count = wp_rand( 5, 100 );
353
354 $stream->start_task(
355 __( 'Migrating restrictions', 'content-control' ),
356 $task_count
357 );
358
359 // test loop of 1000 upgrades.
360 for ( $i2 = 0; $i2 < $task_count; $i2++ ) {
361 usleep( $test_delay );
362 $stream->update_task_progress( $i2 + 1 );
363 }
364
365 usleep( $test_delay );
366
367 // translators: %d: number of restrictions migrated.
368 $stream->complete_task( sprintf( __( '%d restrictions migrated', 'content-control' ), $i2 ) );
369 }
370 usleep( $test_delay );
371
372 $stream->complete_upgrades( __( 'Upgrades complete!', 'content-control' ) );
373 } while ( ! $stream->should_abort() && ! empty( $upgrades ) );
374 } catch ( \Exception $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 }
383 }
384 }
385
386 /**
387 * Render admin notices if available.
388 *
389 * @return void
390 */
391 public function admin_notices() {
392 if ( ! is_admin() || ! current_user_can( $this->container->get_permission( 'manage_settings' ) ) ) {
393 return;
394 }
395
396 if ( ! $this->has_upgrades() ) {
397 return;
398 }
399
400 $screen = get_current_screen();
401
402 if ( 'settings_page_content-control-settings' === $screen->id ) {
403 return;
404 }
405
406 ?>
407 <style>
408 .content-control-notice {
409 display: flex;
410 align-items: center;
411 gap: 16px;
412 padding: 8px;
413 margin-top: 16px;
414 margin-bottom: 16px;
415 }
416
417 .content-control-notice .notice-logo {
418 flex: 0 0 60px;
419 max-width: 60px;
420 font-size: 60px;
421 }
422
423 .content-control-notice .notice-content {
424 flex-grow: 1;
425 }
426
427 .content-control-notice p {
428 margin-bottom: 0;
429 max-width: 800px;
430 }
431
432 .content-control-notice .notice-actions {
433 margin-top: 10px;
434 margin-bottom: 0;
435 padding-left: 0;
436 list-style: none;
437
438 display: flex;
439 gap: 16px;
440 align-items: center;
441 }
442 </style>
443
444 <div class="notice notice-info content-control-notice">
445 <div class="notice-logo">
446 <img class="logo" width="60" src="<?php echo esc_attr( $this->container->get_url( 'assets/images/illustration-check.svg' ) ); ?>" />
447 </div>
448
449 <div class="notice-content">
450 <p>
451 <strong><?php esc_html_e( 'Content Control has been updated and needs to run some database upgrades.', 'content-control' ); ?></strong>
452 </p>
453 <ul class="notice-actions">
454 <li>
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">
456 🚨 <?php esc_html_e( 'Upgrade Now', 'content-control' ); ?>
457 </a>
458 </li>
459 </ul>
460 </div>
461 </div>
462 <?php
463 }
464
465 /**
466 * Add localized vars to settings page if there are upgrades to run.
467 *
468 * @param array<string,mixed> $vars Localized vars.
469 *
470 * @return array<string,mixed>
471 */
472 public function localize_vars( $vars ) {
473 if ( ! is_admin() || ! current_user_can( $this->container->get_permission( 'manage_settings' ) ) ) {
474 return $vars;
475 }
476
477 $vars['hasUpgrades'] = false;
478
479 if ( ! $this->has_upgrades() ) {
480 return $vars;
481 }
482
483 $vars['hasUpgrades'] = true;
484 $vars['upgradeNonce'] = wp_create_nonce( 'content_control_upgrades' );
485 $vars['upgradeUrl'] = admin_url( 'admin-ajax.php?action=content_control_upgrades' );
486 $vars['upgrades'] = [];
487
488 $upgrades = $this->get_required_upgrades();
489
490 foreach ( $upgrades as $upgrade ) {
491 $upgrade_name = get_upgrade_name( $upgrade );
492
493 switch ( $upgrade->get_type() ) {
494 case 'restrictions':
495 $vars['hasRestrictionUpgrades'] = true;
496 break;
497 case 'settings':
498 $vars['hasSettingsUpgrades'] = true;
499 break;
500 }
501
502 $vars['upgrades'][ $upgrade_name ] = [
503 'key' => $upgrade_name,
504 'label' => $upgrade->label(),
505 'description' => $upgrade->description(),
506 ];
507 }
508
509 return $vars;
510 }
511 }
512