PluginProbe ʕ •ᴥ•ʔ
Code Manager / 1.0.31
Code Manager v1.0.31
1.0.48 1.0.47 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.2 1.0.20 1.0.21 1.0.22 1.0.23 1.0.24 1.0.25 1.0.26 1.0.27 1.0.28 1.0.3 1.0.30 1.0.31 1.0.32 1.0.33 1.0.34 1.0.35 1.0.36 1.0.37 1.0.38 1.0.39 1.0.4 1.0.40 1.0.41 1.0.42 1.0.43 1.0.44 1.0.45 1.0.46 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9
code-manager / freemius / includes / class-fs-garbage-collector.php
code-manager / freemius / includes Last commit date
customizer 2 years ago debug 2 years ago entities 2 years ago managers 2 years ago sdk 2 years ago supplements 2 years ago class-freemius-abstract.php 2 years ago class-freemius.php 2 years ago class-fs-admin-notices.php 2 years ago class-fs-api.php 2 years ago class-fs-garbage-collector.php 2 years ago class-fs-lock.php 2 years ago class-fs-logger.php 2 years ago class-fs-options.php 2 years ago class-fs-plugin-updater.php 2 years ago class-fs-security.php 2 years ago class-fs-storage.php 2 years ago class-fs-user-lock.php 2 years ago fs-core-functions.php 2 years ago fs-essential-functions.php 2 years ago fs-html-escaping-functions.php 2 years ago fs-plugin-info-dialog.php 2 years ago index.php 2 years ago l10n.php 2 years ago
class-fs-garbage-collector.php
411 lines
1 <?php
2 /**
3 * @package Freemius
4 * @copyright Copyright (c) 2015, Freemius, Inc.
5 * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
6 * @since 2.6.0
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 interface FS_I_Garbage_Collector {
14 function clean();
15 }
16
17 class FS_Product_Garbage_Collector implements FS_I_Garbage_Collector {
18 /**
19 * @var FS_Options
20 */
21 private $_accounts;
22
23 /**
24 * @var string[]
25 */
26 private $_options_names;
27
28 /**
29 * @var string
30 */
31 private $_type;
32
33 /**
34 * @var string
35 */
36 private $_plural_type;
37
38 /**
39 * @var array<string, int> Map of product slugs to their last load timestamp, only for products that are not active.
40 */
41 private $_gc_timestamp;
42
43 /**
44 * @var array<string, array<string, mixed>> Map of product slugs to their data, as stored by the primary storage of `Freemius` class.
45 */
46 private $_storage_data;
47
48 function __construct( FS_Options $_accounts, $option_names, $type ) {
49 $this->_accounts = $_accounts;
50 $this->_options_names = $option_names;
51 $this->_type = $type;
52 $this->_plural_type = ( $type . 's' );
53 }
54
55 function clean() {
56 $this->_gc_timestamp = $this->_accounts->get_option( 'gc_timestamp', array() );
57 $this->_storage_data = $this->_accounts->get_option( $this->_type . '_data', array() );
58
59 $options = $this->load_options();
60 $has_updated_option = false;
61
62 $products_to_clean = $this->get_products_to_clean();
63
64 foreach( $products_to_clean as $product ) {
65 $slug = $product->slug;
66
67 // Clear the product's data.
68 foreach( $options as $option_name => $option ) {
69 $updated = false;
70
71 /**
72 * We expect to deal with only array like options here.
73 * @todo - Refactor this to create dedicated GC classes for every option, then we can make the code mode predictable.
74 * For example, depending on data integrity of `plugins` we can still miss something entirely in the `plugin_data` or vice-versa.
75 * A better algorithm is to iterate over all options individually in separate classes and check against primary storage to see if those can be garbage collected.
76 * But given the chance of data integrity issue is very low, we let this run for now and gather feedback.
77 */
78 if ( ! is_array( $option ) ) {
79 continue;
80 }
81
82 if ( array_key_exists( $slug, $option ) ) {
83 unset( $option[ $slug ] );
84 $updated = true;
85 } else if ( array_key_exists( "{$slug}:{$this->_type}", $option ) ) { /* admin_notices */
86 unset( $option[ "{$slug}:{$this->_type}" ] );
87 $updated = true;
88 } else if ( isset( $product->id ) && array_key_exists( $product->id, $option ) ) { /* all_licenses */
89 unset( $option[ $product->id ] );
90 $updated = true;
91 } else if ( isset( $product->file ) && array_key_exists( $product->file, $option ) ) { /* file_slug_map */
92 unset( $option[ $product->file ] );
93 $updated = true;
94 }
95
96 if ( $updated ) {
97 $this->_accounts->set_option( $option_name, $option );
98
99 $options[ $option_name ] = $option;
100
101 $has_updated_option = true;
102 }
103 }
104
105 // Clear the product's data from the primary storage.
106 if ( isset( $this->_storage_data[ $slug ] ) ) {
107 unset( $this->_storage_data[ $slug ] );
108 $has_updated_option = true;
109 }
110
111 // Clear from GC timestamp.
112 // @todo - This perhaps needs a separate garbage collector for all expired products. But the chance of left-over is very slim.
113 if ( isset( $this->_gc_timestamp[ $slug ] ) ) {
114 unset( $this->_gc_timestamp[ $slug ] );
115 $has_updated_option = true;
116 }
117 }
118
119 $this->_accounts->set_option( 'gc_timestamp', $this->_gc_timestamp );
120 $this->_accounts->set_option( $this->_type . '_data', $this->_storage_data );
121
122 return $has_updated_option;
123 }
124
125 private function get_all_option_names() {
126 return array_merge(
127 array(
128 'admin_notices',
129 'updates',
130 'all_licenses',
131 'addons',
132 'id_slug_type_path_map',
133 'file_slug_map',
134 ),
135 $this->_options_names
136 );
137 }
138
139 private function get_products() {
140 $products = $this->_accounts->get_option( $this->_plural_type, array() );
141
142 // Fill any missing product found in the primary storage.
143 // @todo - This wouldn't be needed if we use dedicated GC design for every options. The options themselves would provide such information.
144 foreach( $this->_storage_data as $slug => $product_data ) {
145 if ( ! isset( $products[ $slug ] ) ) {
146 $products[ $slug ] = (object) $product_data;
147 }
148 }
149
150 $this->update_gc_timestamp( $products );
151
152 return $products;
153 }
154
155 private function get_products_to_clean() {
156 $products_to_clean = array();
157
158 $products = $this->get_products();
159
160 foreach ( $products as $slug => $product_data ) {
161 if ( ! is_object( $product_data ) ) {
162 continue;
163 }
164
165 if ( $this->is_product_active( $slug ) ) {
166 continue;
167 }
168
169 $is_addon = ( ! empty( $product_data->parent_plugin_id ) );
170
171 if ( ! $is_addon ) {
172 $products_to_clean[] = $product_data;
173 } else {
174 /**
175 * If add-on, add to the beginning of the array so that add-ons are removed before their parent. This is to prevent an unexpected issue when an add-on exists but its parent was already removed.
176 */
177 array_unshift( $products_to_clean, $product_data );
178 }
179 }
180
181 return $products_to_clean;
182 }
183
184 /**
185 * @param string $slug
186 *
187 * @return bool
188 */
189 private function is_product_active( $slug ) {
190 $instances = Freemius::_get_all_instances();
191
192 foreach ( $instances as $instance ) {
193 if ( $instance->get_slug() === $slug ) {
194 return true;
195 }
196 }
197
198 $expiration_time = fs_get_optional_constant( 'WP_FS__GARBAGE_COLLECTOR_EXPIRATION_TIME_SECS', ( WP_FS__TIME_WEEK_IN_SEC * 4 ) );
199
200 if ( $this->get_last_load_timestamp( $slug ) > ( time() - $expiration_time ) ) {
201 // Last activation was within the last 4 weeks.
202 return true;
203 }
204
205 return false;
206 }
207
208 private function load_options() {
209 $options = array();
210 $option_names = $this->get_all_option_names();
211
212 foreach ( $option_names as $option_name ) {
213 $options[ $option_name ] = $this->_accounts->get_option( $option_name, array() );
214 }
215
216 return $options;
217 }
218
219 /**
220 * Updates the garbage collector timestamp, only if it was not already set by the product's primary storage.
221 *
222 * @param array $products
223 *
224 * @return void
225 */
226 private function update_gc_timestamp( $products ) {
227 foreach ($products as $slug => $product_data) {
228 if ( ! is_object( $product_data ) && ! is_array( $product_data ) ) {
229 continue;
230 }
231
232
233 // If the product is active, we don't need to update the gc_timestamp.
234 if ( isset( $this->_storage_data[ $slug ]['last_load_timestamp'] ) ) {
235 continue;
236 }
237
238 // First try to check if the product is present in the primary storage. If so update that.
239 if ( isset( $this->_storage_data[ $slug ] ) ) {
240 $this->_storage_data[ $slug ]['last_load_timestamp'] = time();
241 } else if ( ! isset( $this->_gc_timestamp[ $slug ] ) ) {
242 // If not, fallback to the gc_timestamp, but we don't want to update it more than once.
243 $this->_gc_timestamp[ $slug ] = time();
244 }
245 }
246 }
247
248 private function get_last_load_timestamp( $slug ) {
249 if ( isset( $this->_storage_data[ $slug ]['last_load_timestamp'] ) ) {
250 return $this->_storage_data[ $slug ]['last_load_timestamp'];
251 }
252
253 return isset( $this->_gc_timestamp[ $slug ] ) ?
254 $this->_gc_timestamp[ $slug ] :
255 // This should never happen, but if it does, let's assume the product is not expired.
256 time();
257 }
258 }
259
260 class FS_User_Garbage_Collector implements FS_I_Garbage_Collector {
261 private $_accounts;
262
263 private $_types;
264
265 function __construct( FS_Options $_accounts, array $types ) {
266 $this->_accounts = $_accounts;
267 $this->_types = $types;
268 }
269
270 function clean() {
271 $users = Freemius::get_all_users();
272
273 $user_has_install_map = $this->get_user_has_install_map();
274
275 if ( count( $users ) === count( $user_has_install_map ) ) {
276 return false;
277 }
278
279 $products_user_id_license_ids_map = $this->_accounts->get_option( 'user_id_license_ids_map', array() );
280
281 $has_updated_option = false;
282
283 foreach ( $users as $user_id => $user ) {
284 if ( ! isset( $user_has_install_map[ $user_id ] ) ) {
285 unset( $users[ $user_id ] );
286
287 foreach( $products_user_id_license_ids_map as $product_id => $user_id_license_ids_map ) {
288 unset( $user_id_license_ids_map[ $user_id ] );
289
290 if ( empty( $user_id_license_ids_map ) ) {
291 unset( $products_user_id_license_ids_map[ $product_id ] );
292 } else {
293 $products_user_id_license_ids_map[ $product_id ] = $user_id_license_ids_map;
294 }
295 }
296
297 $this->_accounts->set_option( 'users', $users );
298 $this->_accounts->set_option( 'user_id_license_ids_map', $products_user_id_license_ids_map );
299
300 $has_updated_option = true;
301 }
302 }
303
304 return $has_updated_option;
305 }
306
307 private function get_user_has_install_map() {
308 $user_has_install_map = array();
309
310 foreach ( $this->_types as $product_type ) {
311 $option_name = ( WP_FS__MODULE_TYPE_PLUGIN !== $product_type ) ?
312 "{$product_type}_sites" :
313 'sites';
314
315 $installs = $this->_accounts->get_option( $option_name, array() );
316
317 foreach ( $installs as $install ) {
318 $user_has_install_map[ $install->user_id ] = true;
319 }
320 }
321
322 return $user_has_install_map;
323 }
324 }
325
326 // Main entry-level class.
327 class FS_Garbage_Collector implements FS_I_Garbage_Collector {
328 /**
329 * @var FS_Garbage_Collector
330 * @since 2.6.0
331 */
332 private static $_instance;
333
334 /**
335 * @return FS_Garbage_Collector
336 */
337 static function instance() {
338 if ( ! isset( self::$_instance ) ) {
339 self::$_instance = new self();
340 }
341
342 return self::$_instance;
343 }
344
345 #endregion
346
347 private function __construct() {
348 }
349
350 function clean() {
351 $_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
352
353 $products_cleaners = $this->get_product_cleaners( $_accounts );
354
355 $has_cleaned = false;
356
357 foreach ( $products_cleaners as $products_cleaner ) {
358 if ( $products_cleaner->clean() ) {
359 $has_cleaned = true;
360 }
361 }
362
363 if ( $has_cleaned ) {
364 $user_cleaner = new FS_User_Garbage_Collector(
365 $_accounts,
366 array_keys( $products_cleaners )
367 );
368
369 $user_cleaner->clean();
370 }
371
372 // @todo - We need a garbage collector for `all_plugins` and `active_plugins` (and variants of themes).
373
374 // Always store regardless of whether there were cleaned products or not since during the process, the logic may set the last load timestamp of some products.
375 $_accounts->store();
376 }
377
378 /**
379 * @param FS_Options $_accounts
380 *
381 * @return FS_I_Garbage_Collector[]
382 */
383 private function get_product_cleaners( FS_Options $_accounts ) {
384 /**
385 * @var FS_I_Garbage_Collector[] $products_cleaners
386 */
387 $products_cleaners = array();
388
389 $products_cleaners[ WP_FS__MODULE_TYPE_PLUGIN ] = new FS_Product_Garbage_Collector(
390 $_accounts,
391 array(
392 'sites',
393 'plans',
394 'plugins',
395 ),
396 WP_FS__MODULE_TYPE_PLUGIN
397 );
398
399 $products_cleaners[ WP_FS__MODULE_TYPE_THEME ] = new FS_Product_Garbage_Collector(
400 $_accounts,
401 array(
402 'theme_sites',
403 'theme_plans',
404 'themes',
405 ),
406 WP_FS__MODULE_TYPE_THEME
407 );
408
409 return $products_cleaners;
410 }
411 }