PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / 3.2.2
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits v3.2.2
3.2.2 3.2.3 3.2.1 3.2.0 3.1.9 3.1.8 3.1.7 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.9 trunk 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.3 1.1.4 1.1.5 All 174 releases
master-addons / inc / classes / local-cache-cleanup.php

local-cache-cleanup.php in Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits 3.2.2, at inc/classes/local-cache-cleanup.php

315 lines 10.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace MasterAddons\Inc\Classes;
4
5 if (!defined('ABSPATH')) {
6 exit;
7 }
8
9 // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_rmdir, WordPress.WP.AlternativeFunctions.file_system_operations_unlink -- Removing our own cache tree under uploads; WP_Filesystem credential prompts are not acceptable during a background upgrade routine.
10
11 /**
12 * Removes the local template-library mirror from uploads/master_addons.
13 *
14 * Client sites read the library from the remote API now, so the mirror is dead
15 * weight — on small hosting plans it was filling the disk and failing writes.
16 * This runs once per site, in the background, and only for the directories that
17 * mirrored remote data.
18 *
19 * IT MUST NOT RUN ON el.master-addons.com. That site is the SOURCE of the
20 * library: the same directories there hold published kits, purchased-kit
21 * payloads and generated zips. Three independent guards keep it out:
22 *
23 * 1. the master-addons-site-importer plugin (which only exists on the
24 * library server) sets jltma_remove_local_template_cache to false;
25 * 2. JLTMA_IMPORTER being defined is treated as the same signal, so the
26 * guard holds even if the filter is registered late;
27 * 3. any site can opt out with
28 * add_filter('jltma_remove_local_template_cache', '__return_false').
29 */
30 class Local_Cache_Cleanup
31 {
32 const DONE_OPTION = 'jltma_local_cache_removed';
33 const EVENT = 'jltma_remove_local_cache';
34
35 /**
36 * Bumped when the cleanup itself changes, so sites that already ran an
37 * older pass run the new one once. Version 2 removed the empty directory
38 * tree the library cache used to recreate on every load; version 3 takes
39 * assets_cache and the master_addons root with it.
40 */
41 const VERSION = 3;
42
43 /**
44 * Everything this plugin used to keep under uploads/master_addons.
45 *
46 * The template and kit directories are mirrors of the remote library and
47 * are refetched from the API on demand. assets_cache holds bundled CSS and
48 * JS for this site's own pages, which is regenerated -- into post meta now
49 * that the directory is not created -- so it goes as well: the plugin
50 * leaves no folders on a client site.
51 */
52 const MIRROR_DIRS = [
53 'templates-library',
54 'templates_kits',
55 'purchased_kits',
56 'templates_kit',
57 'assets_cache',
58 ];
59
60 private static $instance = null;
61
62 public static function get_instance()
63 {
64 if (null === self::$instance) {
65 self::$instance = new self();
66 }
67 return self::$instance;
68 }
69
70 private function __construct()
71 {
72 add_action('admin_init', [$this, 'maybe_schedule']);
73 add_action(self::EVENT, [$this, 'run']);
74 }
75
76 /**
77 * Delete the mirror straight away, without waiting for cron.
78 *
79 * Called from the plugin activation hook: deactivating and reactivating is
80 * what a site owner does when they want the plugin to sort itself out, and
81 * telling them to wait a minute for WP-Cron -- which only fires on the next
82 * page view, and not at all when DISABLE_WP_CRON is set -- is not an
83 * answer. Deleting a few thousand files takes well under a second once the
84 * first pass is done, and on activation there is no page render to hold up.
85 */
86 public static function purge_now()
87 {
88 return self::get_instance()->run();
89 }
90
91 /**
92 * Is this site allowed to delete its local mirror?
93 */
94 public static function allowed()
95 {
96 // The library server ships master-addons-site-importer; its uploads
97 // tree is the real library, not a mirror of one.
98 if (defined('JLTMA_IMPORTER')) {
99 return false;
100 }
101
102 return (bool) apply_filters('jltma_remove_local_template_cache', true);
103 }
104
105 /**
106 * Has this site run the current version of the cleanup?
107 */
108 private static function needs_run()
109 {
110 $done = get_option(self::DONE_OPTION);
111 if (!$done) {
112 return true;
113 }
114 $version = is_array($done) && isset($done['version']) ? (int) $done['version'] : 1;
115 if ($version < self::VERSION) {
116 return true;
117 }
118
119 // A finished run is not a promise the directories stay gone: any code
120 // path that still creates one puts it back, and a one-shot cleanup
121 // would then never look again. Four is_dir() calls per admin request is
122 // cheap enough to keep checking.
123 return self::has_leftovers();
124 }
125
126 /**
127 * Is any mirror directory still on disk?
128 */
129 private static function has_leftovers()
130 {
131 $upload_dir = wp_upload_dir(null, false);
132 if (empty($upload_dir['basedir'])) {
133 return false;
134 }
135
136 $root = trailingslashit($upload_dir['basedir']) . 'master_addons';
137 foreach (self::MIRROR_DIRS as $dir) {
138 if (is_dir($root . '/' . $dir)) {
139 return true;
140 }
141 }
142
143 return false;
144 }
145
146 /**
147 * Queue the cleanup once. Deleting a few thousand cached files is not
148 * something to do inside an admin page load, so it goes to cron.
149 */
150 public function maybe_schedule()
151 {
152 if (!self::allowed() || !self::needs_run()) {
153 return;
154 }
155
156 // Cron is not guaranteed: DISABLE_WP_CRON, a broken loopback request or
157 // a site nobody visits on the front end all leave the event pending
158 // forever, and the mirror with it. If an earlier attempt is already
159 // overdue, stop waiting for it and delete inline.
160 $scheduled = wp_next_scheduled(self::EVENT);
161 if ($scheduled) {
162 if ($scheduled < time() - 5 * MINUTE_IN_SECONDS) {
163 wp_unschedule_event($scheduled, self::EVENT);
164 $this->run();
165 }
166 return;
167 }
168
169 wp_schedule_single_event(time() + MINUTE_IN_SECONDS, self::EVENT);
170 }
171
172 /**
173 * Delete the mirror directories, then mark the site done so this never
174 * runs twice.
175 *
176 * @return array dir => files removed
177 */
178 public function run()
179 {
180 if (!self::allowed()) {
181 return [];
182 }
183
184 $upload_dir = wp_upload_dir(null, false);
185 $basedir = !empty($upload_dir['basedir']) ? $upload_dir['basedir'] : '';
186 $removed = [];
187
188 if ($basedir) {
189 $root = trailingslashit($basedir) . 'master_addons';
190 foreach (self::MIRROR_DIRS as $dir) {
191 $removed[$dir] = $this->delete_tree($root . '/' . $dir);
192 }
193
194 // The mirror directories are gone, but master_addons itself may now
195 // hold nothing but the index.php/.htaccess stubs the cache used to
196 // drop in. A client should not be left with an empty tree, so the
197 // root goes too -- unless a directory this plugin did not create is
198 // still sitting in it.
199 $removed['root'] = $this->remove_if_empty($root) ? 1 : 0;
200 }
201
202 $this->clear_mirror_cron();
203
204 update_option(self::DONE_OPTION, [
205 'time' => time(),
206 'version' => self::VERSION,
207 'removed' => $removed,
208 ], false);
209
210 return $removed;
211 }
212
213 /**
214 * Drop the scheduled events whose only job is to refill the mirror.
215 *
216 * update_templates_cache() and update_template_kits_cache() exist to walk
217 * the remote library and write it to disk. With local caching off they have
218 * nothing to write, so leaving them scheduled just wakes the site up to do
219 * nothing -- and any future path that forgets its gate would refill the
220 * tree from a cron nobody is watching.
221 */
222 private function clear_mirror_cron()
223 {
224 foreach (['jltma_templates_cache_update', 'jltma_template_kits_cache_update'] as $hook) {
225 if (function_exists('wp_unschedule_hook')) {
226 wp_unschedule_hook($hook);
227 continue;
228 }
229 while ($timestamp = wp_next_scheduled($hook)) {
230 wp_unschedule_event($timestamp, $hook);
231 }
232 }
233 }
234
235 /**
236 * Recursively delete a directory. Returns the number of files removed.
237 */
238 private function delete_tree($dir)
239 {
240 if (!is_dir($dir)) {
241 return 0;
242 }
243
244 // Never step outside uploads, whatever the caller passed in.
245 $upload_dir = wp_upload_dir(null, false);
246 $basedir = !empty($upload_dir['basedir']) ? realpath($upload_dir['basedir']) : '';
247 $real = realpath($dir);
248 if (!$basedir || !$real || 0 !== strpos($real, $basedir)) {
249 return 0;
250 }
251
252 $files = 0;
253 $items = new \RecursiveIteratorIterator(
254 new \RecursiveDirectoryIterator($real, \FilesystemIterator::SKIP_DOTS),
255 \RecursiveIteratorIterator::CHILD_FIRST
256 );
257
258 foreach ($items as $item) {
259 if ($item->isDir()) {
260 @rmdir($item->getPathname());
261 } else {
262 if (@unlink($item->getPathname())) {
263 $files++;
264 }
265 }
266 }
267
268 @rmdir($real);
269
270 return $files;
271 }
272
273 /**
274 * Delete a directory that holds nothing of the site's own.
275 *
276 * The placeholder files the cache wrote (index.php, .htaccess) do not count
277 * as content -- they only existed to protect the cache tree that is now
278 * gone. Anything else, a real directory above all, keeps the root alive.
279 *
280 * @return bool whether the directory was removed.
281 */
282 private function remove_if_empty($dir)
283 {
284 if (!is_dir($dir)) {
285 return false;
286 }
287
288 // index.php/.htaccess were written to protect a tree that is gone;
289 // .DS_Store and Thumbs.db are the operating system's litter. None of
290 // them is a reason to keep the folder standing.
291 $stubs = ['index.php', 'index.html', '.htaccess', '.DS_Store', 'Thumbs.db'];
292 $entries = @scandir($dir);
293 if (false === $entries) {
294 return false;
295 }
296
297 $found = [];
298 foreach ($entries as $entry) {
299 if ('.' === $entry || '..' === $entry) {
300 continue;
301 }
302 if (is_dir($dir . '/' . $entry) || !in_array($entry, $stubs, true)) {
303 return false;
304 }
305 $found[] = $dir . '/' . $entry;
306 }
307
308 foreach ($found as $file) {
309 @unlink($file);
310 }
311
312 return (bool) @rmdir($dir);
313 }
314 }
315