PluginProbe
Autoptimize / 3.1.5
Autoptimize v3.1.5
2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 All 107 releases
autoptimize / classes / autoptimizeCriticalCSSSettingsAjax.php

autoptimizeCriticalCSSSettingsAjax.php in Autoptimize 3.1.5, at classes/autoptimizeCriticalCSSSettingsAjax.php

550 lines 24.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Critical CSS settings AJAX logic.
4 */
5
6 if ( ! defined( 'ABSPATH' ) ) {
7 exit;
8 }
9
10 class autoptimizeCriticalCSSSettingsAjax {
11 public function __construct() {
12 $this->criticalcss = autoptimize()->criticalcss();
13 $this->run();
14 }
15
16 public function run() {
17 // add filters.
18 add_action( 'wp_ajax_fetch_critcss', array( $this, 'critcss_fetch_callback' ) );
19 add_action( 'wp_ajax_save_critcss', array( $this, 'critcss_save_callback' ) );
20 add_action( 'wp_ajax_rm_critcss', array( $this, 'critcss_rm_callback' ) );
21 add_action( 'wp_ajax_rm_critcss_all', array( $this, 'critcss_rm_all_callback' ) );
22 add_action( 'wp_ajax_ao_ccss_export', array( $this, 'ao_ccss_export_callback' ) );
23 add_action( 'wp_ajax_ao_ccss_import', array( $this, 'ao_ccss_import_callback' ) );
24 add_action( 'wp_ajax_ao_ccss_queuerunner', array( $this, 'ao_ccss_queuerunner_callback' ) );
25 add_action( 'wp_ajax_ao_ccss_saverules', array( $this, 'ao_ccss_saverules_callback' ) );
26 }
27
28 public function critcss_fetch_callback() {
29 // Ajax handler to obtain a critical CSS file from the filesystem.
30 // Check referer.
31 check_ajax_referer( 'fetch_critcss_nonce', 'critcss_fetch_nonce' );
32
33 // Initialize error flag.
34 $error = true;
35
36 // Allow no content for MANUAL rules (as they may not exist just yet).
37 if ( current_user_can( 'manage_options' ) && empty( $_POST['critcssfile'] ) ) {
38 $content = '';
39 $error = false;
40 } elseif ( current_user_can( 'manage_options' ) && $this->critcss_check_filename( $_POST['critcssfile'] ) ) {
41 // Or check user permissios and filename.
42 // Set file path and obtain its content.
43 $critcssfile = AO_CCSS_DIR . strip_tags( $_POST['critcssfile'] );
44 if ( file_exists( $critcssfile ) ) {
45 $content = file_get_contents( $critcssfile );
46 $error = false;
47 }
48 }
49
50 // Prepare response.
51 if ( $error ) {
52 $response['code'] = '500';
53 $response['string'] = 'Error reading file ' . $critcssfile . '.';
54 } else {
55 $response['code'] = '200';
56 $response['string'] = $content;
57 }
58
59 // Dispatch respose.
60 echo json_encode( $response );
61
62 // Close ajax request.
63 wp_die();
64 }
65
66 public function critcss_save_callback() {
67 $error = false;
68 $status = false;
69 $response = array();
70
71 // Ajax handler to write a critical CSS to the filesystem
72 // Check referer.
73 check_ajax_referer( 'save_critcss_nonce', 'critcss_save_nonce' );
74
75 // Allow empty contents for MANUAL rules (as they are fetched later).
76 if ( current_user_can( 'manage_options' ) && empty( $_POST['critcssfile'] ) ) {
77 $critcssfile = false;
78 $status = true;
79 } elseif ( current_user_can( 'manage_options' ) && $this->critcss_check_filename( $_POST['critcssfile'] ) ) {
80 // Or check user permissios and filename
81 // Set critical CSS content.
82 $critcsscontents = stripslashes( $_POST['critcsscontents'] );
83
84 // If there is content and it's valid, write the file.
85 if ( $critcsscontents && $this->criticalcss->check_contents( $critcsscontents ) ) {
86 // Set file path and status.
87 $critcssfile = AO_CCSS_DIR . strip_tags( $_POST['critcssfile'] );
88 $status = file_put_contents( $critcssfile, $critcsscontents, LOCK_EX );
89 // Or set as error.
90 } else {
91 $error = true;
92 $critcssfile = 'CCSS content not acceptable.';
93 }
94 // Or just set an error.
95 } else {
96 $error = true;
97 $critcssfile = 'Not allowed or problem with CCSS filename.';
98 }
99
100 // Prepare response.
101 if ( ! $status || $error ) {
102 $response['code'] = '500';
103 $response['string'] = 'Error saving file ' . $critcssfile . '.';
104 } else {
105 $response['code'] = '200';
106 if ( $critcssfile ) {
107 $response['string'] = 'File ' . $critcssfile . ' saved.';
108 } else {
109 $response['string'] = 'Empty content does not need to be saved.';
110 }
111 }
112
113 // Dispatch respose.
114 echo json_encode( $response );
115
116 // Close ajax request.
117 wp_die();
118 }
119
120 public function critcss_rm_callback() {
121 // Ajax handler to delete a critical CSS from the filesystem
122 // Check referer.
123 check_ajax_referer( 'rm_critcss_nonce', 'critcss_rm_nonce' );
124
125 // Initialize error and status flags.
126 $error = true;
127 $status = false;
128
129 // Allow no file for MANUAL rules (as they may not exist just yet).
130 if ( current_user_can( 'manage_options' ) && empty( $_POST['critcssfile'] ) ) {
131 $error = false;
132 } elseif ( current_user_can( 'manage_options' ) && $this->critcss_check_filename( $_POST['critcssfile'] ) ) {
133 // Or check user permissios and filename
134 // Set file path and delete it.
135 $critcssfile = AO_CCSS_DIR . strip_tags( $_POST['critcssfile'] );
136 if ( file_exists( $critcssfile ) ) {
137 $status = unlink( $critcssfile );
138 $error = false;
139 }
140 }
141
142 // Prepare response.
143 if ( $error ) {
144 $response['code'] = '500';
145 $response['string'] = 'Error removing file ' . $critcssfile . '.';
146 } else {
147 $response['code'] = '200';
148 if ( $status ) {
149 $response['string'] = 'File ' . $critcssfile . ' removed.';
150 } else {
151 $response['string'] = 'No file to be removed.';
152 }
153 }
154
155 // Dispatch respose.
156 echo json_encode( $response );
157
158 // Close ajax request.
159 wp_die();
160 }
161
162 public function critcss_rm_all_callback() {
163 // Ajax handler to delete a critical CSS from the filesystem
164 // Check referer.
165 check_ajax_referer( 'rm_critcss_all_nonce', 'critcss_rm_all_nonce' );
166
167 // Initialize error and status flags.
168 $error = true;
169 $status = false;
170
171 // Remove all ccss files on filesystem.
172 if ( current_user_can( 'manage_options' ) ) {
173 if ( file_exists( AO_CCSS_DIR ) && is_dir( AO_CCSS_DIR ) ) {
174 array_map( 'unlink', glob( AO_CCSS_DIR . 'ccss_*.css', GLOB_BRACE ) );
175 $error = false;
176 $status = true;
177 }
178 }
179
180 // Prepare response.
181 if ( $error ) {
182 $response['code'] = '500';
183 $response['string'] = 'Error removing all critical CSS files.';
184 } else {
185 $response['code'] = '200';
186 if ( $status ) {
187 $response['string'] = 'Critical CSS Files removed.';
188 } else {
189 $response['string'] = 'No file removed.';
190 }
191 }
192
193 // Dispatch respose.
194 echo json_encode( $response );
195
196 // Close ajax request.
197 wp_die();
198 }
199
200 public function ao_ccss_export_callback() {
201 // Ajax handler export settings
202 // Check referer.
203 check_ajax_referer( 'ao_ccss_export_nonce', 'ao_ccss_export_nonce' );
204
205 if ( ! class_exists( 'ZipArchive' ) ) {
206 $response['code'] = '500';
207 $response['msg'] = 'PHP ZipArchive not present, cannot create zipfile';
208 echo json_encode( $response );
209 wp_die();
210 }
211
212 // Init array, get options and prepare the raw object.
213 $settings = array();
214
215 // CCSS settings.
216 $settings['ccss']['rules'] = get_option( 'autoptimize_ccss_rules' );
217 $settings['ccss']['additional'] = get_option( 'autoptimize_ccss_additional' );
218 $settings['ccss']['viewport'] = get_option( 'autoptimize_ccss_viewport' );
219 $settings['ccss']['finclude'] = get_option( 'autoptimize_ccss_finclude' );
220 $settings['ccss']['rtimelimit'] = get_option( 'autoptimize_ccss_rtimelimit' );
221 $settings['ccss']['noptimize'] = get_option( 'autoptimize_ccss_noptimize' );
222 $settings['ccss']['debug'] = get_option( 'autoptimize_ccss_debug' );
223 $settings['ccss']['key'] = get_option( 'autoptimize_ccss_key' );
224 $settings['ccss']['deferjquery'] = get_option( 'autoptimize_ccss_deferjquery' );
225 $settings['ccss']['domain'] = get_option( 'autoptimize_ccss_domain' );
226 $settings['ccss']['forcepath'] = get_option( 'autoptimize_ccss_forcepath' );
227 $settings['ccss']['loggedin'] = get_option( 'autoptimize_ccss_loggedin' );
228 $settings['ccss']['rlimit'] = get_option( 'autoptimize_ccss_rlimit' );
229 $settings['ccss']['unloadccss'] = get_option( 'autoptimize_ccss_unloadccss' );
230
231 // JS settings.
232 $settings['js']['root'] = get_option( 'autoptimize_js' );
233 $settings['js']['aggregate'] = get_option( 'autoptimize_js_aggregate' );
234 $settings['js']['defer_not_aggregate'] = get_option( 'autoptimize_js_defer_not_aggregate' );
235 $settings['js']['defer_inline'] = get_option( 'autoptimize_js_defer_inline' );
236 $settings['js']['exclude'] = get_option( 'autoptimize_js_exclude' );
237 $settings['js']['forcehead'] = get_option( 'autoptimize_js_forcehead' );
238 $settings['js']['justhead'] = get_option( 'autoptimize_js_justhead' );
239 $settings['js']['trycatch'] = get_option( 'autoptimize_js_trycatch' );
240 $settings['js']['include_inline'] = get_option( 'autoptimize_js_include_inline' );
241
242 // CSS settings.
243 $settings['css']['root'] = get_option( 'autoptimize_css' );
244 $settings['css']['aggregate'] = get_option( 'autoptimize_css_aggregate' );
245 $settings['css']['datauris'] = get_option( 'autoptimize_css_datauris' );
246 $settings['css']['justhead'] = get_option( 'autoptimize_css_justhead' );
247 $settings['css']['defer'] = get_option( 'autoptimize_css_defer' );
248 $settings['css']['defer_inline'] = get_option( 'autoptimize_css_defer_inline' );
249 $settings['css']['inline'] = get_option( 'autoptimize_css_inline' );
250 $settings['css']['exclude'] = get_option( 'autoptimize_css_exclude' );
251 $settings['css']['include_inline'] = get_option( 'autoptimize_css_include_inline' );
252
253 // Others.
254 $settings['other']['autoptimize_imgopt_settings'] = get_option( 'autoptimize_imgopt_settings' );
255 $settings['other']['autoptimize_extra_settings'] = get_option( 'autoptimize_extra_settings' );
256 $settings['other']['autoptimize_cache_fallback'] = get_option( 'autoptimize_cache_fallback' );
257 $settings['other']['autoptimize_cache_nogzip'] = get_option( 'autoptimize_cache_nogzip' );
258 $settings['other']['autoptimize_cdn_url'] = get_option( 'autoptimize_cdn_url' );
259 $settings['other']['autoptimize_enable_meta_ao_settings'] = get_option( 'autoptimize_enable_meta_ao_settings' );
260 $settings['other']['autoptimize_enable_site_config'] = get_option( 'autoptimize_enable_site_config' );
261 $settings['other']['autoptimize_html'] = get_option( 'autoptimize_html' );
262 $settings['other']['autoptimize_html_keepcomments'] = get_option( 'autoptimize_html_keepcomments' );
263 $settings['other']['autoptimize_minify_excluded'] = get_option( 'autoptimize_minify_excluded' );
264 $settings['other']['autoptimize_optimize_checkout'] = get_option( 'autoptimize_optimize_checkout' );
265 $settings['other']['autoptimize_optimize_logged'] = get_option( 'autoptimize_optimize_logged' );
266
267 if ( defined( 'AO_PRO_VERSION' ) ) {
268 $settings['pro']['boosters'] = get_option( 'autoptimize_pro_boosters' );
269 $settings['pro']['pagecache'] = get_option( 'autoptimize_pro_pagecache' );
270 }
271
272 // Initialize error flag.
273 $error = true;
274
275 // Check user permissions.
276 if ( current_user_can( 'manage_options' ) ) {
277 // Prepare settings file path and content.
278 $exportfile = AO_CCSS_DIR . 'settings.json';
279 $contents = json_encode( $settings );
280 $status = file_put_contents( $exportfile, $contents, LOCK_EX );
281 $error = false;
282 }
283
284 // Prepare archive.
285 $zipfile = AO_CCSS_DIR . str_replace( array( '.', '/' ), '_', parse_url( AUTOPTIMIZE_WP_SITE_URL, PHP_URL_HOST ) ) . '_' . date( 'Ymd-H\hi' ) . '_ao_ccss_settings.zip'; // @codingStandardsIgnoreLine
286 $file = pathinfo( $zipfile, PATHINFO_BASENAME );
287 $zip = new ZipArchive();
288 $ret = $zip->open( $zipfile, ZipArchive::CREATE );
289 if ( true !== $ret ) {
290 $error = true;
291 } else {
292 $zip->addFile( AO_CCSS_DIR . 'settings.json', 'settings.json' );
293 if ( file_exists( AO_CCSS_DIR . 'queue.json' ) ) {
294 $zip->addFile( AO_CCSS_DIR . 'queue.json', 'queue.json' );
295 }
296 $options = array(
297 'add_path' => './',
298 'remove_all_path' => true,
299 );
300 $zip->addGlob( AO_CCSS_DIR . '*.css', 0, $options );
301 $zip->close();
302 }
303
304 // settings.json has been added to zipfile, so can be removed now.
305 if ( file_exists( $exportfile ) ) {
306 unlink( $exportfile );
307 }
308
309 // Prepare response.
310 if ( ! $status || $error ) {
311 $response['code'] = '500';
312 $response['msg'] = 'Error saving file ' . $file . ', code: ' . $ret;
313 } else {
314 $response['code'] = '200';
315 $response['msg'] = 'File ' . $file . ' saved.';
316 $response['file'] = $file;
317 }
318
319 // Dispatch respose.
320 echo json_encode( $response );
321
322 // Close ajax request.
323 wp_die();
324 }
325
326 public function ao_ccss_import_callback() {
327 // Ajax handler import settings
328 // Check referer.
329 check_ajax_referer( 'ao_ccss_import_nonce', 'ao_ccss_import_nonce' );
330
331 // Initialize error flag.
332 $error = false;
333
334 // Process an uploaded file with no errors.
335 if ( current_user_can( 'manage_options' ) && ! $_FILES['file']['error'] && $_FILES['file']['size'] < 500001 && strpos( $_FILES['file']['name'], '.zip' ) === strlen( $_FILES['file']['name'] ) - 4 ) {
336 // create tmp dir with hard guess name in AO_CCSS_DIR.
337 $_secret_dir = wp_hash( uniqid( md5( AUTOPTIMIZE_CACHE_URL ), true ) );
338 $_import_tmp_dir = trailingslashit( AO_CCSS_DIR . $_secret_dir );
339 mkdir( $_import_tmp_dir, 0774, true );
340
341 // Save file to that tmp directory but give it our own name to prevent directory traversal risks when using original name.
342 $zipfile = $_import_tmp_dir . uniqid( 'import_settings-', true ) . '.zip';
343 move_uploaded_file( $_FILES['file']['tmp_name'], $zipfile );
344
345 // Extract archive in the tmp directory.
346 $zip = new ZipArchive;
347 if ( $zip->open( $zipfile ) === true ) {
348 // loop through all files in the zipfile.
349 for ( $i = 0; $i < $zip->numFiles; $i++ ) { // @codingStandardsIgnoreLine
350 // but only extract known good files.
351 if ( preg_match( '/^settings\.json$|^\.\/ccss_[a-z0-9]{32}\.css$/', $zip->getNameIndex( $i ) ) > 0 ) {
352 $zip->extractTo( AO_CCSS_DIR, $zip->getNameIndex( $i ) );
353 }
354 }
355 $zip->close();
356 } else {
357 $error = 'could not extract';
358 }
359
360 // and remove temp. dir with all contents (the import-zipfile).
361 $this->rrmdir( $_import_tmp_dir );
362
363 if ( ! $error ) {
364 // Archive extraction ok, continue importing settings from AO_CCSS_DIR.
365 // Settings file.
366 $importfile = AO_CCSS_DIR . 'settings.json';
367
368 if ( file_exists( $importfile ) ) {
369 // Get settings and turn them into an object.
370 $settings = json_decode( file_get_contents( $importfile ), true );
371
372 // Update options from settings, but only for known options.
373 // CCSS.
374 foreach ( array( 'rules', 'additional', 'viewport', 'finclude', 'rtimelimit', 'noptimize', 'debug', 'key', 'deferjquery', 'domain', 'forcepath', 'loggedin', 'rlimit', 'unloadccss' ) as $ccss_setting ) {
375 if ( false === array_key_exists( 'ccss', $settings ) || false === array_key_exists( $ccss_setting, $settings['ccss'] ) ) {
376 continue;
377 } else {
378 update_option( 'autoptimize_ccss_' . $ccss_setting, $settings['ccss'][ $ccss_setting ] );
379 }
380 }
381
382 // JS.
383 foreach ( array( 'root', 'aggregate', 'defer_not_aggregate', 'defer_inline', 'exclude', 'forcehead', 'trycatch', 'include_inline' ) as $js_setting ) {
384 if ( false === array_key_exists( 'js', $settings ) || false === array_key_exists( $js_setting, $settings['js'] ) ) {
385 continue;
386 } else if ( 'root' === $js_setting ) {
387 update_option( 'autoptimize_js', $settings['js']['root'] );
388 } else {
389 update_option( 'autoptimize_js_' . $js_setting, $settings['js'][ $js_setting ] );
390 }
391 }
392
393 // CSS.
394 foreach ( array( 'root', 'aggregate', 'datauris', 'justhead', 'defer', 'defer_inline', 'inline', 'exclude', 'include_inline' ) as $css_setting ) {
395 if ( false === array_key_exists( 'css', $settings ) || false === array_key_exists( $css_setting, $settings['css'] ) ) {
396 continue;
397 } else if ( 'root' === $css_setting ) {
398 update_option( 'autoptimize_css', $settings['css']['root'] );
399 } else {
400 update_option( 'autoptimize_css_' . $css_setting, $settings['css'][ $css_setting ] );
401 }
402 }
403
404 // Other.
405 foreach ( array( 'autoptimize_imgopt_settings', 'autoptimize_extra_settings', 'autoptimize_cache_fallback', 'autoptimize_cache_nogzip', 'autoptimize_cdn_url', 'autoptimize_enable_meta_ao_settings', 'autoptimize_enable_site_config', 'autoptimize_html', 'autoptimize_html_keepcomments', 'autoptimize_minify_excluded', 'autoptimize_optimize_checkout', 'autoptimize_optimize_logged' ) as $other_setting ) {
406 if ( false === array_key_exists( 'other', $settings ) || false === array_key_exists( $other_setting, $settings['other'] ) ) {
407 continue;
408 } else {
409 update_option( $other_setting, $settings['other'][ $other_setting ] );
410 }
411 }
412
413 // AO Pro.
414 if ( defined( 'AO_PRO_VERSION' ) && array_key_exists( 'pro', $settings ) ) {
415 update_option( 'autoptimize_pro_boosters', $settings['pro']['boosters'] );
416 update_option( 'autoptimize_pro_pagecache', $settings['pro']['pagecache'] );
417 }
418
419 // settings.json has been imported, so can be removed now.
420 if ( file_exists( $importfile ) ) {
421 unlink( $importfile );
422 }
423 } else {
424 // Settings file doesn't exist, update error flag.
425 $error = 'settings file does not exist';
426 }
427 }
428 } else {
429 $error = 'file could not be saved';
430 }
431
432 // Prepare response.
433 if ( $error ) {
434 $response['code'] = '500';
435 $response['msg'] = 'Error importing settings: ' . $error;
436 } else {
437 $response['code'] = '200';
438 $response['msg'] = 'Settings imported successfully';
439 }
440
441 // Dispatch respose.
442 echo json_encode( $response );
443
444 // Close ajax request.
445 wp_die();
446 }
447
448 public function ao_ccss_queuerunner_callback() {
449 check_ajax_referer( 'ao_ccss_queuerunner_nonce', 'ao_ccss_queuerunner_nonce' );
450
451 // Process an uploaded file with no errors.
452 if ( current_user_can( 'manage_options' ) ) {
453 if ( ! file_exists( AO_CCSS_LOCK ) ) {
454 $ccss_cron = new autoptimizeCriticalCSSCron();
455 $ccss_cron->ao_ccss_queue_control();
456 $response['code'] = '200';
457 $response['msg'] = 'Queue processing done';
458 } else {
459 $response['code'] = '302';
460 $response['msg'] = 'Lock file found';
461 }
462 } else {
463 $response['code'] = '500';
464 $response['msg'] = 'Not allowed';
465 }
466
467 // Dispatch respose.
468 echo json_encode( $response );
469
470 // Close ajax request.
471 wp_die();
472 }
473
474 public function ao_ccss_saverules_callback() {
475 check_ajax_referer( 'ao_ccss_saverules_nonce', 'ao_ccss_saverules_nonce' );
476
477 // save rules over AJAX, too many users forget to press "save changes".
478 if ( current_user_can( 'manage_options' ) ) {
479 if ( array_key_exists( 'critcssrules', $_POST ) ) {
480 $rules = stripslashes( $_POST['critcssrules'] ); // ugly, but seems correct as per https://developer.wordpress.org/reference/functions/stripslashes_deep/#comment-1045 .
481 if ( ! empty( $rules ) ) {
482 $_unsafe_rules_array = json_decode( wp_strip_all_tags( $rules ), true );
483 if ( ! empty( $_unsafe_rules_array ) && is_array( $_unsafe_rules_array ) ) {
484 $_safe_rules_array = array();
485 if ( array_key_exists( 'paths', $_unsafe_rules_array ) ) {
486 $_safe_rules_array['paths'] = $_unsafe_rules_array['paths'];
487 }
488 if ( array_key_exists( 'types', $_unsafe_rules_array ) ) {
489 $_safe_rules_array['types'] = $_unsafe_rules_array['types'];
490 }
491 $_safe_rules = json_encode( $_safe_rules_array, JSON_FORCE_OBJECT );
492 if ( ! empty( $_safe_rules ) ) {
493 update_option( 'autoptimize_ccss_rules', $_safe_rules );
494 $response['code'] = '200';
495 $response['msg'] = 'Rules saved';
496 } else {
497 $_error = 'Could not auto-save rules (safe rules empty)';
498 }
499 } else {
500 $_error = 'Could not auto-save rules (rules could not be json_decoded)';
501 }
502 } else {
503 $_error = 'Could not auto-save rules (rules empty)';
504 }
505 } else {
506 $_error = 'Could not auto-save rules (rules not in $_POST)';
507 }
508 } else {
509 $_error = 'Not allowed';
510 }
511
512 if ( ! isset( $response ) && $_error ) {
513 $response['code'] = '500';
514 $response['msg'] = $_error;
515 }
516
517 // Dispatch respose.
518 echo json_encode( $response );
519
520 // Close ajax request.
521 wp_die();
522 }
523
524 public function critcss_check_filename( $filename ) {
525 // Try to avoid directory traversal when reading/writing/deleting critical CSS files.
526 if ( strpos( $filename, 'ccss_' ) !== 0 ) {
527 return false;
528 } elseif ( substr( $filename, -4, 4 ) !== '.css' ) {
529 return false;
530 } elseif ( sanitize_file_name( $filename ) !== $filename ) {
531 // Use WordPress core's sanitize_file_name to see if anything fishy is going on.
532 return false;
533 } else {
534 return true;
535 }
536 }
537
538 public function rrmdir( $path ) {
539 // recursively remove a directory as found on
540 // https://andy-carter.com/blog/recursively-remove-a-directory-in-php.
541 $files = glob( $path . '/*' );
542 foreach ( $files as $file ) {
543 is_dir( $file ) ? $this->rrmdir( $file ) : unlink( $file );
544 }
545 rmdir( $path );
546
547 return;
548 }
549 }
550