PluginProbe
BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites / trunk
BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites vtrunk
4.1.0 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 2.1.2 2.5.0 2.5.3 2.6.1 All 38 releases
blockspare / freemius / includes / managers / class-fs-clone-manager.php

class-fs-clone-manager.php in BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites trunk, at freemius/includes/managers/class-fs-clone-manager.php

1,671 lines 62.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 * @author Leo Fajardo (@leorw)
7 * @since 2.5.0
8 */
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit;
12 }
13
14 /**
15 * Manages the detection of clones and provides the logged-in WordPress user with options for manually resolving them.
16 *
17 * @since 2.5.0
18 *
19 * @property int $clone_identification_timestamp
20 * @property int $temporary_duplicate_mode_selection_timestamp
21 * @property int $temporary_duplicate_notice_shown_timestamp
22 * @property string $request_handler_id
23 * @property int $request_handler_timestamp
24 * @property int $request_handler_retries_count
25 * @property bool $hide_manual_resolution
26 * @property array $new_blog_install_map
27 */
28 class FS_Clone_Manager {
29 /**
30 * @var FS_Option_Manager
31 */
32 private $_storage;
33 /**
34 * @var FS_Option_Manager
35 */
36 private $_network_storage;
37 /**
38 * @var FS_Admin_Notices
39 */
40 private $_notices;
41 /**
42 * @var FS_Logger
43 */
44 protected $_logger;
45
46 /**
47 * @var int 3 minutes
48 */
49 const CLONE_RESOLUTION_MAX_EXECUTION_TIME = 180;
50 /**
51 * @var int
52 */
53 const CLONE_RESOLUTION_MAX_RETRIES = 3;
54 /**
55 * @var int
56 */
57 const TEMPORARY_DUPLICATE_PERIOD = WP_FS__TIME_WEEK_IN_SEC * 2;
58 /**
59 * @var string
60 */
61 const OPTION_NAME = 'clone_resolution';
62 /**
63 * @var string
64 */
65 const OPTION_MANAGER_NAME = 'clone_management';
66 /**
67 * @var string
68 */
69 const OPTION_TEMPORARY_DUPLICATE = 'temporary_duplicate';
70 /**
71 * @var string
72 */
73 const OPTION_LONG_TERM_DUPLICATE = 'long_term_duplicate';
74 /**
75 * @var string
76 */
77 const OPTION_NEW_HOME = 'new_home';
78
79 #--------------------------------------------------------------------------------
80 #region Singleton
81 #--------------------------------------------------------------------------------
82
83 /**
84 * @var FS_Clone_Manager
85 */
86 private static $_instance;
87
88 /**
89 * @return FS_Clone_Manager
90 */
91 static function instance() {
92 if ( ! isset( self::$_instance ) ) {
93 self::$_instance = new self();
94 }
95
96 return self::$_instance;
97 }
98
99 #endregion
100
101 private function __construct() {
102 $this->_storage = FS_Option_Manager::get_manager( WP_FS___OPTION_PREFIX . self::OPTION_MANAGER_NAME, true );
103 $this->_network_storage = FS_Option_Manager::get_manager( WP_FS___OPTION_PREFIX . self::OPTION_MANAGER_NAME, true, true );
104
105 $this->maybe_migrate_options();
106
107 $this->_notices = FS_Admin_Notices::instance( 'global_clone_resolution_notices', '', '', true );
108 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . '_clone_manager', WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
109 }
110
111 /**
112 * Migrate clone resolution options from 2.5.0 array-based structure, to a new flat structure.
113 *
114 * The reason this logic is not in a separate migration script is that we want to be 100% sure data is migrated before any execution of clone logic.
115 *
116 * @todo Delete this one in the future.
117 */
118 private function maybe_migrate_options() {
119 $storages = array(
120 $this->_storage,
121 $this->_network_storage
122 );
123
124 foreach ( $storages as $storage ) {
125 $clone_data = $storage->get_option( self::OPTION_NAME );
126 if ( is_array( $clone_data ) && ! empty( $clone_data ) ) {
127 foreach ( $clone_data as $key => $val ) {
128 if ( ! is_null( $val ) ) {
129 $storage->set_option( $key, $val );
130 }
131 }
132
133 $storage->unset_option( self::OPTION_NAME, true );
134 }
135 }
136 }
137
138 /**
139 * @author Leo Fajardo (@leorw)
140 * @since 2.5.0
141 */
142 function _init() {
143 if ( is_admin() ) {
144 if ( Freemius::is_admin_post() ) {
145 add_action( 'admin_post_fs_clone_resolution', array( $this, '_handle_clone_resolution' ) );
146 }
147
148 if ( Freemius::is_ajax() ) {
149 Freemius::add_ajax_action_static( 'handle_clone_resolution', array( $this, '_clone_resolution_action_ajax_handler' ) );
150 } else {
151 if (
152 empty( $this->get_clone_identification_timestamp() ) &&
153 (
154 ! fs_is_network_admin() ||
155 ! ( $this->is_clone_resolution_options_notice_shown() || $this->is_temporary_duplicate_notice_shown() )
156 )
157 ) {
158 $this->hide_clone_admin_notices();
159 } else if ( ! Freemius::is_cron() && ! Freemius::is_admin_post() ) {
160 $this->try_resolve_clone_automatically();
161 $this->maybe_show_clone_admin_notice();
162
163 add_action( 'admin_footer', array( $this, '_add_clone_resolution_javascript' ) );
164 }
165 }
166 }
167 }
168
169 /**
170 * Retrieves the timestamp that was stored when a clone was identified.
171 *
172 * @return int|null
173 */
174 function get_clone_identification_timestamp() {
175 return $this->get_option( 'clone_identification_timestamp', true );
176 }
177
178 /**
179 * @author Leo Fajardo (@leorw)
180 * @since 2.5.1
181 *
182 * @param string $sdk_last_version
183 */
184 function maybe_update_clone_resolution_support_flag( $sdk_last_version ) {
185 if ( null !== $this->hide_manual_resolution ) {
186 return;
187 }
188
189 $this->hide_manual_resolution = (
190 ! empty( $sdk_last_version ) &&
191 version_compare( $sdk_last_version, '2.5.0', '<' )
192 );
193 }
194
195 /**
196 * Stores the time when a clone was identified.
197 */
198 function store_clone_identification_timestamp() {
199 $this->clone_identification_timestamp = time();
200 }
201
202 /**
203 * Retrieves the timestamp for the temporary duplicate mode's expiration.
204 *
205 * @return int
206 */
207 function get_temporary_duplicate_expiration_timestamp() {
208 $temporary_duplicate_mode_start_timestamp = $this->was_temporary_duplicate_mode_selected() ?
209 $this->temporary_duplicate_mode_selection_timestamp :
210 $this->get_clone_identification_timestamp();
211
212 return ( $temporary_duplicate_mode_start_timestamp + self::TEMPORARY_DUPLICATE_PERIOD );
213 }
214
215 /**
216 * Determines if the SDK should handle clones. The SDK handles clones only up to 3 times with 3 min interval.
217 *
218 * @return bool
219 */
220 private function should_handle_clones() {
221 if ( ! isset( $this->request_handler_timestamp ) ) {
222 return true;
223 }
224
225 if ( $this->request_handler_retries_count >= self::CLONE_RESOLUTION_MAX_RETRIES ) {
226 return false;
227 }
228
229 // Give the logic that handles clones enough time to finish (it is given 3 minutes for now).
230 return ( time() > ( $this->request_handler_timestamp + self::CLONE_RESOLUTION_MAX_EXECUTION_TIME ) );
231 }
232
233 /**
234 * @author Leo Fajardo (@leorw)
235 * @since 2.5.1
236 *
237 * @return bool
238 */
239 function should_hide_manual_resolution() {
240 return ( true === $this->hide_manual_resolution );
241 }
242
243 /**
244 * Executes the clones handler logic if it should be executed, i.e., based on the return value of the should_handle_clones() method.
245 *
246 * @author Leo Fajardo (@leorw)
247 * @since 2.5.0
248 */
249 function maybe_run_clone_resolution() {
250 if ( ! $this->should_handle_clones() ) {
251 return;
252 }
253
254 $this->request_handler_retries_count = isset( $this->request_handler_retries_count ) ?
255 ( $this->request_handler_retries_count + 1 ) :
256 1;
257
258 $this->request_handler_timestamp = time();
259
260 $handler_id = ( rand() . microtime() );
261 $this->request_handler_id = $handler_id;
262
263 // Add cookies to trigger request with the same user access permissions.
264 $cookies = array();
265 foreach ( $_COOKIE as $name => $value ) {
266 $cookies[] = new WP_Http_Cookie( array(
267 'name' => $name,
268 'value' => $value,
269 ) );
270 }
271
272 wp_remote_post(
273 admin_url( 'admin-post.php' ),
274 array(
275 'method' => 'POST',
276 'body' => array(
277 'action' => 'fs_clone_resolution',
278 'handler_id' => $handler_id,
279 ),
280 'timeout' => 0.01,
281 'blocking' => false,
282 'sslverify' => false,
283 'cookies' => $cookies,
284 )
285 );
286 }
287
288 /**
289 * Executes the clones handler logic.
290 *
291 * @author Leo Fajardo (@leorw)
292 * @since 2.5.0
293 */
294 function _handle_clone_resolution() {
295 $handler_id = fs_request_get( 'handler_id' );
296
297 if ( empty( $handler_id ) ) {
298 return;
299 }
300
301 if (
302 ! isset( $this->request_handler_id ) ||
303 $this->request_handler_id !== $handler_id
304 ) {
305 return;
306 }
307
308 if ( ! $this->try_automatic_resolution() ) {
309 $this->clear_temporary_duplicate_notice_shown_timestamp();
310 }
311 }
312
313 #--------------------------------------------------------------------------------
314 #region Automatic Clone Resolution
315 #--------------------------------------------------------------------------------
316
317 /**
318 * @var array All installs cache.
319 */
320 private $all_installs;
321
322 /**
323 * Checks if a given instance's install is a clone of another subsite in the network.
324 *
325 * @author Vova Feldman (@svovaf)
326 *
327 * @return FS_Site
328 */
329 private function find_network_subsite_clone_install( Freemius $instance ) {
330 if ( ! is_multisite() ) {
331 // Not a multi-site network.
332 return null;
333 }
334
335 if ( ! isset( $this->all_installs ) ) {
336 $this->all_installs = FS_DebugManager::get_all_modules_sites();
337 }
338
339 // Check if there's another blog that has the same site.
340 $module_type = $instance->get_module_type();
341 $sites_by_module_type = ! empty( $this->all_installs[ $module_type ] ) ?
342 $this->all_installs[ $module_type ] :
343 array();
344
345 $slug = $instance->get_slug();
346 $sites_by_slug = ! empty( $sites_by_module_type[ $slug ] ) ?
347 $sites_by_module_type[ $slug ] :
348 array();
349
350 $current_blog_id = get_current_blog_id();
351
352 $current_install = $instance->get_site();
353
354 foreach ( $sites_by_slug as $site ) {
355 if (
356 $current_install->id == $site->id &&
357 $current_blog_id != $site->blog_id
358 ) {
359 // Clone is identical to an install on another subsite in the network.
360 return $site;
361 }
362 }
363
364 return null;
365 }
366
367 /**
368 * Tries to find a different install of the context product that is associated with the current URL and loads it.
369 *
370 * @author Leo Fajardo (@leorw)
371 * @since 2.5.0
372 *
373 * @param Freemius $instance
374 * @param string $url
375 *
376 * @return object
377 */
378 private function find_other_install_by_url( Freemius $instance, $url ) {
379 $result = $instance->get_api_user_scope()->get( "/plugins/{$instance->get_id()}/installs.json?url=" . urlencode( $url ) . "&all=true", true );
380
381 $current_install = $instance->get_site();
382
383 if ( $instance->is_api_result_object( $result, 'installs' ) ) {
384 foreach ( $result->installs as $install ) {
385 if ( $install->id == $current_install->id ) {
386 continue;
387 }
388
389 if (
390 $instance->is_only_premium() &&
391 ! FS_Plugin_License::is_valid_id( $install->license_id )
392 ) {
393 continue;
394 }
395
396 // When searching for installs by a URL, the API will first strip any paths and search for any matching installs by the subdomain. Therefore, we need to test if there's a match between the current URL and the install's URL before continuing.
397 if ( $url !== fs_strip_url_protocol( untrailingslashit( $install->url ) ) ) {
398 continue;
399 }
400
401 // Found a different install that is associated with the current URL, load it and replace the current install with it if no updated install is found.
402 return $install;
403 }
404 }
405
406 return null;
407 }
408
409 /**
410 * Delete the current install associated with a given instance and opt-in/activate-license to create a fresh install.
411 *
412 * @author Vova Feldman (@svovaf)
413 * @since 2.5.0
414 *
415 * @param Freemius $instance
416 * @param FS_Plugin_License|null $license
417 *
418 * @return bool TRUE if successfully connected. FALSE if failed and had to restore install from backup.
419 */
420 private function delete_install_and_connect( Freemius $instance, $license = null ) {
421 $user = Freemius::_get_user_by_id( $instance->get_site()->user_id );
422
423 $instance->delete_current_install( true );
424
425 if ( ! is_object( $user ) ) {
426 // Get logged-in WordPress user.
427 $current_user = Freemius::_get_current_wp_user();
428
429 // Find the relevant FS user by email address.
430 $user = Freemius::_get_user_by_email( $current_user->user_email );
431 }
432
433 $license_key = ( is_object( $license ) ? $license->secret_key : false );
434 $license_owner_id = ( is_object( $license ) ? $license->user_id : null );
435
436 if ( is_object( $user ) ) {
437 // When a clone is found, we prefer to use the same user of the original install for the opt-in.
438 $instance->install_with_user( $user, $license_key, false, false );
439 } else {
440 // If no user is found, activate with the license.
441 $instance->opt_in(
442 false,
443 false,
444 false,
445 $license_key,
446 false,
447 false,
448 false,
449 null,
450 array(),
451 true,
452 $license_owner_id
453 );
454 }
455
456 if ( is_object( $instance->get_site() ) ) {
457 // Install successfully created.
458 return true;
459 }
460
461 // Restore from backup.
462 $instance->restore_backup_site();
463
464 return false;
465 }
466
467 /**
468 * Try to resolve the clone situation automatically.
469 *
470 * @param Freemius $instance
471 * @param string $current_url
472 * @param bool $is_localhost
473 * @param bool|null $is_clone_of_network_subsite
474 *
475 * @return bool If managed to automatically resolve the clone.
476 */
477 private function try_resolve_clone_automatically_by_instance(
478 Freemius $instance,
479 $current_url,
480 $is_localhost,
481 $is_clone_of_network_subsite = null
482 ) {
483 // Try to find a different install of the context product that is associated with the current URL.
484 $associated_install = $this->find_other_install_by_url( $instance, $current_url );
485
486 if ( is_object( $associated_install ) ) {
487 // Replace the current install with a different install that is associated with the current URL.
488 $instance->store_site( new FS_Site( clone $associated_install ) );
489 $instance->sync_install( array( 'is_new_site' => true ), true );
490
491 return true;
492 }
493
494 if ( ! $instance->is_premium() ) {
495 // For free products, opt-in with the context user to create new install.
496 return $this->delete_install_and_connect( $instance );
497 }
498
499 $license = $instance->_get_license();
500 $can_activate_license = ( is_object( $license ) && ! $license->is_utilized( $is_localhost ) );
501
502 if ( ! $can_activate_license ) {
503 // License can't be activated, therefore, can't be automatically resolved.
504 return false;
505 }
506
507 if ( ! WP_FS__IS_LOCALHOST_FOR_SERVER && ! $is_localhost ) {
508 $is_clone_of_network_subsite = ( ! is_null( $is_clone_of_network_subsite ) ) ?
509 $is_clone_of_network_subsite :
510 is_object( $this->find_network_subsite_clone_install( $instance ) );
511
512 if ( ! $is_clone_of_network_subsite ) {
513 return false;
514 }
515 }
516
517 // If the site is a clone of another subsite in the network, or a localhost one, try to auto activate the license.
518 return $this->delete_install_and_connect( $instance, $license );
519 }
520
521 /**
522 * @author Leo Fajardo (@leorw)
523 * @since 2.5.0
524 */
525 private function try_resolve_clone_automatically() {
526 $clone_action = $this->get_clone_resolution_action_from_config();
527
528 if ( ! empty( $clone_action ) ) {
529 $this->try_resolve_clone_automatically_by_config( $clone_action );
530 return;
531 }
532
533 $this->try_automatic_resolution();
534 }
535
536 /**
537 * Tries to resolve the clone situation automatically based on the config in the wp-config.php file.
538 *
539 * @author Leo Fajardo (@leorw)
540 * @since 2.5.0
541 *
542 * @param string $clone_action
543 */
544 private function try_resolve_clone_automatically_by_config( $clone_action ) {
545 $fs_instances = array();
546
547 if ( self::OPTION_LONG_TERM_DUPLICATE === $clone_action ) {
548 $instances = Freemius::_get_all_instances();
549
550 foreach ( $instances as $instance ) {
551 if ( ! $instance->is_registered() ) {
552 continue;
553 }
554
555 if ( ! $instance->is_clone() ) {
556 continue;
557 }
558
559 $license = $instance->has_features_enabled_license() ?
560 $instance->_get_license() :
561 null;
562
563 if (
564 is_object( $license ) &&
565 ! $license->is_utilized(
566 ( WP_FS__IS_LOCALHOST_FOR_SERVER || FS_Site::is_localhost_by_address( Freemius::get_unfiltered_site_url() ) )
567 )
568 ) {
569 $fs_instances[] = $instance;
570 }
571 }
572
573 if ( empty( $fs_instances ) ) {
574 return;
575 }
576 }
577
578 $this->resolve_cloned_sites( $clone_action, $fs_instances );
579 }
580
581 /**
582 * @author Leo Fajard (@leorw)
583 * @since 2.5.0
584 *
585 * @return string|null
586 */
587 private function get_clone_resolution_action_from_config() {
588 if ( ! defined( 'FS__RESOLVE_CLONE_AS' ) ) {
589 return null;
590 }
591
592 if ( ! in_array(
593 FS__RESOLVE_CLONE_AS,
594 array(
595 self::OPTION_NEW_HOME,
596 self::OPTION_TEMPORARY_DUPLICATE,
597 self::OPTION_LONG_TERM_DUPLICATE,
598 )
599 ) ) {
600 return null;
601 }
602
603 return FS__RESOLVE_CLONE_AS;
604 }
605
606 /**
607 * Tries to recover the install of a newly created subsite or resolve it if it's a clone.
608 *
609 * @author Leo Fajardo (@leorw)
610 * @since 2.5.0
611 *
612 * @param Freemius $instance
613 */
614 function maybe_resolve_new_subsite_install_automatically( Freemius $instance ) {
615 if ( ! $instance->is_user_in_admin() ) {
616 // Try to recover an install or resolve a clone only when there's a user in admin to prevent doing it prematurely (e.g., the install can get replaced with clone data again).
617 return;
618 }
619
620 if ( ! is_multisite() ) {
621 return;
622 }
623
624 $new_blog_install_map = $this->new_blog_install_map;
625
626 if ( empty( $new_blog_install_map ) || ! is_array( $new_blog_install_map ) ) {
627 return;
628 }
629
630 $is_network_admin = fs_is_network_admin();
631
632 if ( ! $is_network_admin ) {
633 // If not in network admin, handle the current site.
634 $blog_id = get_current_blog_id();
635 } else {
636 // If in network admin, handle only the first site.
637 $blog_ids = array_keys( $new_blog_install_map );
638 $blog_id = $blog_ids[0];
639 }
640
641 if ( ! isset( $new_blog_install_map[ $blog_id ] ) ) {
642 // There's no site to handle.
643 return;
644 }
645
646 $expected_install_id = $new_blog_install_map[ $blog_id ]['install_id'];
647
648 $current_install = $instance->get_install_by_blog_id( $blog_id );
649 $current_install_id = is_object( $current_install ) ?
650 $current_install->id :
651 null;
652
653 if ( $expected_install_id == $current_install_id ) {
654 // Remove the current site's information from the map to prevent handling it again.
655 $this->remove_new_blog_install_info_from_storage( $blog_id );
656
657 return;
658 }
659
660 require_once WP_FS__DIR_INCLUDES . '/class-fs-lock.php';
661
662 $lock = new FS_Lock( self::OPTION_NAME . '_subsite' );
663
664 if ( ! $lock->try_lock(60) ) {
665 return;
666 }
667
668 $instance->switch_to_blog( $blog_id );
669
670 $current_url = untrailingslashit( Freemius::get_unfiltered_site_url( null, true ) );
671 $current_install_url = is_object( $current_install ) ?
672 fs_strip_url_protocol( untrailingslashit( $current_install->url ) ) :
673 null;
674
675 // This can be `false` even if the install is a clone as the URL can be updated as part of the cloning process.
676 $is_clone = ( ! is_null( $current_install_url ) && $current_url !== $current_install_url );
677
678 if ( ! FS_Site::is_valid_id( $expected_install_id ) ) {
679 $expected_install = null;
680 } else {
681 $expected_install = $instance->fetch_install_by_id( $expected_install_id );
682 }
683
684 if ( FS_Api::is_api_result_entity( $expected_install ) ) {
685 // Replace the current install with the expected install.
686 $instance->store_site( new FS_Site( clone $expected_install ) );
687 $instance->sync_install( array( 'is_new_site' => true ), true );
688 } else {
689 $network_subsite_clone_install = null;
690
691 if ( ! $is_clone ) {
692 // It is possible that `$is_clone` is `false` but the install is actually a clone as the following call checks the install ID and not the URL.
693 $network_subsite_clone_install = $this->find_network_subsite_clone_install( $instance );
694 }
695
696 if ( $is_clone || is_object( $network_subsite_clone_install ) ) {
697 // If there's no expected install (or it couldn't be fetched) and the current install is a clone, try to resolve the clone automatically.
698 $is_localhost = FS_Site::is_localhost_by_address( $current_url );
699
700 $resolved = $this->try_resolve_clone_automatically_by_instance( $instance, $current_url, $is_localhost, is_object( $network_subsite_clone_install ) );
701
702 if ( ! $resolved && is_object( $network_subsite_clone_install ) ) {
703 if ( empty( $this->get_clone_identification_timestamp() ) ) {
704 $this->store_clone_identification_timestamp();
705 }
706
707 // Since the clone couldn't be identified based on the URL, replace the stored install with the cloned install so that the manual clone resolution notice will appear.
708 $instance->store_site( clone $network_subsite_clone_install );
709 }
710 }
711 }
712
713 $instance->restore_current_blog();
714
715 // Remove the current site's information from the map to prevent handling it again.
716 $this->remove_new_blog_install_info_from_storage( $blog_id );
717
718 $lock->unlock();
719 }
720
721 /**
722 * If a new install was created after creating a new subsite, its ID is stored in the blog-install map so that it can be recovered in case it's replaced with a clone install (e.g., when the newly created subsite is a clone). The IDs of the clone subsites that were created while not running this version of the SDK or a higher version will also be stored in the said map so that the clone manager can also try to resolve them later on.
723 *
724 * @author Leo Fajardo (@leorw)
725 * @since 2.5.0
726 *
727 * @param int $blog_id
728 * @param FS_Site $site
729 */
730 function store_blog_install_info( $blog_id, $site = null ) {
731 $new_blog_install_map = $this->new_blog_install_map;
732
733 if (
734 empty( $new_blog_install_map ) ||
735 ! is_array( $new_blog_install_map )
736 ) {
737 $new_blog_install_map = array();
738 }
739
740 $install_id = null;
741
742 if ( is_object( $site ) ) {
743 $install_id = $site->id;
744 }
745
746 $new_blog_install_map[ $blog_id ] = array( 'install_id' => $install_id );
747
748 $this->new_blog_install_map = $new_blog_install_map;
749 }
750
751 /**
752 * @author Leo Fajardo (@leorw)
753 * @since 2.5.0
754 *
755 * @param int $blog_id
756 */
757 private function remove_new_blog_install_info_from_storage( $blog_id ) {
758 $new_blog_install_map = $this->new_blog_install_map;
759
760 unset( $new_blog_install_map[ $blog_id ] );
761 $this->new_blog_install_map = $new_blog_install_map;
762 }
763
764 /**
765 * Tries to resolve all clones automatically.
766 *
767 * @author Leo Fajardo (@leorw)
768 * @since 2.5.0
769 *
770 * @return bool If managed to automatically resolve all clones.
771 */
772 private function try_automatic_resolution() {
773 $this->_logger->entrance();
774
775 require_once WP_FS__DIR_INCLUDES . '/class-fs-lock.php';
776
777 $lock = new FS_Lock( self::OPTION_NAME );
778
779 /**
780 * Try to acquire lock for the next 60 sec based on the thread ID.
781 */
782 if ( ! $lock->try_lock( 60 ) ) {
783 return false;
784 }
785
786 $current_url = untrailingslashit( Freemius::get_unfiltered_site_url( null, true ) );
787 $is_localhost = FS_Site::is_localhost_by_address( $current_url );
788
789 $require_manual_resolution = false;
790
791 $instances = Freemius::_get_all_instances();
792
793 foreach ( $instances as $instance ) {
794 if ( ! $instance->is_registered() ) {
795 continue;
796 }
797
798 if ( ! $instance->is_clone() ) {
799 continue;
800 }
801
802 if ( ! $this->try_resolve_clone_automatically_by_instance( $instance, $current_url, $is_localhost ) ) {
803 $require_manual_resolution = true;
804 }
805 }
806
807 // Create a 1-day lock.
808 $lock->lock( WP_FS__TIME_24_HOURS_IN_SEC );
809
810 return ( ! $require_manual_resolution );
811 }
812
813 #endregion
814
815 #--------------------------------------------------------------------------------
816 #region Manual Clone Resolution
817 #--------------------------------------------------------------------------------
818
819 /**
820 * @author Leo Fajardo (@leorw)
821 * @since 2.5.0
822 */
823 function _add_clone_resolution_javascript() {
824 $vars = array( 'ajax_action' => Freemius::get_ajax_action_static( 'handle_clone_resolution' ) );
825
826 fs_require_once_template( 'clone-resolution-js.php', $vars );
827 }
828
829 /**
830 * @author Leo Fajardo (@leorw)
831 * @since 2.5.0
832 */
833 function _clone_resolution_action_ajax_handler() {
834 $this->_logger->entrance();
835
836 check_ajax_referer( Freemius::get_ajax_action_static( 'handle_clone_resolution' ), 'security' );
837
838 $clone_action = fs_request_get( 'clone_action' );
839 $blog_id = is_multisite() ?
840 fs_request_get( 'blog_id' ) :
841 0;
842
843 if ( is_multisite() && $blog_id == get_current_blog_id() ) {
844 $blog_id = 0;
845 }
846
847 if ( empty( $clone_action ) ) {
848 Freemius::shoot_ajax_failure( array(
849 'message' => fs_text_inline( 'Invalid clone resolution action.', 'invalid-clone-resolution-action-error' ),
850 'redirect_url' => '',
851 ) );
852 }
853
854 $result = $this->resolve_cloned_sites( $clone_action, array(), $blog_id );
855
856 Freemius::shoot_ajax_success( $result );
857 }
858
859 /**
860 * @author Leo Fajardo (@leorw)
861 * @since 2.5.0
862 *
863 * @param string $clone_action
864 * @param Freemius[] $fs_instances
865 * @param int $blog_id
866 *
867 * @return array
868 */
869 private function resolve_cloned_sites( $clone_action, $fs_instances = array(), $blog_id = 0 ) {
870 $this->_logger->entrance();
871
872 $result = array();
873
874 $instances_with_clone = array();
875 $instances_with_clone_count = 0;
876 $install_by_instance_id = array();
877
878 $instances = ( ! empty( $fs_instances ) ) ?
879 $fs_instances :
880 Freemius::_get_all_instances();
881
882 $should_switch_to_blog = ( $blog_id > 0 );
883
884 foreach ( $instances as $instance ) {
885 if ( $should_switch_to_blog ) {
886 $instance->switch_to_blog( $blog_id );
887 }
888
889 if ( $instance->is_registered() && $instance->is_clone() ) {
890 $instances_with_clone[] = $instance;
891
892 $instances_with_clone_count ++;
893
894 $install_by_instance_id[ $instance->get_id() ] = $instance->get_site();
895 }
896 }
897
898 if ( self::OPTION_TEMPORARY_DUPLICATE === $clone_action ) {
899 $this->store_temporary_duplicate_timestamp();
900 } else {
901 $redirect_url = '';
902
903 foreach ( $instances_with_clone as $instance ) {
904 if ( $should_switch_to_blog ) {
905 $instance->switch_to_blog( $blog_id );
906 }
907
908 $has_error = false;
909
910 if ( self::OPTION_NEW_HOME === $clone_action ) {
911 $instance->sync_install( array( 'is_new_site' => true ), true );
912
913 if ( $instance->is_clone() ) {
914 $has_error = true;
915 }
916 } else {
917 $instance->_handle_long_term_duplicate();
918
919 if ( ! is_object( $instance->get_site() ) ) {
920 $has_error = true;
921 }
922 }
923
924 if ( $has_error && 1 === $instances_with_clone_count ) {
925 $redirect_url = $instance->get_activation_url();
926 }
927 }
928
929 $result = ( array( 'redirect_url' => $redirect_url ) );
930 }
931
932 foreach ( $instances_with_clone as $instance ) {
933 if ( $should_switch_to_blog ) {
934 $instance->switch_to_blog( $blog_id );
935 }
936
937 // No longer a clone, send an update.
938 if ( ! $instance->is_clone() ) {
939 $instance->send_clone_resolution_update(
940 $clone_action,
941 $install_by_instance_id[ $instance->get_id() ]
942 );
943 }
944 }
945
946 if ( 'temporary_duplicate_license_activation' !== $clone_action ) {
947 $this->remove_clone_resolution_options_notice();
948 } else {
949 $this->remove_temporary_duplicate_notice();
950 }
951
952 if ( $should_switch_to_blog ) {
953 foreach ( $instances as $instance ) {
954 $instance->restore_current_blog();
955 }
956 }
957
958 return $result;
959 }
960
961 /**
962 * @author Leo Fajardo (@leorw)
963 * @since 2.5.0
964 */
965 private function hide_clone_admin_notices() {
966 $this->remove_clone_resolution_options_notice( false );
967 $this->remove_temporary_duplicate_notice( false );
968 }
969
970 /**
971 * @author Leo Fajardo (@leorw)
972 * @since 2.5.0
973 */
974 function maybe_show_clone_admin_notice() {
975 $this->_logger->entrance();
976
977 if ( fs_is_network_admin() ) {
978 $existing_notice_ids = $this->maybe_remove_notices();
979
980 if ( ! empty( $existing_notice_ids ) ) {
981 fs_enqueue_local_style( 'fs_clone_resolution_notice', '/admin/clone-resolution.css' );
982 }
983
984 return;
985 }
986
987 $first_instance_with_clone = null;
988
989 $site_urls = array();
990 $sites_with_license_urls = array();
991 $sites_with_premium_version_count = 0;
992 $product_ids = array();
993 $product_titles = array();
994
995 $instances = Freemius::_get_all_instances();
996
997 foreach ( $instances as $instance ) {
998 if ( ! $instance->is_registered() ) {
999 continue;
1000 }
1001
1002 if ( ! $instance->is_clone( true ) ) {
1003 continue;
1004 }
1005
1006 $install = $instance->get_site();
1007
1008 $site_urls[] = $install->url;
1009 $product_ids[] = $instance->get_id();
1010 $product_titles[] = $instance->get_plugin_title();
1011
1012 if ( is_null( $first_instance_with_clone ) ) {
1013 $first_instance_with_clone = $instance;
1014 }
1015
1016 if ( is_object( $instance->_get_license() ) ) {
1017 $sites_with_license_urls[] = $install->url;
1018 }
1019
1020 if ( $instance->is_premium() ) {
1021 $sites_with_premium_version_count ++;
1022 }
1023 }
1024
1025 if ( empty( $site_urls ) && empty( $sites_with_license_urls ) ) {
1026 $this->hide_clone_admin_notices();
1027
1028 return;
1029 }
1030
1031 $site_urls = array_unique( $site_urls );
1032 $sites_with_license_urls = array_unique( $sites_with_license_urls );
1033
1034 $module_label = fs_text_inline( 'products', 'products' );
1035 $admin_notice_module_title = null;
1036
1037 $has_temporary_duplicate_mode_expired = $this->has_temporary_duplicate_mode_expired();
1038
1039 if (
1040 ! $this->was_temporary_duplicate_mode_selected() ||
1041 $has_temporary_duplicate_mode_expired
1042 ) {
1043 if ( ! empty( $site_urls ) ) {
1044 fs_enqueue_local_style( 'fs_clone_resolution_notice', '/admin/clone-resolution.css' );
1045
1046 $doc_url = 'https://freemius.com/help/documentation/wordpress-sdk/safe-mode-clone-resolution-duplicate-website/';
1047
1048 if ( 1 === count( $instances ) ) {
1049 $doc_url = fs_apply_filter(
1050 $first_instance_with_clone->get_unique_affix(),
1051 'clone_resolution_documentation_url',
1052 $doc_url
1053 );
1054 }
1055
1056 $this->add_manual_clone_resolution_admin_notice(
1057 $product_ids,
1058 $product_titles,
1059 $site_urls,
1060 Freemius::get_unfiltered_site_url(),
1061 ( count( $site_urls ) === count( $sites_with_license_urls ) ),
1062 ( count( $site_urls ) === $sites_with_premium_version_count ),
1063 $doc_url
1064 );
1065 }
1066
1067 return;
1068 }
1069
1070 if ( empty( $sites_with_license_urls ) ) {
1071 return;
1072 }
1073
1074 if ( ! $this->is_temporary_duplicate_notice_shown() ) {
1075 $last_time_temporary_duplicate_notice_shown = $this->temporary_duplicate_notice_shown_timestamp;
1076 $was_temporary_duplicate_notice_shown_before = is_numeric( $last_time_temporary_duplicate_notice_shown );
1077
1078 if ( $was_temporary_duplicate_notice_shown_before ) {
1079 $temporary_duplicate_mode_expiration_timestamp = $this->get_temporary_duplicate_expiration_timestamp();
1080 $current_time = time();
1081
1082 if (
1083 $current_time > $temporary_duplicate_mode_expiration_timestamp ||
1084 $current_time < ( $temporary_duplicate_mode_expiration_timestamp - ( 2 * WP_FS__TIME_24_HOURS_IN_SEC ) )
1085 ) {
1086 // Do not show the notice if the temporary duplicate mode has already expired or it will expire more than 2 days from now.
1087 return;
1088 }
1089 }
1090 }
1091
1092 if ( 1 === count( $sites_with_license_urls ) ) {
1093 $module_label = $first_instance_with_clone->get_module_label( true );
1094 $admin_notice_module_title = $first_instance_with_clone->get_plugin_title();
1095 }
1096
1097 fs_enqueue_local_style( 'fs_clone_resolution_notice', '/admin/clone-resolution.css' );
1098
1099 $this->add_temporary_duplicate_sticky_notice(
1100 $product_ids,
1101 $this->get_temporary_duplicate_admin_notice_string( $sites_with_license_urls, $product_titles, $module_label ),
1102 $admin_notice_module_title
1103 );
1104 }
1105
1106 /**
1107 * Removes the notices from the storage if the context product is either no longer active on the context subsite or it's active but there's no longer any clone. This prevents the notices from being shown on the network-level admin page when they are no longer relevant.
1108 *
1109 * @author Leo Fajardo (@leorw)
1110 * @since 2.5.1
1111 *
1112 * @return string[]
1113 */
1114 private function maybe_remove_notices() {
1115 $notices = array(
1116 'clone_resolution_options_notice' => $this->_notices->get_sticky( 'clone_resolution_options_notice', true ),
1117 'temporary_duplicate_notice' => $this->_notices->get_sticky( 'temporary_duplicate_notice', true ),
1118 );
1119
1120 $instances = Freemius::_get_all_instances();
1121
1122 foreach ( $notices as $id => $notice ) {
1123 if ( ! is_array( $notice ) ) {
1124 unset( $notices[ $id ] );
1125 continue;
1126 }
1127
1128 if ( empty( $notice['data'] ) || ! is_array( $notice['data'] ) ) {
1129 continue;
1130 }
1131
1132 if ( empty( $notice['data']['product_ids'] ) || empty( $notice['data']['blog_id'] ) ) {
1133 continue;
1134 }
1135
1136 $product_ids = $notice['data']['product_ids'];
1137 $blog_id = $notice['data']['blog_id'];
1138 $has_clone = false;
1139
1140 if ( ! is_null( get_site( $blog_id ) ) ) {
1141 foreach ( $product_ids as $product_id ) {
1142 if ( ! isset( $instances[ 'm_' . $product_id ] ) ) {
1143 continue;
1144 }
1145
1146 $instance = $instances[ 'm_' . $product_id ];
1147
1148 $plugin_basename = $instance->get_plugin_basename();
1149
1150 $is_plugin_active = is_plugin_active_for_network( $plugin_basename );
1151
1152 if ( ! $is_plugin_active ) {
1153 switch_to_blog( $blog_id );
1154
1155 $is_plugin_active = is_plugin_active( $plugin_basename );
1156
1157 restore_current_blog();
1158 }
1159
1160 if ( ! $is_plugin_active ) {
1161 continue;
1162 }
1163
1164 $install = $instance->get_install_by_blog_id( $blog_id );
1165
1166 if ( ! is_object( $install ) ) {
1167 continue;
1168 }
1169
1170 $subsite_url = Freemius::get_unfiltered_site_url( $blog_id, true, true );
1171
1172 $has_clone = ( fs_strip_url_protocol( trailingslashit( $install->url ) ) !== $subsite_url );
1173 }
1174 }
1175
1176 if ( ! $has_clone ) {
1177 $this->_notices->remove_sticky( $id, true, false );
1178 unset( $notices[ $id ] );
1179 }
1180 }
1181
1182 return array_keys( $notices );
1183 }
1184
1185 /**
1186 * Adds a notice that provides the logged-in WordPress user with manual clone resolution options.
1187 *
1188 * @param number[] $product_ids
1189 * @param string[] $site_urls
1190 * @param string $current_url
1191 * @param bool $has_license
1192 * @param bool $is_premium
1193 * @param string $doc_url
1194 */
1195 private function add_manual_clone_resolution_admin_notice(
1196 $product_ids,
1197 $product_titles,
1198 $site_urls,
1199 $current_url,
1200 $has_license,
1201 $is_premium,
1202 $doc_url
1203 ) {
1204 $this->_logger->entrance();
1205
1206 $total_sites = count( $site_urls );
1207 $sites_list = '';
1208
1209 $total_products = count( $product_titles );
1210 $products_list = '';
1211
1212 if ( 1 === $total_products ) {
1213 $notice_header = sprintf(
1214 '<div class="fs-notice-header"><p>%s</p></div>',
1215 fs_esc_html_inline( '%1$s has been placed into safe mode because we noticed that %2$s is an exact copy of %3$s.', 'single-cloned-site-safe-mode-message' )
1216 );
1217 } else {
1218 $notice_header = sprintf(
1219 '<div class="fs-notice-header"><p>%s</p></div>',
1220 ( 1 === $total_sites ) ?
1221 fs_esc_html_inline( 'The products below have been placed into safe mode because we noticed that %2$s is an exact copy of %3$s:%1$s', 'multiple-products-cloned-site-safe-mode-message' ) :
1222 fs_esc_html_inline( 'The products below have been placed into safe mode because we noticed that %2$s is an exact copy of these sites:%3$s%1$s', 'multiple-products-multiple-cloned-sites-safe-mode-message' )
1223 );
1224
1225 foreach ( $product_titles as $product_title ) {
1226 $products_list .= sprintf( '<li>%s</li>', $product_title );
1227 }
1228
1229 $products_list = '<ol>' . $products_list . '</ol>';
1230
1231 foreach ( $site_urls as $site_url ) {
1232 $sites_list .= sprintf(
1233 '<li><a href="%s" target="_blank">%s</a></li>',
1234 $site_url,
1235 fs_strip_url_protocol( $site_url )
1236 );
1237 }
1238
1239 $sites_list = '<ol>' . $sites_list . '</ol>';
1240 }
1241
1242 $remote_site_link = '<b>' . (1 === $total_sites ?
1243 sprintf(
1244 '<a href="%s" target="_blank">%s</a>',
1245 $site_urls[0],
1246 fs_strip_url_protocol( $site_urls[0] )
1247 ) :
1248 fs_text_inline( 'the above-mentioned sites', 'above-mentioned-sites' )) . '</b>';
1249
1250 $current_site_link = sprintf(
1251 '<b><a href="%s" target="_blank">%s</a></b>',
1252 $current_url,
1253 fs_strip_url_protocol( $current_url )
1254 );
1255
1256 $button_template = '<button class="button" data-clone-action="%s">%s</button>';
1257 $option_template = '<div class="fs-clone-resolution-option"><strong>%s</strong><p>%s</p><div>%s</div></div>';
1258
1259 $duplicate_option = sprintf(
1260 $option_template,
1261 fs_esc_html_inline( 'Is %2$s a duplicate of %4$s?', 'duplicate-site-confirmation-message' ),
1262 fs_esc_html_inline( 'Yes, %2$s is a duplicate of %4$s for the purpose of testing, staging, or development.', 'duplicate-site-message' ),
1263 ( $this->has_temporary_duplicate_mode_expired() ?
1264 sprintf(
1265 $button_template,
1266 'long_term_duplicate',
1267 fs_text_inline( 'Long-Term Duplicate', 'long-term-duplicate' )
1268 ) :
1269 sprintf(
1270 $button_template,
1271 'temporary_duplicate',
1272 fs_text_inline( 'Duplicate Website', 'duplicate-site' )
1273 ) )
1274 );
1275
1276 $migration_option = sprintf(
1277 $option_template,
1278 fs_esc_html_inline( 'Is %2$s the new home of %4$s?', 'migrate-site-confirmation-message' ),
1279 sprintf(
1280 fs_esc_html_inline( 'Yes, %%2$s is replacing %%4$s. I would like to migrate my %s from %%4$s to %%2$s.', 'migrate-site-message' ),
1281 ( $has_license ? fs_text_inline( 'license', 'license' ) : fs_text_inline( 'data', 'data' ) )
1282 ),
1283 sprintf(
1284 $button_template,
1285 'new_home',
1286 $has_license ?
1287 fs_text_inline( 'Migrate License', 'migrate-product-license' ) :
1288 fs_text_inline( 'Migrate', 'migrate-product-data' )
1289 )
1290 );
1291
1292 $new_website = sprintf(
1293 $option_template,
1294 fs_esc_html_inline( 'Is %2$s a new website?', 'new-site-confirmation-message' ),
1295 fs_esc_html_inline( 'Yes, %2$s is a new and different website that is separate from %4$s.', 'new-site-message' ) .
1296 ($is_premium ?
1297 ' ' . fs_text_inline( 'It requires license activation.', 'new-site-requires-license-activation-message' ) :
1298 ''
1299 ),
1300 sprintf(
1301 $button_template,
1302 'new_website',
1303 ( ! $is_premium || ! $has_license ) ?
1304 fs_text_inline( 'New Website', 'new-website' ) :
1305 fs_text_inline( 'Activate License', 'activate-license' )
1306 )
1307 );
1308
1309 $blog_id = get_current_blog_id();
1310
1311 /**
1312 * %1$s - single product's title or product titles list.
1313 * %2$s - site's URL.
1314 * %3$s - single install's URL or install URLs list.
1315 * %4$s - Clone site's link or "the above-mentioned sites" if there are multiple clone sites.
1316 */
1317 $message = sprintf(
1318 $notice_header .
1319 '<div class="fs-clone-resolution-options-container" data-ajax-url="' . esc_attr( admin_url( 'admin-ajax.php?_fs_network_admin=false', 'relative' ) ) . '" data-blog-id="' . $blog_id . '">' .
1320 $duplicate_option .
1321 $migration_option .
1322 $new_website . '</div>' .
1323 sprintf( '<div class="fs-clone-documentation-container">Unsure what to do? <a href="%s" target="_blank">Read more here</a>.</div>', $doc_url ),
1324 // %1$s
1325 ( 1 === $total_products ?
1326 sprintf( '<b>%s</b>', $product_titles[0] ) :
1327 ( 1 === $total_sites ?
1328 sprintf( '<div>%s</div>', $products_list ) :
1329 sprintf( '<div><p><strong>%s</strong>:</p>%s</div>', fs_esc_html_x_inline( 'Products', 'Clone resolution admin notice products list label', 'products' ), $products_list ) )
1330 ),
1331 // %2$s
1332 $current_site_link,
1333 // %3$s
1334 ( 1 === $total_sites ?
1335 $remote_site_link :
1336 $sites_list ),
1337 // %4$s
1338 $remote_site_link
1339 );
1340
1341 $this->_notices->add_sticky(
1342 $message,
1343 'clone_resolution_options_notice',
1344 '',
1345 'warn',
1346 true,
1347 null,
1348 null,
1349 true,
1350 // Intentionally not dismissible.
1351 false,
1352 array(
1353 'product_ids' => $product_ids,
1354 'blog_id' => $blog_id
1355 )
1356 );
1357 }
1358
1359 #endregion
1360
1361 #--------------------------------------------------------------------------------
1362 #region Temporary Duplicate (Short Term)
1363 #--------------------------------------------------------------------------------
1364
1365 /**
1366 * @author Leo Fajardo (@leorw)
1367 * @since 2.5.0
1368 *
1369 * @return string
1370 */
1371 private function get_temporary_duplicate_admin_notice_string(
1372 $site_urls,
1373 $product_titles,
1374 $module_label
1375 ) {
1376 $this->_logger->entrance();
1377
1378 $temporary_duplicate_end_date = $this->get_temporary_duplicate_expiration_timestamp();
1379 $temporary_duplicate_end_date = date( 'M j, Y', $temporary_duplicate_end_date );
1380
1381 $current_url = Freemius::get_unfiltered_site_url();
1382 $current_site_link = sprintf(
1383 '<b><a href="%s" target="_blank">%s</a></b>',
1384 $current_url,
1385 fs_strip_url_protocol( $current_url )
1386 );
1387
1388 $total_sites = count( $site_urls );
1389 $sites_list = '';
1390
1391 $total_products = count( $product_titles );
1392 $products_list = '';
1393
1394 if ( $total_sites > 1 ) {
1395 foreach ( $site_urls as $site_url ) {
1396 $sites_list .= sprintf(
1397 '<li><a href="%s" target="_blank">%s</a></li>',
1398 $site_url,
1399 fs_strip_url_protocol( $site_url )
1400 );
1401 }
1402
1403 $sites_list = '<ol class="fs-sites-list">' . $sites_list . '</ol>';
1404 }
1405
1406 if ( $total_products > 1 ) {
1407 foreach ( $product_titles as $product_title ) {
1408 $products_list .= sprintf( '<li>%s</li>', $product_title );
1409 }
1410
1411 $products_list = '<ol>' . $products_list . '</ol>';
1412 }
1413
1414 return sprintf(
1415 sprintf(
1416 '<div>%s</div>',
1417 ( 1 === $total_sites ?
1418 sprintf( '<p>%s</p>', fs_esc_html_inline( 'You marked this website, %s, as a temporary duplicate of %s.', 'temporary-duplicate-message' ) ) :
1419 sprintf( '<p>%s:</p>', fs_esc_html_inline( 'You marked this website, %s, as a temporary duplicate of these sites', 'temporary-duplicate-of-sites-message' ) ) . '%s' )
1420 ) . '%s',
1421 $current_site_link,
1422 ( 1 === $total_sites ?
1423 sprintf(
1424 '<b><a href="%s" target="_blank">%s</a></b>',
1425 $site_urls[0],
1426 fs_strip_url_protocol( $site_urls[0] )
1427 ) :
1428 $sites_list ),
1429 sprintf(
1430 '<div class="fs-clone-resolution-options-container fs-duplicate-site-options" data-ajax-url="%s" data-blog-id="' . get_current_blog_id() . '"><p>%s</p>%s<p>%s</p></div>',
1431 esc_attr( admin_url( 'admin-ajax.php?_fs_network_admin=false', 'relative' ) ),
1432 sprintf(
1433 fs_esc_html_inline( "%s automatic security & feature updates and paid functionality will keep working without interruptions until %s (or when your license expires, whatever comes first).", 'duplicate-site-confirmation-message' ),
1434 ( 1 === $total_products ?
1435 sprintf(
1436 fs_esc_html_x_inline( "The %s's", '"The <product_label>", e.g.: "The plugin"', 'the-product-x'),
1437 "<strong>{$module_label}</strong>"
1438 ) :
1439 fs_esc_html_inline( "The following products'", 'the-following-products' ) ),
1440 sprintf( '<strong>%s</strong>', $temporary_duplicate_end_date )
1441 ),
1442 ( 1 === $total_products ?
1443 '' :
1444 sprintf( '<div>%s</div>', $products_list )
1445 ),
1446 sprintf(
1447 fs_esc_html_inline( 'If this is a long term duplicate, to keep automatic updates and paid functionality after %s, please %s.', 'duplicate-site-message' ),
1448 sprintf( '<strong>%s</strong>', $temporary_duplicate_end_date),
1449 sprintf( '<a href="#" id="fs_temporary_duplicate_license_activation_link" data-clone-action="temporary_duplicate_license_activation">%s</a>', fs_esc_html_inline( 'activate a license here', 'activate-license-here' ) )
1450 )
1451 )
1452 );
1453 }
1454
1455 /**
1456 * Determines if the temporary duplicate mode has already expired.
1457 *
1458 * @return bool
1459 */
1460 function has_temporary_duplicate_mode_expired() {
1461 $temporary_duplicate_mode_start_timestamp = $this->was_temporary_duplicate_mode_selected() ?
1462 $this->get_option( 'temporary_duplicate_mode_selection_timestamp', true ) :
1463 $this->get_clone_identification_timestamp();
1464
1465 if ( ! is_numeric( $temporary_duplicate_mode_start_timestamp ) ) {
1466 return false;
1467 }
1468
1469 return ( time() > ( $temporary_duplicate_mode_start_timestamp + self::TEMPORARY_DUPLICATE_PERIOD ) );
1470 }
1471
1472 /**
1473 * Determines if the logged-in WordPress user manually selected the temporary duplicate mode for the site.
1474 *
1475 * @return bool
1476 */
1477 function was_temporary_duplicate_mode_selected() {
1478 return is_numeric( $this->temporary_duplicate_mode_selection_timestamp );
1479 }
1480
1481 /**
1482 * Stores the time when the logged-in WordPress user selected the temporary duplicate mode for the site.
1483 */
1484 private function store_temporary_duplicate_timestamp() {
1485 $this->temporary_duplicate_mode_selection_timestamp = time();
1486 }
1487
1488 /**
1489 * Removes the notice that is shown when the logged-in WordPress user has selected the temporary duplicate mode for the site.
1490 *
1491 * @param bool $store
1492 */
1493 function remove_clone_resolution_options_notice( $store = true ) {
1494 $this->_notices->remove_sticky( 'clone_resolution_options_notice', true, $store );
1495 }
1496
1497 /**
1498 * Removes the notice that is shown when the logged-in WordPress user has selected the temporary duplicate mode for the site.
1499 *
1500 * @param bool $store
1501 */
1502 function remove_temporary_duplicate_notice( $store = true ) {
1503 $this->_notices->remove_sticky( 'temporary_duplicate_notice', true, $store );
1504 }
1505
1506 /**
1507 * Determines if the manual clone resolution options notice is currently being shown.
1508 *
1509 * @return bool
1510 */
1511 function is_clone_resolution_options_notice_shown() {
1512 return $this->_notices->has_sticky( 'clone_resolution_options_notice', true );
1513 }
1514
1515 /**
1516 * Determines if the temporary duplicate notice is currently being shown.
1517 *
1518 * @return bool
1519 */
1520 function is_temporary_duplicate_notice_shown() {
1521 return $this->_notices->has_sticky( 'temporary_duplicate_notice', true );
1522 }
1523
1524 /**
1525 * Determines if a site was marked as a temporary duplicate and if it's still a temporary duplicate.
1526 *
1527 * @return bool
1528 */
1529 function is_temporary_duplicate_by_blog_id( $blog_id ) {
1530 $timestamp = $this->get_option( 'temporary_duplicate_mode_selection_timestamp', false, $blog_id );
1531
1532 return (
1533 is_numeric( $timestamp ) &&
1534 time() < ( $timestamp + self::TEMPORARY_DUPLICATE_PERIOD )
1535 );
1536 }
1537
1538 /**
1539 * Determines the last time the temporary duplicate notice was shown.
1540 *
1541 * @return int|null
1542 */
1543 function last_time_temporary_duplicate_notice_was_shown() {
1544 return $this->temporary_duplicate_notice_shown_timestamp;
1545 }
1546
1547 /**
1548 * Clears the time that has been stored when the temporary duplicate notice was shown.
1549 */
1550 function clear_temporary_duplicate_notice_shown_timestamp() {
1551 unset( $this->temporary_duplicate_notice_shown_timestamp );
1552 }
1553
1554 /**
1555 * Adds a temporary duplicate notice that provides the logged-in WordPress user with an option to activate a license for the site.
1556 *
1557 * @param number[] $product_ids
1558 * @param string $message
1559 * @param string|null $plugin_title
1560 */
1561 function add_temporary_duplicate_sticky_notice(
1562 $product_ids,
1563 $message,
1564 $plugin_title = null
1565 ) {
1566 $this->_logger->entrance();
1567
1568 $this->_notices->add_sticky(
1569 $message,
1570 'temporary_duplicate_notice',
1571 '',
1572 'promotion',
1573 true,
1574 null,
1575 $plugin_title,
1576 true,
1577 true,
1578 array(
1579 'product_ids' => $product_ids,
1580 'blog_id' => get_current_blog_id()
1581 )
1582 );
1583
1584 $this->temporary_duplicate_notice_shown_timestamp = time();
1585 }
1586
1587 #endregion
1588
1589 /**
1590 * @author Leo Fajardo
1591 * @since 2.5.0
1592 *
1593 * @param string $key
1594 *
1595 * @return bool
1596 */
1597 private function should_use_network_storage( $key ) {
1598 return ( 'new_blog_install_map' === $key );
1599 }
1600
1601 /**
1602 * @param string $key
1603 * @param number|null $blog_id
1604 *
1605 * @return FS_Option_Manager
1606 */
1607 private function get_storage( $key, $blog_id = null ) {
1608 if ( is_numeric( $blog_id ) ){
1609 return FS_Option_Manager::get_manager( WP_FS___OPTION_PREFIX . self::OPTION_MANAGER_NAME, true, $blog_id );
1610 }
1611
1612 return $this->should_use_network_storage( $key ) ?
1613 $this->_network_storage :
1614 $this->_storage;
1615 }
1616
1617 /**
1618 * @param string $name
1619 * @param bool $flush
1620 * @param number|null $blog_id
1621 *
1622 * @return mixed
1623 */
1624 private function get_option( $name, $flush = false, $blog_id = null ) {
1625 return $this->get_storage( $name, $blog_id )->get_option( $name, null, $flush );
1626 }
1627
1628 #--------------------------------------------------------------------------------
1629 #region Magic methods
1630 #--------------------------------------------------------------------------------
1631
1632 /**
1633 * @param string $name
1634 * @param int|string $value
1635 */
1636 function __set( $name, $value ) {
1637 $this->get_storage( $name )->set_option( $name, $value, true );
1638 }
1639
1640 /**
1641 * @param string $name
1642 *
1643 * @return bool
1644 */
1645 function __isset( $name ) {
1646 return $this->get_storage( $name )->has_option( $name, true );
1647 }
1648
1649 /**
1650 * @param string $name
1651 */
1652 function __unset( $name ) {
1653 $this->get_storage( $name )->unset_option( $name, true );
1654 }
1655
1656 /**
1657 * @param string $name
1658 *
1659 * @return null|int|string
1660 */
1661 function __get( $name ) {
1662 return $this->get_option(
1663 $name,
1664 // Reload storage from DB when accessing request_handler_* options to avoid race conditions.
1665 fs_starts_with( $name, 'request_handler' )
1666 );
1667 }
1668
1669 #endregion
1670 }
1671