PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.11.1
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.11.1
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.11.1, at includes/ajax/dashboard.php

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