PluginProbe
Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More / trunk
Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More vtrunk
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 trunk, at classes/Controllers/Admin/Upgrades.php

510 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 if ( ! isset( $_REQUEST['nonce'] ) || ! is_string( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), 'content_control_upgrades' ) ) {
224 wp_send_json_error( __( 'Invalid nonce.', 'content-control' ) );
225 }
226
227 if ( ! current_user_can( $this->container->get_permission( 'manage_settings' ) ) ) {
228 wp_send_json_error( __( 'You do not have permission to run upgrades.', 'content-control' ) );
229 }
230
231 try {
232 $upgrades = $this->get_required_upgrades();
233 $count = count( $upgrades );
234 $stream = new \ContentControl\Services\UpgradeStream( 'upgrades' );
235 $stream->start();
236
237 // First do/while loop starts the stream and breaks if connection aborted.
238 do {
239 $stream->start_upgrades( $count, __( 'Upgrades started', 'content-control' ) );
240
241 $failed_upgrades = [];
242
243 // This second while loop runs the upgrades.
244 while ( ! empty( $this->get_required_upgrades() ) ) {
245 $required_upgrades = $this->get_required_upgrades();
246 $upgrade = array_shift( $required_upgrades );
247 $upgrade_name = get_upgrade_name( $upgrade );
248
249 if ( ! isset( $failed_upgrades[ $upgrade_name ] ) ) {
250 $failed_upgrades[ $upgrade_name ] = 0;
251 } elseif ( $failed_upgrades[ $upgrade_name ] > 0 ) {
252 $stream->send_event(
253 'task:retry',
254 [
255 'message' => __( 'Retrying upgrade', 'content-control' ),
256 'data' => [
257 'key' => $upgrade_name,
258 'label' => $upgrade->label(),
259 ],
260 ]
261 );
262 }
263
264 if ( $failed_upgrades[ $upgrade_name ] > 2 ) {
265 $stream->send_error( [
266 'message' => __( 'Some upgrades failed to complete.', 'content-control' ),
267 ] );
268
269 $stream->send_event( 'upgrades:error', [
270 'message' => __( 'Upgrade did not complete, see error logs above.', 'content-control' ),
271 'data' => $failed_upgrades,
272 ] );
273 return;
274 }
275
276 $result = $upgrade->stream_run( $stream );
277
278 if ( is_wp_error( $result ) ) {
279 $stream->send_error( [
280 'message' => sprintf(
281 // translators: %s: error message.
282 __( 'Some upgrades failed to complete: %s', 'content-control' ),
283 $result->get_error_message()
284 ),
285 'data' => $result,
286 ] );
287 } elseif ( false !== $result ) {
288 mark_upgrade_complete( $upgrade );
289 } else {
290 // False means the upgrade failed.
291 ++$failed_upgrades[ $upgrade_name ];
292 }
293 }
294
295 // Filter out any upgrades that have fail counts of 0.
296 $failed_upgrades = array_filter( $failed_upgrades );
297
298 if ( ! empty( $failed_upgrades ) ) {
299 $stream->send_error( [
300 'message' => __( 'Some upgrades failed to complete.', 'content-control' ),
301 'data' => $failed_upgrades,
302 ] );
303
304 $stream->complete_upgrades( __( 'Upgrades complete with errors.', 'content-control' ) );
305 } else {
306 $stream->complete_upgrades( __( 'Upgrades complete!', 'content-control' ) );
307 }
308 } while ( ! $stream->should_abort() );
309 } catch ( \Exception $e ) {
310 if ( isset( $stream ) ) {
311 $stream->send_error( $e );
312 } else {
313 wp_send_json_error( $e );
314 }
315 }
316 }
317
318 /**
319 * AJAX Handler.
320 *
321 * @return void
322 */
323 public function ajax_handler_demo() {
324 if ( ! isset( $_REQUEST['nonce'] ) || ! is_string( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), 'content_control_upgrades' ) ) {
325 wp_send_json_error( __( 'Invalid nonce.', 'content-control' ) );
326 }
327
328 if ( ! current_user_can( $this->container->get_permission( 'manage_settings' ) ) ) {
329 wp_send_json_error( __( 'You do not have permission to run upgrades.', 'content-control' ) );
330 }
331
332 try {
333 $upgrades = $this->get_required_upgrades();
334 $count = count( $upgrades ) * 2;
335
336 $stream = new \ContentControl\Services\UpgradeStream( 'upgrades' );
337
338 $stream->start();
339
340 $count = wp_rand( 3, 10 );
341
342 do {
343 $stream->start_upgrades( $count, __( 'Upgrades started', 'content-control' ) );
344
345 // test loop of 1000 upgrades.
346 $test_delay = 60000;
347 for ( $i = 0; $i < $count; $i++ ) {
348 usleep( $test_delay );
349
350 $task_count = wp_rand( 5, 100 );
351
352 $stream->start_task(
353 __( 'Migrating restrictions', 'content-control' ),
354 $task_count
355 );
356
357 // test loop of 1000 upgrades.
358 for ( $i2 = 0; $i2 < $task_count; $i2++ ) {
359 usleep( $test_delay );
360 $stream->update_task_progress( $i2 + 1 );
361 }
362
363 usleep( $test_delay );
364
365 // translators: %d: number of restrictions migrated.
366 $stream->complete_task( sprintf( __( '%d restrictions migrated', 'content-control' ), $i2 ) );
367 }
368 usleep( $test_delay );
369
370 $stream->complete_upgrades( __( 'Upgrades complete!', 'content-control' ) );
371 } while ( ! $stream->should_abort() && ! empty( $upgrades ) );
372 } catch ( \Exception $e ) {
373 if ( isset( $stream ) ) {
374 $stream->send_error( [
375 'message' => $e->getMessage(),
376 'data' => $e,
377 ] );
378 } else {
379 wp_send_json_error( $e );
380 }
381 }
382 }
383
384 /**
385 * Render admin notices if available.
386 *
387 * @return void
388 */
389 public function admin_notices() {
390 if ( ! is_admin() || ! current_user_can( $this->container->get_permission( 'manage_settings' ) ) ) {
391 return;
392 }
393
394 if ( ! $this->has_upgrades() ) {
395 return;
396 }
397
398 $screen = get_current_screen();
399
400 if ( 'settings_page_content-control-settings' === $screen->id ) {
401 return;
402 }
403
404 ?>
405 <style>
406 .content-control-notice {
407 display: flex;
408 align-items: center;
409 gap: 16px;
410 padding: 8px;
411 margin-top: 16px;
412 margin-bottom: 16px;
413 }
414
415 .content-control-notice .notice-logo {
416 flex: 0 0 60px;
417 max-width: 60px;
418 font-size: 60px;
419 }
420
421 .content-control-notice .notice-content {
422 flex-grow: 1;
423 }
424
425 .content-control-notice p {
426 margin-bottom: 0;
427 max-width: 800px;
428 }
429
430 .content-control-notice .notice-actions {
431 margin-top: 10px;
432 margin-bottom: 0;
433 padding-left: 0;
434 list-style: none;
435
436 display: flex;
437 gap: 16px;
438 align-items: center;
439 }
440 </style>
441
442 <div class="notice notice-info content-control-notice">
443 <div class="notice-logo">
444 <img class="logo" width="60" src="<?php echo esc_attr( $this->container->get_url( 'assets/images/illustration-check.svg' ) ); ?>" />
445 </div>
446
447 <div class="notice-content">
448 <p>
449 <strong><?php esc_html_e( 'Content Control has been updated and needs to run some database upgrades.', 'content-control' ); ?></strong>
450 </p>
451 <ul class="notice-actions">
452 <li>
453 <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">
454 🚨 <?php esc_html_e( 'Upgrade Now', 'content-control' ); ?>
455 </a>
456 </li>
457 </ul>
458 </div>
459 </div>
460 <?php
461 }
462
463 /**
464 * Add localized vars to settings page if there are upgrades to run.
465 *
466 * @param array<string,mixed> $vars Localized vars.
467 *
468 * @return array<string,mixed>
469 */
470 public function localize_vars( $vars ) {
471 if ( ! is_admin() || ! current_user_can( $this->container->get_permission( 'manage_settings' ) ) ) {
472 return $vars;
473 }
474
475 $vars['hasUpgrades'] = false;
476
477 if ( ! $this->has_upgrades() ) {
478 return $vars;
479 }
480
481 $vars['hasUpgrades'] = true;
482 $vars['upgradeNonce'] = wp_create_nonce( 'content_control_upgrades' );
483 $vars['upgradeUrl'] = admin_url( 'admin-ajax.php?action=content_control_upgrades' );
484 $vars['upgrades'] = [];
485
486 $upgrades = $this->get_required_upgrades();
487
488 foreach ( $upgrades as $upgrade ) {
489 $upgrade_name = get_upgrade_name( $upgrade );
490
491 switch ( $upgrade->get_type() ) {
492 case 'restrictions':
493 $vars['hasRestrictionUpgrades'] = true;
494 break;
495 case 'settings':
496 $vars['hasSettingsUpgrades'] = true;
497 break;
498 }
499
500 $vars['upgrades'][ $upgrade_name ] = [
501 'key' => $upgrade_name,
502 'label' => $upgrade->label(),
503 'description' => $upgrade->description(),
504 ];
505 }
506
507 return $vars;
508 }
509 }
510