PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.13.0
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.13.0
2.13.0 2.13.1 2.12.0 2.11.1 2.11.0 2.10.0 2.9.0 2.7.4 2.7.5 2.7.6 2.7.7 2.8.0 2.8.1 2.9.1 trunk 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 All 80 releases
ablocks / includes / ajax / dashboard.php

dashboard.php in aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder 2.13.0, at includes/ajax/dashboard.php

855 lines 26.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace ABlocks\Ajax;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 use ABlocks\Classes\AbstractAjaxHandler;
10 use ABlocks\Classes\FileUpload;
11 use ABlocks\Classes\FontLoadLocally;
12 use ABlocks\Classes\FontCollector;
13 use ABlocks\Classes\FontStack;
14 use ABlocks\Classes\DesignAudit;
15 use ABlocks\Helper;
16
17 class Dashboard extends AbstractAjaxHandler {
18 public function __construct() {
19 $this->actions = array(
20 'get_admin_menu_items' => array(
21 'callback' => array( $this, 'get_admin_menu_items' ),
22 'capability' => 'ablocks_access',
23 ),
24 'regenerate_assets' => array(
25 'callback' => array( $this, 'regenerate_assets' ),
26 'capability' => 'ablocks_manage_performance',
27 ),
28 'page_cache_status' => array(
29 'callback' => array( $this, 'page_cache_status' ),
30 'capability' => 'ablocks_manage_performance',
31 ),
32 'purge_page_cache' => array(
33 'callback' => array( $this, 'purge_page_cache' ),
34 'capability' => 'ablocks_manage_performance',
35 ),
36 'run_scanner' => array(
37 'callback' => array( $this, 'run_scanner' ),
38 'capability' => 'ablocks_run_scanner',
39 ),
40 'scanner_dismiss' => array(
41 'callback' => array( $this, 'scanner_dismiss' ),
42 'capability' => 'ablocks_run_scanner',
43 'fields' => array(
44 'id' => 'string',
45 'dismiss' => 'boolean',
46 ),
47 ),
48 'scanner_apply_fix' => array(
49 'callback' => array( $this, 'scanner_apply_fix' ),
50 'capability' => 'ablocks_run_scanner',
51 'fields' => array(
52 'field' => 'string',
53 ),
54 ),
55 'image_stats' => array(
56 'callback' => array( $this, 'image_stats' ),
57 'capability' => 'ablocks_manage_performance',
58 ),
59 'image_optimize_batch' => array(
60 'callback' => array( $this, 'image_optimize_batch' ),
61 'capability' => 'ablocks_manage_performance',
62 'fields' => array(
63 'level' => 'string',
64 'webp' => 'boolean',
65 'all' => 'boolean',
66 ),
67 ),
68 'image_discard_originals' => array(
69 'callback' => array( $this, 'image_discard_originals' ),
70 'capability' => 'ablocks_manage_performance',
71 ),
72 'image_restore_all' => array(
73 'callback' => array( $this, 'image_restore_all' ),
74 'capability' => 'ablocks_manage_performance',
75 ),
76 'image_scan_unused' => array(
77 'callback' => array( $this, 'image_scan_unused' ),
78 'capability' => 'ablocks_manage_performance',
79 'fields' => array(
80 'offset' => 'integer',
81 ),
82 ),
83 'image_scan_duplicates' => array(
84 'callback' => array( $this, 'image_scan_duplicates' ),
85 'capability' => 'ablocks_manage_performance',
86 'fields' => array(
87 'offset' => 'integer',
88 ),
89 ),
90 'image_quarantine' => array(
91 'callback' => array( $this, 'image_quarantine' ),
92 'capability' => 'ablocks_manage_performance',
93 'fields' => array(
94 'ids' => 'json',
95 ),
96 ),
97 'image_quarantine_action' => array(
98 'callback' => array( $this, 'image_quarantine_action' ),
99 'capability' => 'ablocks_manage_performance',
100 'fields' => array(
101 'do' => 'string',
102 'id' => 'integer',
103 ),
104 ),
105 'clear_demo_transients' => array(
106 'callback' => array( $this, 'clear_demo_transients' ),
107 'capability' => 'ablocks_import_templates',
108 ),
109 'install_academy_lms' => array(
110 'callback' => array( $this, 'install_academy_lms' ),
111 'capability' => 'install_plugins',
112 ),
113 'install_storeengine' => array(
114 'callback' => array( $this, 'install_storeengine' ),
115 'capability' => 'install_plugins',
116 ),
117 'install_ecm' => array(
118 'callback' => array( $this, 'install_ecm' ),
119 'capability' => 'install_plugins',
120 ),
121 'download_google_fonts' => array(
122 'callback' => array( $this, 'download_google_fonts' ),
123 'capability' => 'ablocks_manage_global_styles',
124 ),
125 'font_audit' => array(
126 'callback' => array( $this, 'font_audit' ),
127 'capability' => 'ablocks_manage_global_styles',
128 ),
129 'design_audit' => array(
130 'callback' => array( $this, 'design_audit' ),
131 'capability' => 'ablocks_manage_global_styles',
132 ),
133 );
134 }
135
136 /**
137 * Which content still carries hand-picked typography or colours instead of a
138 * global preset.
139 *
140 * The editor lock stops new ones being made; it deliberately leaves existing
141 * content alone so turning the setting on can't restyle a live site. This is
142 * how you find what is already there and fix it on purpose.
143 */
144 public function design_audit() {
145 $post_ids = get_posts(
146 [
147 'post_type' => array_merge(
148 [ 'post', 'page' ],
149 FontCollector::get_site_font_post_types()
150 ),
151 'post_status' => [ 'publish', 'draft', 'pending', 'private' ],
152 'posts_per_page' => 200,
153 'fields' => 'ids',
154 'no_found_rows' => true,
155 'update_post_meta_cache' => false,
156 'update_post_term_cache' => false,
157 ]
158 );
159
160 $pages = [];
161 $families = 0;
162 $typography = 0;
163 $colors = 0;
164
165 foreach ( $post_ids as $post_id ) {
166 $post = get_post( $post_id );
167 if ( ! $post instanceof \WP_Post || false === strpos( (string) $post->post_content, 'wp:ablocks' ) ) {
168 continue;
169 }
170
171 $found = DesignAudit::scan_blocks( parse_blocks( $post->post_content ) );
172 if ( empty( $found['families'] ) && empty( $found['typography'] ) && empty( $found['colors'] ) ) {
173 continue;
174 }
175
176 $families += count( $found['families'] );
177 $typography += count( $found['typography'] );
178 $colors += count( $found['colors'] );
179
180 $pages[] = [
181 'id' => $post_id,
182 'title' => get_the_title( $post_id ),
183 'type' => get_post_type( $post_id ),
184 'edit_link' => get_edit_post_link( $post_id, 'raw' ),
185 'families' => array_values( array_unique( $found['families'] ) ),
186 'typography' => array_values( array_unique( $found['typography'] ) ),
187 'colors' => array_values( array_unique( $found['colors'] ) ),
188 // Sorted by the things the lock actually governs.
189 'total' => count( $found['families'] ) + count( $found['colors'] ),
190 ];
191 }//end foreach
192
193 // Worst offenders first — that is where cleanup pays.
194 usort(
195 $pages,
196 function ( $a, $b ) {
197 return $b['total'] <=> $a['total'];
198 }
199 );
200
201 wp_send_json_success(
202 [
203 'pages' => array_slice( $pages, 0, 50 ),
204 'total_pages' => count( $pages ),
205 'total_families' => $families,
206 'total_typography' => $typography,
207 'total_colors' => $colors,
208 'scanned' => count( $post_ids ),
209 'typography_locked' => (bool) Helper::get_settings( 'lock_global_typography', false ),
210 'strict_locked' => (bool) Helper::get_settings( 'lock_global_typography_strict', false ),
211 'colors_locked' => (bool) Helper::get_settings( 'lock_global_colors', false ),
212 ]
213 );
214 }
215
216 /**
217 * Which fonts this site loads, where they come from, and where they are used.
218 *
219 * Answers the questions that make font problems hard to debug: is this family
220 * self-hosted or still coming from Google, does it have a zero-shift fallback,
221 * and which page is pulling in the eleventh weight.
222 */
223 public function font_audit() {
224 $loader = new FontLoadLocally();
225
226 $global_fonts = FontCollector::get_global_fonts();
227 $site_fonts = FontCollector::get_site_fonts();
228
229 $post_ids = get_posts(
230 [
231 'post_type' => 'any',
232 'post_status' => [ 'publish', 'draft', 'pending', 'private' ],
233 'posts_per_page' => 100,
234 'fields' => 'ids',
235 'no_found_rows' => true,
236 'update_post_term_cache' => false,
237 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
238 [
239 'key' => FontCollector::POST_META_KEY,
240 'compare' => 'EXISTS',
241 ],
242 ],
243 ]
244 );
245
246 $all_fonts = FontCollector::merge( $global_fonts, $site_fonts );
247 $pages = [];
248
249 foreach ( $post_ids as $post_id ) {
250 $fonts = get_post_meta( $post_id, FontCollector::POST_META_KEY, true );
251 if ( empty( $fonts ) || ! is_array( $fonts ) ) {
252 continue;
253 }
254
255 $all_fonts = FontCollector::merge( $all_fonts, $fonts );
256
257 $weight_count = 0;
258 foreach ( $fonts as $weights ) {
259 $weight_count += count( (array) $weights );
260 }
261
262 $pages[] = [
263 'id' => $post_id,
264 'title' => get_the_title( $post_id ),
265 'type' => get_post_type( $post_id ),
266 'edit_link' => get_edit_post_link( $post_id, 'raw' ),
267 'families' => array_keys( $fonts ),
268 'weights' => $weight_count,
269 ];
270 }//end foreach
271
272 // Heaviest pages first — that is where a fix is worth making.
273 usort(
274 $pages,
275 function ( $a, $b ) {
276 return $b['weights'] <=> $a['weights'];
277 }
278 );
279
280 $families = [];
281 foreach ( $all_fonts as $family => $weights ) {
282 $weights = array_values( array_unique( array_map( 'strval', (array) $weights ) ) );
283 sort( $weights );
284
285 $missing = $loader->get_missing( [ $family => $weights ] );
286
287 $families[] = [
288 'family' => $family,
289 'weights' => $weights,
290 'category' => FontStack::get_category( $family ),
291 'stack' => FontStack::build( $family ),
292 'self_hosted' => empty( $missing ),
293 'has_metrics' => FontStack::has_metrics( $family ),
294 'in_global' => isset( $global_fonts[ $family ] ),
295 'in_site' => isset( $site_fonts[ $family ] ),
296 ];
297 }
298
299 usort(
300 $families,
301 function ( $a, $b ) {
302 return strcasecmp( $a['family'], $b['family'] );
303 }
304 );
305
306 $total_weights = 0;
307 foreach ( $all_fonts as $weights ) {
308 $total_weights += count( (array) $weights );
309 }
310
311 wp_send_json_success(
312 [
313 'families' => $families,
314 'pages' => array_slice( $pages, 0, 25 ),
315 'total_families' => count( $families ),
316 'total_weights' => $total_weights,
317 'remote_families' => count(
318 array_filter(
319 $families,
320 function ( $item ) {
321 return ! $item['self_hosted'];
322 }
323 )
324 ),
325 'metric_fallbacks' => FontStack::metric_fallback_enabled(),
326 ]
327 );
328 }
329
330 public function get_admin_menu_items() {
331 $menu_items = wp_json_encode( Helper::get_admin_menu_list() );
332 wp_send_json_success( $menu_items );
333 }
334 public function regenerate_assets() {
335 $FileUpload = new FileUpload();
336 $has_delete = $FileUpload->delete_files();
337 Helper::clear_third_party_plugin_cache();
338 update_option( ABLOCKS_FONTS_SETTINGS_NAME, '{}' );
339 wp_send_json_success( $has_delete );
340 }
341
342 /**
343 * Cache occupancy and environment, for the Performance settings tab.
344 *
345 * `object_cache` is reported because the template cache is inert without a
346 * persistent one — its toggle would otherwise appear to do nothing, and the
347 * UI needs to say why rather than leave the user guessing.
348 */
349 public function page_cache_status() {
350 $stats = \ABlocks\Classes\PageCache\Store::stats();
351
352 $css_dir = \ABlocks\Performance\StyleConsolidator::base_dir();
353 $css_files = is_dir( $css_dir ) ? glob( $css_dir . '/*.css' ) : [];
354
355 wp_send_json_success(
356 array(
357 'pages' => (int) $stats['pages'],
358 'files' => (int) $stats['files'],
359 'bytes' => (int) $stats['bytes'],
360 'size' => size_format( $stats['bytes'], 2 ),
361 'css_files' => is_array( $css_files ) ? count( $css_files ) : 0,
362 'object_cache' => \ABlocks\Classes\CacheBackend::is_persistent(),
363 )
364 );
365 }
366
367 /**
368 * Empty every cache this plugin owns, and report what is left.
369 *
370 * Consolidated stylesheets are deliberately NOT removed here. They are
371 * content-addressed and referenced by cached HTML, so deleting them while a
372 * cached page still points at one would strip that page's styling. They are
373 * reclaimed by age instead, via StyleConsolidator::prune().
374 */
375 public function purge_page_cache() {
376 $removed = \ABlocks\Classes\PageCache\Store::flush();
377
378 // Fragments and resolved templates live behind generation counters, so
379 // bumping those invalidates them without a scan.
380 \ABlocks\Performance\FragmentCache::bump_version();
381 \ABlocks\Performance\TemplateCache::maybe_bump_version();
382
383 Helper::clear_third_party_plugin_cache();
384
385 $stats = \ABlocks\Classes\PageCache\Store::stats();
386
387 wp_send_json_success(
388 array(
389 'removed' => (int) $removed,
390 'pages' => (int) $stats['pages'],
391 'size' => size_format( $stats['bytes'], 2 ),
392 )
393 );
394 }
395
396 /**
397 * Audit the site and report what to fix.
398 */
399 public function run_scanner() {
400 // is_plugin_active() lives in an admin include that is not loaded for
401 // AJAX requests, and the conflicting-cache check depends on it.
402 if ( ! function_exists( 'is_plugin_active' ) ) {
403 require_once ABSPATH . 'wp-admin/includes/plugin.php';
404 }
405
406 wp_send_json_success( \ABlocks\Classes\Scanner\Scanner::run() );
407 }
408
409 /**
410 * Ignore a check, or bring an ignored one back.
411 *
412 * Kept rather than deleted: an ignored check still appears in its own list
413 * with an undo, so a decision made months ago stays visible instead of
414 * quietly disappearing from the report.
415 *
416 * @param array $payload Request fields.
417 */
418 public function scanner_dismiss( $payload ) {
419 $id = ! empty( $payload['id'] ) ? sanitize_key( $payload['id'] ) : '';
420 if ( '' === $id ) {
421 wp_send_json_error( __( 'No check given.', 'ablocks' ) );
422 }
423
424 if ( ! function_exists( 'is_plugin_active' ) ) {
425 require_once ABSPATH . 'wp-admin/includes/plugin.php';
426 }
427
428 \ABlocks\Classes\Scanner\Scanner::set_dismissed( $id, ! empty( $payload['dismiss'] ) );
429
430 wp_send_json_success( array( 'report' => \ABlocks\Classes\Scanner\Scanner::run() ) );
431 }
432
433 /**
434 * Switch on one setting the scanner recommended.
435 *
436 * The allowed fields are taken from the scanner's own current output rather
437 * than from a hard-coded list. That means this endpoint can only ever change
438 * something the scanner is, right now, asking the user to change — a request
439 * naming any other option is rejected, and the two can never drift apart.
440 *
441 * @param array $payload Request fields.
442 */
443 public function scanner_apply_fix( $payload ) {
444 $field = ! empty( $payload['field'] ) ? sanitize_key( $payload['field'] ) : '';
445 if ( '' === $field ) {
446 wp_send_json_error( __( 'No setting given.', 'ablocks' ) );
447 }
448
449 if ( ! function_exists( 'is_plugin_active' ) ) {
450 require_once ABSPATH . 'wp-admin/includes/plugin.php';
451 }
452
453 $report = \ABlocks\Classes\Scanner\Scanner::run();
454 $allowed = array();
455 foreach ( $report['checks'] as $check ) {
456 if ( ! empty( $check['field'] ) ) {
457 $allowed[] = $check['field'];
458 }
459 }
460
461 if ( ! in_array( $field, $allowed, true ) ) {
462 wp_send_json_error( __( 'That setting is not one the scanner offers to change.', 'ablocks' ) );
463 }
464
465 $settings = json_decode( get_option( ABLOCKS_SETTINGS_NAME, '{}' ), true );
466 $settings = is_array( $settings ) ? $settings : array();
467 $settings[ $field ] = true;
468
469 update_option( ABLOCKS_SETTINGS_NAME, wp_json_encode( $settings ) );
470
471 // The scanner reads settings through the global that is populated at
472 // bootstrap, so refresh it before re-running or the fix appears not to
473 // have taken effect.
474 $GLOBALS['ablocks_settings'] = json_decode( wp_json_encode( $settings ) );
475
476 wp_send_json_success(
477 array(
478 'field' => $field,
479 'report' => \ABlocks\Classes\Scanner\Scanner::run(),
480 )
481 );
482 }
483
484 /**
485 * Totals for the Images panel.
486 */
487 public function image_stats() {
488 wp_send_json_success( \ABlocks\Performance\ImageTools::stats() );
489 }
490
491 /**
492 * Optimize one batch of images.
493 *
494 * Deliberately a small batch per request rather than one long job: PHP
495 * timeouts and memory limits are the normal failure mode for bulk media
496 * work, and a browser that walks batches can report progress and resume.
497 *
498 * @param array $payload Request fields.
499 */
500 public function image_optimize_batch( $payload ) {
501 $level = ! empty( $payload['level'] ) ? (string) $payload['level'] : '2x';
502 $webp = ! empty( $payload['webp'] );
503 $all = ! empty( $payload['all'] );
504
505 $ids = \ABlocks\Classes\Images\Compressor::pending_ids( 5, $all );
506 $before = 0;
507 $after = 0;
508 $done = array();
509
510 foreach ( $ids as $id ) {
511 $result = \ABlocks\Classes\Images\Compressor::optimize_attachment( $id, $level, $webp );
512 $before += (int) $result['before'];
513 $after += (int) $result['after'];
514
515 // Named so the UI can show what it is working through rather than an
516 // anonymous counter.
517 $done[] = array(
518 'id' => (int) $id,
519 'title' => get_the_title( $id ),
520 'thumb' => wp_get_attachment_image_url( $id, 'thumbnail' ),
521 'before' => size_format( (int) $result['before'], 1 ),
522 'after' => size_format( (int) $result['after'], 1 ),
523 'saved' => $result['before'] > 0
524 ? round( 100 * ( $result['before'] - $result['after'] ) / $result['before'] )
525 : 0,
526 );
527 }
528
529 $stats = \ABlocks\Performance\ImageTools::stats();
530
531 wp_send_json_success(
532 array(
533 'processed' => count( $ids ),
534 'done' => count( $ids ) < 5,
535 'items' => $done,
536 'saved' => size_format( max( 0, $before - $after ), 1 ),
537 'stats' => $stats,
538 )
539 );
540 }
541
542 /**
543 * Delete every stored original, reclaiming the disk they occupy.
544 *
545 * Irreversible by definition: it removes the only copy that "restore" could
546 * have restored, and the only clean source a different strength could have
547 * been applied from. Each affected attachment is flagged so a later run
548 * refuses rather than silently adopting the compressed file as its baseline.
549 */
550 public function image_discard_originals() {
551 $ids = \ABlocks\Classes\Images\Compressor::ids_with_originals();
552 $freed = 0;
553
554 foreach ( $ids as $id ) {
555 $freed += \ABlocks\Classes\Images\Compressor::discard_originals( $id );
556 }
557
558 wp_send_json_success(
559 array(
560 'cleared' => count( $ids ),
561 'freed' => size_format( $freed, 1 ),
562 'stats' => \ABlocks\Performance\ImageTools::stats(),
563 )
564 );
565 }
566
567 /**
568 * Put every optimized image back to its original.
569 */
570 public function image_restore_all() {
571 $ids = \ABlocks\Classes\Images\Compressor::pending_ids( 10000, true );
572 $count = 0;
573 foreach ( $ids as $id ) {
574 $count += \ABlocks\Classes\Images\Compressor::restore_attachment( $id );
575 }
576
577 wp_send_json_success(
578 array(
579 'restored' => $count,
580 'stats' => \ABlocks\Performance\ImageTools::stats(),
581 )
582 );
583 }
584
585 /**
586 * Scan a slice of the media library for unreferenced images.
587 *
588 * @param array $payload Request fields.
589 */
590 public function image_scan_unused( $payload ) {
591 $offset = isset( $payload['offset'] ) ? (int) $payload['offset'] : 0;
592 $batch = \ABlocks\Classes\Images\UnusedScanner::scan( 25, $offset );
593
594 foreach ( $batch['items'] as &$item ) {
595 $item['size'] = size_format( $item['bytes'], 1 );
596 }
597 unset( $item );
598
599 wp_send_json_success( $batch );
600 }
601
602 /**
603 * Hash a slice of the library, and return the duplicate groups once done.
604 *
605 * Hashing reads whole files, so it is batched like the other bulk tools —
606 * the browser walks it rather than one request trying to read the entire
607 * media library before PHP gives up.
608 *
609 * @param array $payload Request fields.
610 */
611 public function image_scan_duplicates( $payload ) {
612 $offset = isset( $payload['offset'] ) ? (int) $payload['offset'] : 0;
613 $batch = \ABlocks\Classes\Images\DuplicateScanner::scan( 40, $offset );
614
615 // Groups are only assembled at the end: reporting them mid-scan would
616 // show duplicates that turn out to have a third copy further down.
617 $batch['groups'] = $batch['done'] ? \ABlocks\Classes\Images\DuplicateScanner::groups() : array();
618 $batch['summary'] = $batch['done'] ? \ABlocks\Classes\Images\DuplicateScanner::summary() : null;
619
620 wp_send_json_success( $batch );
621 }
622
623 /**
624 * Move reviewed images into quarantine.
625 *
626 * @param array $payload Request fields.
627 */
628 public function image_quarantine( $payload ) {
629 $ids = isset( $payload['ids'] ) ? (array) $payload['ids'] : array();
630 $ids = array_filter( array_map( 'intval', $ids ) );
631
632 $held = 0;
633 $bytes = 0;
634 $skipped = array();
635
636 foreach ( $ids as $id ) {
637 $result = \ABlocks\Classes\Images\Quarantine::hold( $id );
638 if ( $result['ok'] ) {
639 $held++;
640 $bytes += (int) $result['bytes'];
641 } else {
642 $skipped[] = array(
643 'id' => $id,
644 'reason' => $result['message'],
645 );
646 }
647 }
648
649 wp_send_json_success(
650 array(
651 'held' => $held,
652 'freed' => size_format( $bytes, 1 ),
653 'skipped' => $skipped,
654 'stats' => \ABlocks\Performance\ImageTools::stats(),
655 )
656 );
657 }
658
659 /**
660 * List, restore from, or empty the quarantine.
661 *
662 * @param array $payload Request fields.
663 */
664 public function image_quarantine_action( $payload ) {
665 $do = ! empty( $payload['do'] ) ? sanitize_key( $payload['do'] ) : 'list';
666
667 if ( 'restore' === $do ) {
668 $ok = \ABlocks\Classes\Images\Quarantine::restore( (int) $payload['id'] );
669 wp_send_json_success(
670 array(
671 'ok' => (bool) $ok,
672 'items' => array_values( \ABlocks\Classes\Images\Quarantine::records() ),
673 'stats' => \ABlocks\Performance\ImageTools::stats(),
674 )
675 );
676 }
677
678 if ( 'sweep' === $do ) {
679 // Zero days: the user asked to empty it now, so retention does not
680 // apply. Retention protects against forgetting, not against intent.
681 $result = \ABlocks\Classes\Images\Quarantine::sweep( 0 );
682 wp_send_json_success(
683 array(
684 'deleted' => (int) $result['deleted'],
685 'freed' => size_format( (int) $result['bytes'], 1 ),
686 'items' => array_values( \ABlocks\Classes\Images\Quarantine::records() ),
687 'stats' => \ABlocks\Performance\ImageTools::stats(),
688 )
689 );
690 }
691
692 $items = array_values( \ABlocks\Classes\Images\Quarantine::records() );
693 foreach ( $items as &$item ) {
694 $item['size'] = size_format( isset( $item['bytes'] ) ? (int) $item['bytes'] : 0, 1 );
695 $item['held'] = isset( $item['time'] ) ? gmdate( 'Y-m-d', (int) $item['time'] ) : '';
696 unset( $item['files'] );
697 }
698 unset( $item );
699
700 wp_send_json_success( array( 'items' => $items ) );
701 }
702
703 public function clear_demo_transients() {
704 global $wpdb;
705
706 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
707 $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE '_transient_ablocks_demo_%' OR option_name LIKE '_transient_timeout_ablocks_demo_%'" );
708
709 if ( ! empty( $wpdb->last_error ) ) {
710 wp_send_json_error( $wpdb->last_error );
711 }
712
713 wp_send_json_success();
714 }
715 public function install_academy_lms() {
716 // Check user permissions
717 if ( ! current_user_can( 'install_plugins' ) ) {
718 wp_send_json_error( __( 'You do not have sufficient permissions to install plugins.', 'ablocks' ) );
719 }
720
721 // Check if the plugin is already installed
722 if ( ! Helper::is_plugin_installed( 'academy/academy.php' ) ) {
723
724 $plugin_status = $this->install_plugin( 'academy', true );
725 if ( $plugin_status ) {
726 wp_send_json_success( __( 'Plugin installed and activated successfully!', 'ablocks' ) );
727 }
728 wp_send_json_error( __( 'Sorry, failed to download.', 'ablocks' ) );
729 }
730
731 // Activate the plugin
732 $activate_status = activate_plugin( 'academy/academy.php' );
733 if ( is_wp_error( $activate_status ) ) {
734 wp_send_json_error( 'Plugin activation failed: ' . $activate_status->get_error_message() );
735 }
736 wp_send_json_success( __( 'Plugin installed and activated successfully!', 'ablocks' ) );
737 }
738 public function install_storeengine() {
739 // Check user permissions
740 if ( ! current_user_can( 'install_plugins' ) ) {
741 wp_send_json_error( __( 'You do not have sufficient permissions to install plugins.', 'ablocks' ) );
742 }
743
744 // Check if the plugin is already installed
745 if ( ! Helper::is_plugin_installed( 'storeengine/storeengine.php' ) ) {
746
747 $plugin_status = $this->install_plugin( 'storeengine', true );
748 if ( $plugin_status ) {
749 wp_send_json_success( __( 'Plugin installed and activated successfully!', 'ablocks' ) );
750 }
751 wp_send_json_error( __( 'Sorry, failed to download.', 'ablocks' ) );
752 }
753
754 // Activate the plugin
755 $activate_status = activate_plugin( 'storeengine/storeengine.php' );
756 if ( is_wp_error( $activate_status ) ) {
757 wp_send_json_error( 'Plugin activation failed: ' . $activate_status->get_error_message() );
758 }
759 wp_send_json_success( __( 'Plugin installed and activated successfully!', 'ablocks' ) );
760 }
761 public function install_ecm() {
762 // Check user permissions
763 if ( ! current_user_can( 'install_plugins' ) ) {
764 wp_send_json_error( __( 'You do not have sufficient permissions to install plugins.', 'ablocks' ) );
765 }
766
767 // Check if the plugin is already installed
768 if ( ! Helper::is_plugin_installed( 'easy-content-manager/easy-content-manager.php' ) ) {
769
770 $plugin_status = $this->install_plugin( 'easy-content-manager', true );
771 if ( $plugin_status ) {
772 wp_send_json_success( __( 'Plugin installed and activated successfully!', 'ablocks' ) );
773 }
774 wp_send_json_error( __( 'Sorry, failed to download.', 'ablocks' ) );
775 }
776
777 // Activate the plugin
778 $activate_status = activate_plugin( 'easy-content-manager/easy-content-manager.php' );
779 if ( is_wp_error( $activate_status ) ) {
780 wp_send_json_error( 'Plugin activation failed: ' . $activate_status->get_error_message() );
781 }
782 wp_send_json_success( __( 'Plugin installed and activated successfully!', 'ablocks' ) );
783 }
784 public function install_plugin( $slug = '', $active = true ) {
785 if ( empty( $slug ) ) {
786 return new \WP_Error( 'empty_arg', __( 'Argument should not be empty.', 'ablocks' ) );
787 }
788
789 include_once ABSPATH . 'wp-admin/includes/file.php';
790 include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
791 include_once ABSPATH . 'wp-admin/includes/class-automatic-upgrader-skin.php';
792
793 $plugin_data = $this->get_remote_plugin_data( $slug );
794
795 if ( is_wp_error( $plugin_data ) ) {
796 return $plugin_data;
797 }
798
799 $upgrader = new \Plugin_Upgrader( new \Automatic_Upgrader_Skin() );
800
801 // install plugin
802 $install = $upgrader->install( $plugin_data->download_link );
803
804 if ( is_wp_error( $install ) ) {
805 return $install;
806 }
807
808 // activate plugin
809 if ( $install === true && $active ) {
810 $active = activate_plugin( $upgrader->plugin_info(), '', false, true );
811
812 if ( is_wp_error( $active ) ) {
813 return $active;
814 }
815
816 return $active === null;
817 }
818
819 return $install;
820 }
821
822 public function get_remote_plugin_data( $slug = '' ) {
823 if ( empty( $slug ) ) {
824 return new \WP_Error( 'empty_arg', __( 'Argument should not be empty.', 'ablocks' ) );
825 }
826
827 $response = wp_remote_post(
828 'http://api.wordpress.org/plugins/info/1.0/',
829 array(
830 'body' => array(
831 'action' => 'plugin_information',
832 'request' => maybe_serialize( (object) array(
833 'slug' => $slug,
834 'fields' => array(
835 'version' => false,
836 ),
837 )),
838 ),
839 )
840 );
841
842 if ( is_wp_error( $response ) ) {
843 return $response;
844 }
845
846 return maybe_unserialize( wp_remote_retrieve_body( $response ) );
847 }
848
849 public function download_google_fonts() {
850 global $ablocks_fonts;
851 $fontDownloader = new FontLoadLocally();
852 $fontDownloader->process_font_queue( $ablocks_fonts );
853 }
854 }
855