PluginProbe
Independent Analytics – WordPress Analytics Plugin / 2.5.0
Independent Analytics – WordPress Analytics Plugin v2.5.0
2.15.5 2.15.4 2.15.3 2.15.2 2.15.1 2.15.0 2.14.10 trunk 1.1 1.10 1.10.1 1.11 1.12 1.13 1.14 1.15 1.16 1.17 1.17.1 1.17.2 1.17.3 1.17.4 1.18 1.18.1 1.19.0 All 120 releases
independent-analytics / freemius / includes / managers / class-fs-clone-manager.php
class-fs-clone-manager.php
1,661 lines 63.4 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 = Freemius::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 string|false $license_key
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_key = false ) {
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 if ( is_object( $user ) ) {
434 // When a clone is found, we prefer to use the same user of the original install for the opt-in.
435 $instance->install_with_user( $user, $license_key, false, false );
436 } else {
437 // If no user is found, activate with the license.
438 $instance->opt_in(
439 false,
440 false,
441 false,
442 $license_key
443 );
444 }
445
446 if ( is_object( $instance->get_site() ) ) {
447 // Install successfully created.
448 return true;
449 }
450
451 // Restore from backup.
452 $instance->restore_backup_site();
453
454 return false;
455 }
456
457 /**
458 * Try to resolve the clone situation automatically.
459 *
460 * @param Freemius $instance
461 * @param string $current_url
462 * @param bool $is_localhost
463 * @param bool|null $is_clone_of_network_subsite
464 *
465 * @return bool If managed to automatically resolve the clone.
466 */
467 private function try_resolve_clone_automatically_by_instance(
468 Freemius $instance,
469 $current_url,
470 $is_localhost,
471 $is_clone_of_network_subsite = null
472 ) {
473 // Try to find a different install of the context product that is associated with the current URL.
474 $associated_install = $this->find_other_install_by_url( $instance, $current_url );
475
476 if ( is_object( $associated_install ) ) {
477 // Replace the current install with a different install that is associated with the current URL.
478 $instance->store_site( new FS_Site( clone $associated_install ) );
479 $instance->sync_install( array( 'is_new_site' => true ), true );
480
481 return true;
482 }
483
484 if ( ! $instance->is_premium() ) {
485 // For free products, opt-in with the context user to create new install.
486 return $this->delete_install_and_connect( $instance );
487 }
488
489 $license = $instance->_get_license();
490 $can_activate_license = ( is_object( $license ) && ! $license->is_utilized( $is_localhost ) );
491
492 if ( ! $can_activate_license ) {
493 // License can't be activated, therefore, can't be automatically resolved.
494 return false;
495 }
496
497 if ( ! WP_FS__IS_LOCALHOST_FOR_SERVER && ! $is_localhost ) {
498 $is_clone_of_network_subsite = ( ! is_null( $is_clone_of_network_subsite ) ) ?
499 $is_clone_of_network_subsite :
500 is_object( $this->find_network_subsite_clone_install( $instance ) );
501
502 if ( ! $is_clone_of_network_subsite ) {
503 return false;
504 }
505 }
506
507 // If the site is a clone of another subsite in the network, or a localhost one, try to auto activate the license.
508 return $this->delete_install_and_connect( $instance, $license->secret_key );
509 }
510
511 /**
512 * @author Leo Fajardo (@leorw)
513 * @since 2.5.0
514 */
515 private function try_resolve_clone_automatically() {
516 $clone_action = $this->get_clone_resolution_action_from_config();
517
518 if ( ! empty( $clone_action ) ) {
519 $this->try_resolve_clone_automatically_by_config( $clone_action );
520 return;
521 }
522
523 $this->try_automatic_resolution();
524 }
525
526 /**
527 * Tries to resolve the clone situation automatically based on the config in the wp-config.php file.
528 *
529 * @author Leo Fajardo (@leorw)
530 * @since 2.5.0
531 *
532 * @param string $clone_action
533 */
534 private function try_resolve_clone_automatically_by_config( $clone_action ) {
535 $fs_instances = array();
536
537 if ( self::OPTION_LONG_TERM_DUPLICATE === $clone_action ) {
538 $instances = Freemius::_get_all_instances();
539
540 foreach ( $instances as $instance ) {
541 if ( ! $instance->is_registered() ) {
542 continue;
543 }
544
545 if ( ! $instance->is_clone() ) {
546 continue;
547 }
548
549 $license = $instance->has_features_enabled_license() ?
550 $instance->_get_license() :
551 null;
552
553 if (
554 is_object( $license ) &&
555 ! $license->is_utilized(
556 ( WP_FS__IS_LOCALHOST_FOR_SERVER || FS_Site::is_localhost_by_address( Freemius::get_unfiltered_site_url() ) )
557 )
558 ) {
559 $fs_instances[] = $instance;
560 }
561 }
562
563 if ( empty( $fs_instances ) ) {
564 return;
565 }
566 }
567
568 $this->resolve_cloned_sites( $clone_action, $fs_instances );
569 }
570
571 /**
572 * @author Leo Fajard (@leorw)
573 * @since 2.5.0
574 *
575 * @return string|null
576 */
577 private function get_clone_resolution_action_from_config() {
578 if ( ! defined( 'FS__RESOLVE_CLONE_AS' ) ) {
579 return null;
580 }
581
582 if ( ! in_array(
583 FS__RESOLVE_CLONE_AS,
584 array(
585 self::OPTION_NEW_HOME,
586 self::OPTION_TEMPORARY_DUPLICATE,
587 self::OPTION_LONG_TERM_DUPLICATE,
588 )
589 ) ) {
590 return null;
591 }
592
593 return FS__RESOLVE_CLONE_AS;
594 }
595
596 /**
597 * Tries to recover the install of a newly created subsite or resolve it if it's a clone.
598 *
599 * @author Leo Fajardo (@leorw)
600 * @since 2.5.0
601 *
602 * @param Freemius $instance
603 */
604 function maybe_resolve_new_subsite_install_automatically( Freemius $instance ) {
605 if ( ! $instance->is_user_in_admin() ) {
606 // 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).
607 return;
608 }
609
610 if ( ! is_multisite() ) {
611 return;
612 }
613
614 $new_blog_install_map = $this->new_blog_install_map;
615
616 if ( empty( $new_blog_install_map ) || ! is_array( $new_blog_install_map ) ) {
617 return;
618 }
619
620 $is_network_admin = fs_is_network_admin();
621
622 if ( ! $is_network_admin ) {
623 // If not in network admin, handle the current site.
624 $blog_id = get_current_blog_id();
625 } else {
626 // If in network admin, handle only the first site.
627 $blog_ids = array_keys( $new_blog_install_map );
628 $blog_id = $blog_ids[0];
629 }
630
631 if ( ! isset( $new_blog_install_map[ $blog_id ] ) ) {
632 // There's no site to handle.
633 return;
634 }
635
636 $expected_install_id = $new_blog_install_map[ $blog_id ]['install_id'];
637
638 $current_install = $instance->get_install_by_blog_id( $blog_id );
639 $current_install_id = is_object( $current_install ) ?
640 $current_install->id :
641 null;
642
643 if ( $expected_install_id == $current_install_id ) {
644 // Remove the current site's information from the map to prevent handling it again.
645 $this->remove_new_blog_install_info_from_storage( $blog_id );
646
647 return;
648 }
649
650 require_once WP_FS__DIR_INCLUDES . '/class-fs-lock.php';
651
652 $lock = new FS_Lock( self::OPTION_NAME . '_subsite' );
653
654 if ( ! $lock->try_lock(60) ) {
655 return;
656 }
657
658 $instance->switch_to_blog( $blog_id );
659
660 $current_url = untrailingslashit( Freemius::get_unfiltered_site_url( null, true ) );
661 $current_install_url = is_object( $current_install ) ?
662 fs_strip_url_protocol( untrailingslashit( $current_install->url ) ) :
663 null;
664
665 // This can be `false` even if the install is a clone as the URL can be updated as part of the cloning process.
666 $is_clone = ( ! is_null( $current_install_url ) && $current_url !== $current_install_url );
667
668 if ( ! FS_Site::is_valid_id( $expected_install_id ) ) {
669 $expected_install = null;
670 } else {
671 $expected_install = $instance->fetch_install_by_id( $expected_install_id );
672 }
673
674 if ( FS_Api::is_api_result_entity( $expected_install ) ) {
675 // Replace the current install with the expected install.
676 $instance->store_site( new FS_Site( clone $expected_install ) );
677 $instance->sync_install( array( 'is_new_site' => true ), true );
678 } else {
679 $network_subsite_clone_install = null;
680
681 if ( ! $is_clone ) {
682 // 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.
683 $network_subsite_clone_install = $this->find_network_subsite_clone_install( $instance );
684 }
685
686 if ( $is_clone || is_object( $network_subsite_clone_install ) ) {
687 // 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.
688 $is_localhost = FS_Site::is_localhost_by_address( $current_url );
689
690 $resolved = $this->try_resolve_clone_automatically_by_instance( $instance, $current_url, $is_localhost, is_object( $network_subsite_clone_install ) );
691
692 if ( ! $resolved && is_object( $network_subsite_clone_install ) ) {
693 if ( empty( $this->get_clone_identification_timestamp() ) ) {
694 $this->store_clone_identification_timestamp();
695 }
696
697 // 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.
698 $instance->store_site( clone $network_subsite_clone_install );
699 }
700 }
701 }
702
703 $instance->restore_current_blog();
704
705 // Remove the current site's information from the map to prevent handling it again.
706 $this->remove_new_blog_install_info_from_storage( $blog_id );
707
708 $lock->unlock();
709 }
710
711 /**
712 * 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.
713 *
714 * @author Leo Fajardo (@leorw)
715 * @since 2.5.0
716 *
717 * @param int $blog_id
718 * @param FS_Site $site
719 */
720 function store_blog_install_info( $blog_id, $site = null ) {
721 $new_blog_install_map = $this->new_blog_install_map;
722
723 if (
724 empty( $new_blog_install_map ) ||
725 ! is_array( $new_blog_install_map )
726 ) {
727 $new_blog_install_map = array();
728 }
729
730 $install_id = null;
731
732 if ( is_object( $site ) ) {
733 $install_id = $site->id;
734 }
735
736 $new_blog_install_map[ $blog_id ] = array( 'install_id' => $install_id );
737
738 $this->new_blog_install_map = $new_blog_install_map;
739 }
740
741 /**
742 * @author Leo Fajardo (@leorw)
743 * @since 2.5.0
744 *
745 * @param int $blog_id
746 */
747 private function remove_new_blog_install_info_from_storage( $blog_id ) {
748 $new_blog_install_map = $this->new_blog_install_map;
749
750 unset( $new_blog_install_map[ $blog_id ] );
751 $this->new_blog_install_map = $new_blog_install_map;
752 }
753
754 /**
755 * Tries to resolve all clones automatically.
756 *
757 * @author Leo Fajardo (@leorw)
758 * @since 2.5.0
759 *
760 * @return bool If managed to automatically resolve all clones.
761 */
762 private function try_automatic_resolution() {
763 $this->_logger->entrance();
764
765 require_once WP_FS__DIR_INCLUDES . '/class-fs-lock.php';
766
767 $lock = new FS_Lock( self::OPTION_NAME );
768
769 /**
770 * Try to acquire lock for the next 60 sec based on the thread ID.
771 */
772 if ( ! $lock->try_lock( 60 ) ) {
773 return false;
774 }
775
776 $current_url = untrailingslashit( Freemius::get_unfiltered_site_url( null, true ) );
777 $is_localhost = FS_Site::is_localhost_by_address( $current_url );
778
779 $require_manual_resolution = false;
780
781 $instances = Freemius::_get_all_instances();
782
783 foreach ( $instances as $instance ) {
784 if ( ! $instance->is_registered() ) {
785 continue;
786 }
787
788 if ( ! $instance->is_clone() ) {
789 continue;
790 }
791
792 if ( ! $this->try_resolve_clone_automatically_by_instance( $instance, $current_url, $is_localhost ) ) {
793 $require_manual_resolution = true;
794 }
795 }
796
797 // Create a 1-day lock.
798 $lock->lock( WP_FS__TIME_24_HOURS_IN_SEC );
799
800 return ( ! $require_manual_resolution );
801 }
802
803 #endregion
804
805 #--------------------------------------------------------------------------------
806 #region Manual Clone Resolution
807 #--------------------------------------------------------------------------------
808
809 /**
810 * @author Leo Fajardo (@leorw)
811 * @since 2.5.0
812 */
813 function _add_clone_resolution_javascript() {
814 $vars = array( 'ajax_action' => Freemius::get_ajax_action_static( 'handle_clone_resolution' ) );
815
816 fs_require_once_template( 'clone-resolution-js.php', $vars );
817 }
818
819 /**
820 * @author Leo Fajardo (@leorw)
821 * @since 2.5.0
822 */
823 function _clone_resolution_action_ajax_handler() {
824 $this->_logger->entrance();
825
826 check_ajax_referer( Freemius::get_ajax_action_static( 'handle_clone_resolution' ), 'security' );
827
828 $clone_action = fs_request_get( 'clone_action' );
829 $blog_id = is_multisite() ?
830 fs_request_get( 'blog_id' ) :
831 0;
832
833 if ( is_multisite() && $blog_id == get_current_blog_id() ) {
834 $blog_id = 0;
835 }
836
837 if ( empty( $clone_action ) ) {
838 Freemius::shoot_ajax_failure( array(
839 'message' => fs_text_inline( 'Invalid clone resolution action.', 'invalid-clone-resolution-action-error' ),
840 'redirect_url' => '',
841 ) );
842 }
843
844 $result = $this->resolve_cloned_sites( $clone_action, array(), $blog_id );
845
846 Freemius::shoot_ajax_success( $result );
847 }
848
849 /**
850 * @author Leo Fajardo (@leorw)
851 * @since 2.5.0
852 *
853 * @param string $clone_action
854 * @param Freemius[] $fs_instances
855 * @param int $blog_id
856 *
857 * @return array
858 */
859 private function resolve_cloned_sites( $clone_action, $fs_instances = array(), $blog_id = 0 ) {
860 $this->_logger->entrance();
861
862 $result = array();
863
864 $instances_with_clone = array();
865 $instances_with_clone_count = 0;
866 $install_by_instance_id = array();
867
868 $instances = ( ! empty( $fs_instances ) ) ?
869 $fs_instances :
870 Freemius::_get_all_instances();
871
872 $should_switch_to_blog = ( $blog_id > 0 );
873
874 foreach ( $instances as $instance ) {
875 if ( $should_switch_to_blog ) {
876 $instance->switch_to_blog( $blog_id );
877 }
878
879 if ( $instance->is_registered() && $instance->is_clone() ) {
880 $instances_with_clone[] = $instance;
881
882 $instances_with_clone_count ++;
883
884 $install_by_instance_id[ $instance->get_id() ] = $instance->get_site();
885 }
886 }
887
888 if ( self::OPTION_TEMPORARY_DUPLICATE === $clone_action ) {
889 $this->store_temporary_duplicate_timestamp();
890 } else {
891 $redirect_url = '';
892
893 foreach ( $instances_with_clone as $instance ) {
894 if ( $should_switch_to_blog ) {
895 $instance->switch_to_blog( $blog_id );
896 }
897
898 $has_error = false;
899
900 if ( self::OPTION_NEW_HOME === $clone_action ) {
901 $instance->sync_install( array( 'is_new_site' => true ), true );
902
903 if ( $instance->is_clone() ) {
904 $has_error = true;
905 }
906 } else {
907 $instance->_handle_long_term_duplicate();
908
909 if ( ! is_object( $instance->get_site() ) ) {
910 $has_error = true;
911 }
912 }
913
914 if ( $has_error && 1 === $instances_with_clone_count ) {
915 $redirect_url = $instance->get_activation_url();
916 }
917 }
918
919 $result = ( array( 'redirect_url' => $redirect_url ) );
920 }
921
922 foreach ( $instances_with_clone as $instance ) {
923 if ( $should_switch_to_blog ) {
924 $instance->switch_to_blog( $blog_id );
925 }
926
927 // No longer a clone, send an update.
928 if ( ! $instance->is_clone() ) {
929 $instance->send_clone_resolution_update(
930 $clone_action,
931 $install_by_instance_id[ $instance->get_id() ]
932 );
933 }
934 }
935
936 if ( 'temporary_duplicate_license_activation' !== $clone_action ) {
937 $this->remove_clone_resolution_options_notice();
938 } else {
939 $this->remove_temporary_duplicate_notice();
940 }
941
942 if ( $should_switch_to_blog ) {
943 foreach ( $instances as $instance ) {
944 $instance->restore_current_blog();
945 }
946 }
947
948 return $result;
949 }
950
951 /**
952 * @author Leo Fajardo (@leorw)
953 * @since 2.5.0
954 */
955 private function hide_clone_admin_notices() {
956 $this->remove_clone_resolution_options_notice( false );
957 $this->remove_temporary_duplicate_notice( false );
958 }
959
960 /**
961 * @author Leo Fajardo (@leorw)
962 * @since 2.5.0
963 */
964 function maybe_show_clone_admin_notice() {
965 $this->_logger->entrance();
966
967 if ( fs_is_network_admin() ) {
968 $existing_notice_ids = $this->maybe_remove_notices();
969
970 if ( ! empty( $existing_notice_ids ) ) {
971 fs_enqueue_local_style( 'fs_clone_resolution_notice', '/admin/clone-resolution.css' );
972 }
973
974 return;
975 }
976
977 $first_instance_with_clone = null;
978
979 $site_urls = array();
980 $sites_with_license_urls = array();
981 $sites_with_premium_version_count = 0;
982 $product_ids = array();
983 $product_titles = array();
984
985 $instances = Freemius::_get_all_instances();
986
987 foreach ( $instances as $instance ) {
988 if ( ! $instance->is_registered() ) {
989 continue;
990 }
991
992 if ( ! $instance->is_clone( true ) ) {
993 continue;
994 }
995
996 $install = $instance->get_site();
997
998 $site_urls[] = $install->url;
999 $product_ids[] = $instance->get_id();
1000 $product_titles[] = $instance->get_plugin_title();
1001
1002 if ( is_null( $first_instance_with_clone ) ) {
1003 $first_instance_with_clone = $instance;
1004 }
1005
1006 if ( is_object( $instance->_get_license() ) ) {
1007 $sites_with_license_urls[] = $install->url;
1008 }
1009
1010 if ( $instance->is_premium() ) {
1011 $sites_with_premium_version_count ++;
1012 }
1013 }
1014
1015 if ( empty( $site_urls ) && empty( $sites_with_license_urls ) ) {
1016 $this->hide_clone_admin_notices();
1017
1018 return;
1019 }
1020
1021 $site_urls = array_unique( $site_urls );
1022 $sites_with_license_urls = array_unique( $sites_with_license_urls );
1023
1024 $module_label = fs_text_inline( 'products', 'products' );
1025 $admin_notice_module_title = null;
1026
1027 $has_temporary_duplicate_mode_expired = $this->has_temporary_duplicate_mode_expired();
1028
1029 if (
1030 ! $this->was_temporary_duplicate_mode_selected() ||
1031 $has_temporary_duplicate_mode_expired
1032 ) {
1033 if ( ! empty( $site_urls ) ) {
1034 fs_enqueue_local_style( 'fs_clone_resolution_notice', '/admin/clone-resolution.css' );
1035
1036 $doc_url = 'https://freemius.com/help/documentation/wordpress-sdk/safe-mode-clone-resolution-duplicate-website/';
1037
1038 if ( 1 === count( $instances ) ) {
1039 $doc_url = fs_apply_filter(
1040 $first_instance_with_clone->get_unique_affix(),
1041 'clone_resolution_documentation_url',
1042 $doc_url
1043 );
1044 }
1045
1046 $this->add_manual_clone_resolution_admin_notice(
1047 $product_ids,
1048 $product_titles,
1049 $site_urls,
1050 Freemius::get_unfiltered_site_url(),
1051 ( count( $site_urls ) === count( $sites_with_license_urls ) ),
1052 ( count( $site_urls ) === $sites_with_premium_version_count ),
1053 $doc_url
1054 );
1055 }
1056
1057 return;
1058 }
1059
1060 if ( empty( $sites_with_license_urls ) ) {
1061 return;
1062 }
1063
1064 if ( ! $this->is_temporary_duplicate_notice_shown() ) {
1065 $last_time_temporary_duplicate_notice_shown = $this->temporary_duplicate_notice_shown_timestamp;
1066 $was_temporary_duplicate_notice_shown_before = is_numeric( $last_time_temporary_duplicate_notice_shown );
1067
1068 if ( $was_temporary_duplicate_notice_shown_before ) {
1069 $temporary_duplicate_mode_expiration_timestamp = $this->get_temporary_duplicate_expiration_timestamp();
1070 $current_time = time();
1071
1072 if (
1073 $current_time > $temporary_duplicate_mode_expiration_timestamp ||
1074 $current_time < ( $temporary_duplicate_mode_expiration_timestamp - ( 2 * WP_FS__TIME_24_HOURS_IN_SEC ) )
1075 ) {
1076 // Do not show the notice if the temporary duplicate mode has already expired or it will expire more than 2 days from now.
1077 return;
1078 }
1079 }
1080 }
1081
1082 if ( 1 === count( $sites_with_license_urls ) ) {
1083 $module_label = $first_instance_with_clone->get_module_label( true );
1084 $admin_notice_module_title = $first_instance_with_clone->get_plugin_title();
1085 }
1086
1087 fs_enqueue_local_style( 'fs_clone_resolution_notice', '/admin/clone-resolution.css' );
1088
1089 $this->add_temporary_duplicate_sticky_notice(
1090 $product_ids,
1091 $this->get_temporary_duplicate_admin_notice_string( $sites_with_license_urls, $product_titles, $module_label ),
1092 $admin_notice_module_title
1093 );
1094 }
1095
1096 /**
1097 * 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.
1098 *
1099 * @author Leo Fajardo (@leorw)
1100 * @since 2.5.1
1101 *
1102 * @return string[]
1103 */
1104 private function maybe_remove_notices() {
1105 $notices = array(
1106 'clone_resolution_options_notice' => $this->_notices->get_sticky( 'clone_resolution_options_notice', true ),
1107 'temporary_duplicate_notice' => $this->_notices->get_sticky( 'temporary_duplicate_notice', true ),
1108 );
1109
1110 $instances = Freemius::_get_all_instances();
1111
1112 foreach ( $notices as $id => $notice ) {
1113 if ( ! is_array( $notice ) ) {
1114 unset( $notices[ $id ] );
1115 continue;
1116 }
1117
1118 if ( empty( $notice['data'] ) || ! is_array( $notice['data'] ) ) {
1119 continue;
1120 }
1121
1122 if ( empty( $notice['data']['product_ids'] ) || empty( $notice['data']['blog_id'] ) ) {
1123 continue;
1124 }
1125
1126 $product_ids = $notice['data']['product_ids'];
1127 $blog_id = $notice['data']['blog_id'];
1128 $has_clone = false;
1129
1130 if ( ! is_null( get_site( $blog_id ) ) ) {
1131 foreach ( $product_ids as $product_id ) {
1132 if ( ! isset( $instances[ 'm_' . $product_id ] ) ) {
1133 continue;
1134 }
1135
1136 $instance = $instances[ 'm_' . $product_id ];
1137
1138 $plugin_basename = $instance->get_plugin_basename();
1139
1140 $is_plugin_active = is_plugin_active_for_network( $plugin_basename );
1141
1142 if ( ! $is_plugin_active ) {
1143 switch_to_blog( $blog_id );
1144
1145 $is_plugin_active = is_plugin_active( $plugin_basename );
1146
1147 restore_current_blog();
1148 }
1149
1150 if ( ! $is_plugin_active ) {
1151 continue;
1152 }
1153
1154 $install = $instance->get_install_by_blog_id( $blog_id );
1155
1156 if ( ! is_object( $install ) ) {
1157 continue;
1158 }
1159
1160 $subsite_url = Freemius::get_unfiltered_site_url( $blog_id, true, true );
1161
1162 $has_clone = ( fs_strip_url_protocol( trailingslashit( $install->url ) ) !== $subsite_url );
1163 }
1164 }
1165
1166 if ( ! $has_clone ) {
1167 $this->_notices->remove_sticky( $id, true, false );
1168 unset( $notices[ $id ] );
1169 }
1170 }
1171
1172 return array_keys( $notices );
1173 }
1174
1175 /**
1176 * Adds a notice that provides the logged-in WordPress user with manual clone resolution options.
1177 *
1178 * @param number[] $product_ids
1179 * @param string[] $site_urls
1180 * @param string $current_url
1181 * @param bool $has_license
1182 * @param bool $is_premium
1183 * @param string $doc_url
1184 */
1185 private function add_manual_clone_resolution_admin_notice(
1186 $product_ids,
1187 $product_titles,
1188 $site_urls,
1189 $current_url,
1190 $has_license,
1191 $is_premium,
1192 $doc_url
1193 ) {
1194 $this->_logger->entrance();
1195
1196 $total_sites = count( $site_urls );
1197 $sites_list = '';
1198
1199 $total_products = count( $product_titles );
1200 $products_list = '';
1201
1202 if ( 1 === $total_products ) {
1203 $notice_header = sprintf(
1204 '<div class="fs-notice-header"><p>%s</p></div>',
1205 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' )
1206 );
1207 } else {
1208 $notice_header = sprintf(
1209 '<div class="fs-notice-header"><p>%s</p></div>',
1210 ( 1 === $total_sites ) ?
1211 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' ) :
1212 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' )
1213 );
1214
1215 foreach ( $product_titles as $product_title ) {
1216 $products_list .= sprintf( '<li>%s</li>', $product_title );
1217 }
1218
1219 $products_list = '<ol>' . $products_list . '</ol>';
1220
1221 foreach ( $site_urls as $site_url ) {
1222 $sites_list .= sprintf(
1223 '<li><a href="%s" target="_blank">%s</a></li>',
1224 $site_url,
1225 fs_strip_url_protocol( $site_url )
1226 );
1227 }
1228
1229 $sites_list = '<ol>' . $sites_list . '</ol>';
1230 }
1231
1232 $remote_site_link = '<b>' . (1 === $total_sites ?
1233 sprintf(
1234 '<a href="%s" target="_blank">%s</a>',
1235 $site_urls[0],
1236 fs_strip_url_protocol( $site_urls[0] )
1237 ) :
1238 fs_text_inline( 'the above-mentioned sites', 'above-mentioned-sites' )) . '</b>';
1239
1240 $current_site_link = sprintf(
1241 '<b><a href="%s" target="_blank">%s</a></b>',
1242 $current_url,
1243 fs_strip_url_protocol( $current_url )
1244 );
1245
1246 $button_template = '<button class="button" data-clone-action="%s">%s</button>';
1247 $option_template = '<div class="fs-clone-resolution-option"><strong>%s</strong><p>%s</p><div>%s</div></div>';
1248
1249 $duplicate_option = sprintf(
1250 $option_template,
1251 fs_esc_html_inline( 'Is %2$s a duplicate of %4$s?', 'duplicate-site-confirmation-message' ),
1252 fs_esc_html_inline( 'Yes, %2$s is a duplicate of %4$s for the purpose of testing, staging, or development.', 'duplicate-site-message' ),
1253 ( $this->has_temporary_duplicate_mode_expired() ?
1254 sprintf(
1255 $button_template,
1256 'long_term_duplicate',
1257 fs_text_inline( 'Long-Term Duplicate', 'long-term-duplicate' )
1258 ) :
1259 sprintf(
1260 $button_template,
1261 'temporary_duplicate',
1262 fs_text_inline( 'Duplicate Website', 'duplicate-site' )
1263 ) )
1264 );
1265
1266 $migration_option = sprintf(
1267 $option_template,
1268 fs_esc_html_inline( 'Is %2$s the new home of %4$s?', 'migrate-site-confirmation-message' ),
1269 sprintf(
1270 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' ),
1271 ( $has_license ? fs_text_inline( 'license', 'license' ) : fs_text_inline( 'data', 'data' ) )
1272 ),
1273 sprintf(
1274 $button_template,
1275 'new_home',
1276 $has_license ?
1277 fs_text_inline( 'Migrate License', 'migrate-product-license' ) :
1278 fs_text_inline( 'Migrate', 'migrate-product-data' )
1279 )
1280 );
1281
1282 $new_website = sprintf(
1283 $option_template,
1284 fs_esc_html_inline( 'Is %2$s a new website?', 'new-site-confirmation-message' ),
1285 fs_esc_html_inline( 'Yes, %2$s is a new and different website that is separate from %4$s.', 'new-site-message' ) .
1286 ($is_premium ?
1287 ' ' . fs_text_inline( 'It requires license activation.', 'new-site-requires-license-activation-message' ) :
1288 ''
1289 ),
1290 sprintf(
1291 $button_template,
1292 'new_website',
1293 ( ! $is_premium || ! $has_license ) ?
1294 fs_text_inline( 'New Website', 'new-website' ) :
1295 fs_text_inline( 'Activate License', 'activate-license' )
1296 )
1297 );
1298
1299 $blog_id = get_current_blog_id();
1300
1301 /**
1302 * %1$s - single product's title or product titles list.
1303 * %2$s - site's URL.
1304 * %3$s - single install's URL or install URLs list.
1305 * %4$s - Clone site's link or "the above-mentioned sites" if there are multiple clone sites.
1306 */
1307 $message = sprintf(
1308 $notice_header .
1309 '<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 . '">' .
1310 $duplicate_option .
1311 $migration_option .
1312 $new_website . '</div>' .
1313 sprintf( '<div class="fs-clone-documentation-container">Unsure what to do? <a href="%s" target="_blank">Read more here</a>.</div>', $doc_url ),
1314 // %1$s
1315 ( 1 === $total_products ?
1316 sprintf( '<b>%s</b>', $product_titles[0] ) :
1317 ( 1 === $total_sites ?
1318 sprintf( '<div>%s</div>', $products_list ) :
1319 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 ) )
1320 ),
1321 // %2$s
1322 $current_site_link,
1323 // %3$s
1324 ( 1 === $total_sites ?
1325 $remote_site_link :
1326 $sites_list ),
1327 // %4$s
1328 $remote_site_link
1329 );
1330
1331 $this->_notices->add_sticky(
1332 $message,
1333 'clone_resolution_options_notice',
1334 '',
1335 'warn',
1336 true,
1337 null,
1338 null,
1339 true,
1340 // Intentionally not dismissible.
1341 false,
1342 array(
1343 'product_ids' => $product_ids,
1344 'blog_id' => $blog_id
1345 )
1346 );
1347 }
1348
1349 #endregion
1350
1351 #--------------------------------------------------------------------------------
1352 #region Temporary Duplicate (Short Term)
1353 #--------------------------------------------------------------------------------
1354
1355 /**
1356 * @author Leo Fajardo (@leorw)
1357 * @since 2.5.0
1358 *
1359 * @return string
1360 */
1361 private function get_temporary_duplicate_admin_notice_string(
1362 $site_urls,
1363 $product_titles,
1364 $module_label
1365 ) {
1366 $this->_logger->entrance();
1367
1368 $temporary_duplicate_end_date = $this->get_temporary_duplicate_expiration_timestamp();
1369 $temporary_duplicate_end_date = date( 'M j, Y', $temporary_duplicate_end_date );
1370
1371 $current_url = Freemius::get_unfiltered_site_url();
1372 $current_site_link = sprintf(
1373 '<b><a href="%s" target="_blank">%s</a></b>',
1374 $current_url,
1375 fs_strip_url_protocol( $current_url )
1376 );
1377
1378 $total_sites = count( $site_urls );
1379 $sites_list = '';
1380
1381 $total_products = count( $product_titles );
1382 $products_list = '';
1383
1384 if ( $total_sites > 1 ) {
1385 foreach ( $site_urls as $site_url ) {
1386 $sites_list .= sprintf(
1387 '<li><a href="%s" target="_blank">%s</a></li>',
1388 $site_url,
1389 fs_strip_url_protocol( $site_url )
1390 );
1391 }
1392
1393 $sites_list = '<ol class="fs-sites-list">' . $sites_list . '</ol>';
1394 }
1395
1396 if ( $total_products > 1 ) {
1397 foreach ( $product_titles as $product_title ) {
1398 $products_list .= sprintf( '<li>%s</li>', $product_title );
1399 }
1400
1401 $products_list = '<ol>' . $products_list . '</ol>';
1402 }
1403
1404 return sprintf(
1405 sprintf(
1406 '<div>%s</div>',
1407 ( 1 === $total_sites ?
1408 sprintf( '<p>%s</p>', fs_esc_html_inline( 'You marked this website, %s, as a temporary duplicate of %s.', 'temporary-duplicate-message' ) ) :
1409 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' )
1410 ) . '%s',
1411 $current_site_link,
1412 ( 1 === $total_sites ?
1413 sprintf(
1414 '<b><a href="%s" target="_blank">%s</a></b>',
1415 $site_urls[0],
1416 fs_strip_url_protocol( $site_urls[0] )
1417 ) :
1418 $sites_list ),
1419 sprintf(
1420 '<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>',
1421 esc_attr( admin_url( 'admin-ajax.php?_fs_network_admin=false', 'relative' ) ),
1422 sprintf(
1423 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' ),
1424 ( 1 === $total_products ?
1425 sprintf(
1426 fs_esc_html_x_inline( "The %s's", '"The <product_label>", e.g.: "The plugin"', 'the-product-x'),
1427 "<strong>{$module_label}</strong>"
1428 ) :
1429 fs_esc_html_inline( "The following products'", 'the-following-products' ) ),
1430 sprintf( '<strong>%s</strong>', $temporary_duplicate_end_date )
1431 ),
1432 ( 1 === $total_products ?
1433 '' :
1434 sprintf( '<div>%s</div>', $products_list )
1435 ),
1436 sprintf(
1437 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' ),
1438 sprintf( '<strong>%s</strong>', $temporary_duplicate_end_date),
1439 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' ) )
1440 )
1441 )
1442 );
1443 }
1444
1445 /**
1446 * Determines if the temporary duplicate mode has already expired.
1447 *
1448 * @return bool
1449 */
1450 function has_temporary_duplicate_mode_expired() {
1451 $temporary_duplicate_mode_start_timestamp = $this->was_temporary_duplicate_mode_selected() ?
1452 $this->get_option( 'temporary_duplicate_mode_selection_timestamp', true ) :
1453 $this->get_clone_identification_timestamp();
1454
1455 if ( ! is_numeric( $temporary_duplicate_mode_start_timestamp ) ) {
1456 return false;
1457 }
1458
1459 return ( time() > ( $temporary_duplicate_mode_start_timestamp + self::TEMPORARY_DUPLICATE_PERIOD ) );
1460 }
1461
1462 /**
1463 * Determines if the logged-in WordPress user manually selected the temporary duplicate mode for the site.
1464 *
1465 * @return bool
1466 */
1467 function was_temporary_duplicate_mode_selected() {
1468 return is_numeric( $this->temporary_duplicate_mode_selection_timestamp );
1469 }
1470
1471 /**
1472 * Stores the time when the logged-in WordPress user selected the temporary duplicate mode for the site.
1473 */
1474 private function store_temporary_duplicate_timestamp() {
1475 $this->temporary_duplicate_mode_selection_timestamp = time();
1476 }
1477
1478 /**
1479 * Removes the notice that is shown when the logged-in WordPress user has selected the temporary duplicate mode for the site.
1480 *
1481 * @param bool $store
1482 */
1483 function remove_clone_resolution_options_notice( $store = true ) {
1484 $this->_notices->remove_sticky( 'clone_resolution_options_notice', true, $store );
1485 }
1486
1487 /**
1488 * Removes the notice that is shown when the logged-in WordPress user has selected the temporary duplicate mode for the site.
1489 *
1490 * @param bool $store
1491 */
1492 function remove_temporary_duplicate_notice( $store = true ) {
1493 $this->_notices->remove_sticky( 'temporary_duplicate_notice', true, $store );
1494 }
1495
1496 /**
1497 * Determines if the manual clone resolution options notice is currently being shown.
1498 *
1499 * @return bool
1500 */
1501 function is_clone_resolution_options_notice_shown() {
1502 return $this->_notices->has_sticky( 'clone_resolution_options_notice', true );
1503 }
1504
1505 /**
1506 * Determines if the temporary duplicate notice is currently being shown.
1507 *
1508 * @return bool
1509 */
1510 function is_temporary_duplicate_notice_shown() {
1511 return $this->_notices->has_sticky( 'temporary_duplicate_notice', true );
1512 }
1513
1514 /**
1515 * Determines if a site was marked as a temporary duplicate and if it's still a temporary duplicate.
1516 *
1517 * @return bool
1518 */
1519 function is_temporary_duplicate_by_blog_id( $blog_id ) {
1520 $timestamp = $this->get_option( 'temporary_duplicate_mode_selection_timestamp', false, $blog_id );
1521
1522 return (
1523 is_numeric( $timestamp ) &&
1524 time() < ( $timestamp + self::TEMPORARY_DUPLICATE_PERIOD )
1525 );
1526 }
1527
1528 /**
1529 * Determines the last time the temporary duplicate notice was shown.
1530 *
1531 * @return int|null
1532 */
1533 function last_time_temporary_duplicate_notice_was_shown() {
1534 return $this->temporary_duplicate_notice_shown_timestamp;
1535 }
1536
1537 /**
1538 * Clears the time that has been stored when the temporary duplicate notice was shown.
1539 */
1540 function clear_temporary_duplicate_notice_shown_timestamp() {
1541 unset( $this->temporary_duplicate_notice_shown_timestamp );
1542 }
1543
1544 /**
1545 * Adds a temporary duplicate notice that provides the logged-in WordPress user with an option to activate a license for the site.
1546 *
1547 * @param number[] $product_ids
1548 * @param string $message
1549 * @param string|null $plugin_title
1550 */
1551 function add_temporary_duplicate_sticky_notice(
1552 $product_ids,
1553 $message,
1554 $plugin_title = null
1555 ) {
1556 $this->_logger->entrance();
1557
1558 $this->_notices->add_sticky(
1559 $message,
1560 'temporary_duplicate_notice',
1561 '',
1562 'promotion',
1563 true,
1564 null,
1565 $plugin_title,
1566 true,
1567 true,
1568 array(
1569 'product_ids' => $product_ids,
1570 'blog_id' => get_current_blog_id()
1571 )
1572 );
1573
1574 $this->temporary_duplicate_notice_shown_timestamp = time();
1575 }
1576
1577 #endregion
1578
1579 /**
1580 * @author Leo Fajardo
1581 * @since 2.5.0
1582 *
1583 * @param string $key
1584 *
1585 * @return bool
1586 */
1587 private function should_use_network_storage( $key ) {
1588 return ( 'new_blog_install_map' === $key );
1589 }
1590
1591 /**
1592 * @param string $key
1593 * @param number|null $blog_id
1594 *
1595 * @return FS_Option_Manager
1596 */
1597 private function get_storage( $key, $blog_id = null ) {
1598 if ( is_numeric( $blog_id ) ){
1599 return FS_Option_Manager::get_manager( WP_FS___OPTION_PREFIX . self::OPTION_MANAGER_NAME, true, $blog_id );
1600 }
1601
1602 return $this->should_use_network_storage( $key ) ?
1603 $this->_network_storage :
1604 $this->_storage;
1605 }
1606
1607 /**
1608 * @param string $name
1609 * @param bool $flush
1610 * @param number|null $blog_id
1611 *
1612 * @return mixed
1613 */
1614 private function get_option( $name, $flush = false, $blog_id = null ) {
1615 return $this->get_storage( $name, $blog_id )->get_option( $name, null, $flush );
1616 }
1617
1618 #--------------------------------------------------------------------------------
1619 #region Magic methods
1620 #--------------------------------------------------------------------------------
1621
1622 /**
1623 * @param string $name
1624 * @param int|string $value
1625 */
1626 function __set( $name, $value ) {
1627 $this->get_storage( $name )->set_option( $name, $value, true );
1628 }
1629
1630 /**
1631 * @param string $name
1632 *
1633 * @return bool
1634 */
1635 function __isset( $name ) {
1636 return $this->get_storage( $name )->has_option( $name, true );
1637 }
1638
1639 /**
1640 * @param string $name
1641 */
1642 function __unset( $name ) {
1643 $this->get_storage( $name )->unset_option( $name, true );
1644 }
1645
1646 /**
1647 * @param string $name
1648 *
1649 * @return null|int|string
1650 */
1651 function __get( $name ) {
1652 return $this->get_option(
1653 $name,
1654 // Reload storage from DB when accessing request_handler_* options to avoid race conditions.
1655 fs_starts_with( $name, 'request_handler' )
1656 );
1657 }
1658
1659 #endregion
1660 }
1661