PluginProbe
WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards / 5.5.3
WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards v5.5.3
5.5.83 5.5.82 5.5.81 5.5.80 5.5.79 5.5.77 5.5.76 5.5.75 5.5.73 5.5.72 5.5.22 5.5.23 5.5.29 5.5.3 5.5.31 5.5.32 5.5.34 5.5.35 5.5.36 5.5.37 5.5.4 5.5.40 5.5.41 5.5.42 5.5.43 All 159 releases
wp-data-access / freemius / includes / class-freemius.php

class-freemius.php in WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards 5.5.3, at freemius/includes/class-freemius.php

26,465 lines 968.0 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 * @since 1.0.3
7 */
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 // "final class"
13 class Freemius extends Freemius_Abstract {
14 /**
15 * SDK Version
16 *
17 * @var string
18 */
19 public $version = WP_FS__SDK_VERSION;
20
21 #region Plugin Info
22
23 /**
24 * @since 1.0.1
25 *
26 * @var string
27 */
28 private $_slug;
29
30 /**
31 * @since 1.0.0
32 *
33 * @var string
34 */
35 private $_plugin_basename;
36 /**
37 * @since 2.2.1
38 *
39 * @var string
40 */
41 private $_premium_plugin_basename;
42 /**
43 * @since 1.0.0
44 *
45 * @var string
46 */
47 private $_free_plugin_basename;
48 /**
49 * @since 1.0.0
50 *
51 * @var string
52 */
53 private $_plugin_dir_path;
54 /**
55 * @since 1.0.0
56 *
57 * @var string
58 */
59 private $_plugin_dir_name;
60 /**
61 * @since 1.0.0
62 *
63 * @var string
64 */
65 private $_plugin_main_file_path;
66 /**
67 * @var string[]
68 */
69 private $_plugin_data;
70 /**
71 * @since 1.0.9
72 *
73 * @var string
74 */
75 private $_plugin_name;
76 /**
77 * @since 1.2.2
78 *
79 * @var string
80 */
81 private $_module_type;
82
83 #endregion Plugin Info
84
85 /**
86 * @since 1.0.9
87 *
88 * @var bool If false, don't turn Freemius on.
89 */
90 private $_is_on;
91
92 /**
93 * @since 1.1.3
94 *
95 * @var bool If false, don't turn Freemius on.
96 */
97 private $_is_anonymous;
98
99 /**
100 * @since 1.0.9
101 * @var bool If false, issues with connectivity to Freemius API.
102 */
103 private $_has_api_connection;
104
105 /**
106 * @since 1.0.9
107 * @since 2.0.0 Default to true since we need the property during the instance construction, prior to the dynamic_init() execution.
108 * @var bool Hints the SDK if plugin can support anonymous mode (if skip connect is visible).
109 */
110 private $_enable_anonymous = true;
111
112 /**
113 * @since 1.1.7.5
114 * @var bool Hints the SDK if plugin should run in anonymous mode (only adds feedback form).
115 */
116 private $_anonymous_mode;
117
118 /**
119 * @since 1.1.9
120 * @var bool Hints the SDK if plugin have any free plans.
121 */
122 private $_is_premium_only;
123
124 /**
125 * @since 1.2.1.6
126 * @var bool Hints the SDK if plugin have premium code version at all.
127 */
128 private $_has_premium_version;
129
130 /**
131 * @since 1.2.1.6
132 * @var bool Hints the SDK if plugin should ignore pending mode by simulating a skip.
133 */
134 private $_ignore_pending_mode;
135
136 /**
137 * @since 1.0.8
138 * @var bool Hints the SDK if the plugin has any paid plans.
139 */
140 private $_has_paid_plans;
141
142 /**
143 * @since 1.2.1.5
144 * @var int Hints the SDK if the plugin offers a trial period. If negative, no trial, if zero - has a trial but
145 * without a specified period, if positive - the number of trial days.
146 */
147 private $_trial_days = - 1;
148
149 /**
150 * @since 1.2.1.5
151 * @var bool Hints the SDK if the trial requires a payment method or not.
152 */
153 private $_is_trial_require_payment = false;
154
155 /**
156 * @since 1.0.7
157 * @var bool Hints the SDK if the plugin is WordPress.org compliant.
158 */
159 private $_is_org_compliant;
160
161 /**
162 * @since 1.0.7
163 * @var bool Hints the SDK if the plugin is has add-ons.
164 */
165 private $_has_addons;
166
167 /**
168 * @since 2.4.5
169 * @var string Navigation type: 'menu' or 'tabs'.
170 */
171 private $_navigation;
172
173 const NAVIGATION_MENU = 'menu';
174 const NAVIGATION_TABS = 'tabs';
175
176 /**
177 * @since 1.1.6
178 * @var string[]bool.
179 */
180 private $_permissions;
181
182 /**
183 * @var FS_Storage
184 */
185 private $_storage;
186
187 /**
188 * @since 1.2.2.7
189 * @var FS_Cache_Manager
190 */
191 private $_cache;
192
193 /**
194 * @since 1.0.0
195 *
196 * @var FS_Logger
197 */
198 private $_logger;
199 /**
200 * @since 1.0.4
201 *
202 * @var FS_Plugin
203 */
204 private $_plugin = false;
205 /**
206 * @since 1.0.4
207 *
208 * @var FS_Plugin|false
209 */
210 private $_parent_plugin = false;
211 /**
212 * @since 1.1.1
213 *
214 * @var Freemius
215 */
216 private $_parent = false;
217 /**
218 * @since 1.0.1
219 *
220 * @var FS_User
221 */
222 private $_user = false;
223 /**
224 * @since 1.0.1
225 *
226 * @var FS_Site
227 */
228 private $_site = false;
229 /**
230 * @since 1.0.1
231 *
232 * @var FS_Plugin_License
233 */
234 private $_license;
235 /**
236 * @since 1.0.2
237 *
238 * @var FS_Plugin_Plan[]
239 */
240 private $_plans = false;
241 /**
242 * @var FS_Plugin_License[]
243 * @since 1.0.5
244 */
245 private $_licenses = false;
246
247 /**
248 * @since 1.0.1
249 *
250 * @var FS_Admin_Menu_Manager
251 */
252 private $_menu;
253
254 /**
255 * @var FS_Admin_Notices
256 */
257 private $_admin_notices;
258
259 /**
260 * @since 1.1.6
261 *
262 * @var FS_Admin_Notices
263 */
264 private static $_global_admin_notices;
265
266 /**
267 * @var FS_Logger
268 * @since 1.0.0
269 */
270 private static $_static_logger;
271
272 /**
273 * @var FS_Options
274 * @since 1.0.2
275 */
276 private static $_accounts;
277
278 /**
279 * @since 1.2.2
280 *
281 * @var number
282 */
283 private $_module_id;
284
285 /**
286 * @var Freemius[]
287 */
288 private static $_instances = array();
289
290 /**
291 * @since 1.2.3
292 *
293 * @var FS_Affiliate
294 */
295 private $affiliate = null;
296
297 /**
298 * @since 1.2.3
299 *
300 * @var FS_AffiliateTerms
301 */
302 private $plugin_affiliate_terms = null;
303
304 /**
305 * @since 1.2.3
306 *
307 * @var FS_AffiliateTerms
308 */
309 private $custom_affiliate_terms = null;
310
311 /**
312 * @since 2.0.0
313 *
314 * @var bool
315 */
316 private $_is_multisite_integrated;
317
318 /**
319 * @since 2.0.0
320 *
321 * @var bool True if the current request is for a network admin screen and the plugin is network active.
322 */
323 private $_is_network_active;
324
325 /**
326 * @since 2.0.0
327 *
328 * @var int|null The original blog ID the plugin was loaded with.
329 */
330 private $_blog_id = null;
331
332 /**
333 * @since 2.0.0
334 *
335 * @var int|null The current execution context. When true, run on network context. When int, run on the specified blog context.
336 */
337 private $_context_is_network_or_blog_id = null;
338
339 /**
340 * @since 2.0.0
341 *
342 * @var string
343 */
344 private $_dynamically_added_top_level_page_hook_name = '';
345
346 /**
347 * @author Leo Fajardo (@leorw)
348 * @since 2.3.1
349 *
350 * @var bool
351 */
352 private $is_whitelabeled;
353
354 /**
355 * @author Leo Fajardo (@leorw)
356 * @since 2.4.0
357 *
358 * @var bool
359 */
360 private $_is_bundle_license_auto_activation_enabled = false;
361
362 #region Uninstall Reasons IDs
363
364 const REASON_NO_LONGER_NEEDED = 1;
365 const REASON_FOUND_A_BETTER_PLUGIN = 2;
366 const REASON_NEEDED_FOR_A_SHORT_PERIOD = 3;
367 const REASON_BROKE_MY_SITE = 4;
368 const REASON_SUDDENLY_STOPPED_WORKING = 5;
369 const REASON_CANT_PAY_ANYMORE = 6;
370 const REASON_OTHER = 7;
371 const REASON_DIDNT_WORK = 8;
372 const REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION = 9;
373 const REASON_COULDNT_MAKE_IT_WORK = 10;
374 const REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE = 11;
375 const REASON_NOT_WORKING = 12;
376 const REASON_NOT_WHAT_I_WAS_LOOKING_FOR = 13;
377 const REASON_DIDNT_WORK_AS_EXPECTED = 14;
378 const REASON_TEMPORARY_DEACTIVATION = 15;
379
380 #endregion
381
382 /**
383 * @author Leo Fajardo (@leorw)
384 * @since 2.3.1
385 *
386 * @var boolean|null
387 */
388 private $_use_external_pricing = null;
389 /**
390 * @author Leo Fajardo (@leorw)
391 * @since 2.4.2
392 *
393 * @var string|null
394 */
395 private $_pricing_js_path = null;
396
397 const VERSION_MAX_CHARS = 16;
398 const LANGUAGE_MAX_CHARS = 8;
399
400 /* Ctor
401 ------------------------------------------------------------------------------------------------------------------*/
402
403 /**
404 * Main singleton instance.
405 *
406 * @author Vova Feldman (@svovaf)
407 * @since 1.0.0
408 *
409 * @param number $module_id
410 * @param string|bool $slug
411 * @param bool $is_init Since 1.2.1 Is initiation sequence.
412 */
413 private function __construct( $module_id, $slug = false, $is_init = false ) {
414 $main_file = false;
415
416 if ( $is_init && is_numeric( $module_id ) && is_string( $slug ) ) {
417 $main_file = $this->store_id_slug_type_path_map( $module_id, $slug );
418 }
419
420 $this->_module_id = $module_id;
421 $this->_slug = $this->get_slug();
422 $this->_module_type = $this->get_module_type();
423
424 $this->_blog_id = is_multisite() ? get_current_blog_id() : null;
425
426 $this->_storage = FS_Storage::instance( $this->_module_type, $this->_slug );
427
428 // If not set or 24 hours have already passed from the last time it's set, set the last load timestamp to the current time.
429 if (
430 ! isset( $this->_storage->last_load_timestamp ) ||
431 $this->_storage->last_load_timestamp < ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC ) )
432 ) {
433 $this->_storage->last_load_timestamp = time();
434 }
435
436 $this->_cache = FS_Cache_Manager::get_manager( WP_FS___OPTION_PREFIX . "cache_{$module_id}" );
437
438 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $this->get_unique_affix(), WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
439
440 $this->_plugin_main_file_path = $this->_find_caller_plugin_file( $is_init, $main_file );
441 $this->_plugin_dir_path = plugin_dir_path( $this->_plugin_main_file_path );
442 $this->_plugin_basename = $this->get_plugin_basename();
443 $this->_free_plugin_basename = str_replace( '-premium/', '/', $this->_plugin_basename );
444
445 $this->_is_multisite_integrated = (
446 defined( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) &&
447 ( true === constant( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) )
448 );
449
450 $this->_is_network_active = (
451 is_multisite() &&
452 $this->_is_multisite_integrated &&
453 // Themes are always network activated, but the ACTUAL activation is per site.
454 $this->is_plugin() &&
455 (
456 is_plugin_active_for_network( $this->_plugin_basename ) ||
457 // Plugin network level activation or uninstall.
458 ( fs_is_network_admin() && is_plugin_inactive( $this->_plugin_basename ) )
459 )
460 );
461
462 $this->_storage->set_network_active(
463 $this->_is_network_active,
464 $this->is_delegated_connection()
465 );
466
467 if ( ! isset( $this->_storage->is_network_activated ) ) {
468 $this->_storage->is_network_activated = $this->_is_network_active;
469 }
470
471 if ( $this->_storage->is_network_activated != $this->_is_network_active ) {
472 // Update last activation level.
473 $this->_storage->is_network_activated = $this->_is_network_active;
474
475 $this->maybe_adjust_storage();
476 }
477
478 #region Migration
479
480 if ( is_multisite() ) {
481 /**
482 * If the install_timestamp exists on the site level but doesn't exist on the
483 * network level storage, it means that we need to process the storage with migration.
484 *
485 * The code in this `if` scope will only be executed once and only for the first site that will execute it because once we migrate the storage data, install_timestamp will be already set in the network level storage.
486 *
487 * @author Vova Feldman (@svovaf)
488 * @since 2.0.0
489 */
490 if ( false === $this->_storage->get( 'install_timestamp', false, true ) &&
491 false !== $this->_storage->get( 'install_timestamp', false, false )
492 ) {
493 // Initiate storage migration.
494 $this->_storage->migrate_to_network();
495
496 // Migrate module cache to network level storage.
497 $this->_cache->migrate_to_network();
498 }
499 }
500
501 #endregion
502
503 $base_name_split = explode( '/', $this->_plugin_basename );
504 $this->_plugin_dir_name = $base_name_split[0];
505
506 if ( $this->_logger->is_on() ) {
507 $this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
508 $this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
509 $this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
510 $this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
511 $this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
512 }
513
514 // Remember link between file to slug.
515 $this->store_file_slug_map();
516
517 // Store plugin's initial install timestamp.
518 if ( ! isset( $this->_storage->install_timestamp ) ) {
519 $this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
520 }
521
522 if ( ! is_object( $this->_plugin ) ) {
523 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
524 }
525
526 $this->_admin_notices = FS_Admin_Notices::instance(
527 $this->_slug . ( $this->is_theme() ? ':theme' : '' ),
528 /**
529 * Ensure that the admin notice will always have a title by using the stored plugin title if available and
530 * retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
531 *
532 * @author Leo Fajardo (@leorw)
533 * @since 1.2.2
534 */
535 ( is_object( $this->_plugin ) && isset( $this->_plugin->title ) ?
536 $this->_plugin->title :
537 $this->get_plugin_name()
538 ),
539 $this->get_unique_affix()
540 );
541
542 if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
543 fs_request_is_action( 'restart_freemius' )
544 ) {
545 FS_Api::clear_cache();
546 $this->_cache->clear();
547 }
548
549 $this->register_constructor_hooks();
550
551 /**
552 * Starting from version 2.0.0, `FS_Site` entities no longer have the `plan` property and have `plan_id`
553 * instead. This should be called before calling `_load_account()`, otherwise, `$this->_site` will not be
554 * loaded in `_load_account` for versions of SDK starting from 2.0.0.
555 *
556 * @author Leo Fajardo (@leorw)
557 */
558 self::migrate_install_plan_to_plan_id( $this->_storage );
559
560 $this->_load_account();
561
562 $this->_version_updates_handler();
563 }
564
565 /**
566 * @author Leo Fajardo (@leorw)
567 * @since 2.3.0
568 */
569 private function maybe_adjust_storage() {
570 $install_timestamp = null;
571 $prev_is_premium = null;
572
573 $options_to_update = array();
574
575 $is_network_admin = fs_is_network_admin();
576
577 $network_install_timestamp = $this->_storage->get( 'install_timestamp', null, true );
578
579 if ( ! $is_network_admin ) {
580 if ( is_null( $network_install_timestamp ) ) {
581 // Plugin was not network-activated before.
582 return;
583 }
584
585 if ( is_null( $this->_storage->get( 'install_timestamp', null, false ) ) ) {
586 // Set the `install_timestamp` only if it's not yet set.
587 $install_timestamp = $network_install_timestamp;
588 }
589
590 $prev_is_premium = $this->_storage->get( 'prev_is_premium', null, true );
591 } else {
592 $current_wp_user = self::_get_current_wp_user();
593 $current_fs_user = self::_get_user_by_email( $current_wp_user->user_email );
594 $network_user_info = array();
595
596 $skips_count = 0;
597
598 $sites = self::get_sites();
599 $sites_count = count( $sites );
600
601 $blog_id_2_install_map = array();
602
603 $is_first_non_ignored_blog = true;
604
605 foreach ( $sites as $site ) {
606 $blog_id = self::get_site_blog_id( $site );
607
608 $blog_install_timestamp = $this->_storage->get( 'install_timestamp', null, $blog_id );
609
610 if ( is_null( $blog_install_timestamp ) ) {
611 // Plugin has not been installed on this blog.
612 continue;
613 }
614
615 $is_earlier_install = (
616 ! is_null( $install_timestamp ) &&
617 $blog_install_timestamp < $install_timestamp
618 );
619
620 $install = $this->get_install_by_blog_id( $blog_id );
621
622 $update_network_user_info = false;
623
624 if ( ! is_object( $install ) ) {
625 if ( ! $this->_storage->get( 'is_anonymous', false, $blog_id ) ) {
626 // The opt-in decision (whether to skip or opt in) is yet to be made.
627 continue;
628 }
629
630 $skips_count ++;
631 } else {
632 $blog_id_2_install_map[ $blog_id ] = $install;
633
634 if ( empty( $network_user_info ) ) {
635 // Set the network user info for the 1st time. Choose any user information whether or not it is for the current WP user.
636 $update_network_user_info = true;
637 }
638
639 if ( ! $update_network_user_info &&
640 is_object( $current_fs_user ) &&
641 $network_user_info['user_id'] != $current_fs_user->id &&
642 $install->user_id == $current_fs_user->id
643 ) {
644 // If an install that is owned by the current WP user is found, use its user information instead.
645 $update_network_user_info = true;
646 }
647
648 if ( ! $update_network_user_info &&
649 $is_earlier_install &&
650 ( ! is_object( $current_fs_user ) || $current_fs_user->id == $install->user_id )
651 ) {
652 // Update to the earliest install info if there's no install found so far that is owned by the current WP user; OR only if the found install is owned by the current WP user.
653 $update_network_user_info = true;
654 }
655 }
656
657 if ( $update_network_user_info ) {
658 $network_user_info = array(
659 'user_id' => $install->user_id,
660 'blog_id' => $blog_id
661 );
662 }
663
664 $site_prev_is_premium = $this->_storage->get( 'prev_is_premium', null, $blog_id );
665
666 if ( $is_first_non_ignored_blog ) {
667 $prev_is_premium = $site_prev_is_premium;
668
669 if ( is_null( $network_install_timestamp ) ) {
670 $install_timestamp = $blog_install_timestamp;
671 }
672
673 $is_first_non_ignored_blog = false;
674
675 continue;
676 }
677
678 if ( ! is_null( $prev_is_premium ) && $prev_is_premium !== $site_prev_is_premium ) {
679 // If a different `$site_prev_is_premium` value is found, do not include the option in the collection of options to update.
680 $prev_is_premium = null;
681 }
682
683 if ( $is_earlier_install ) {
684 // If an earlier install timestamp is found.
685 $install_timestamp = $blog_install_timestamp;
686 }
687 }
688
689 $installs_count = count( $blog_id_2_install_map );
690
691 if ( $sites_count === ( $installs_count + $skips_count ) ) {
692 if ( ! empty( $network_user_info ) ) {
693 $options_to_update['network_user_id'] = $network_user_info['user_id'];
694 $options_to_update['network_install_blog_id'] = $network_user_info['blog_id'];
695
696 foreach ( $blog_id_2_install_map as $blog_id => $install ) {
697 if ( $install->user_id == $network_user_info['user_id'] ) {
698 continue;
699 }
700
701 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
702 }
703 }
704
705 if ( $sites_count === $skips_count ) {
706 /**
707 * Assume network-level skipping as the intended action if all actions identified were only
708 * skipping of the connection (i.e., no opt-ins and delegated connections so far).
709 */
710 $options_to_update['is_anonymous_ms'] = true;
711 } else if ( $sites_count === $installs_count ) {
712 /**
713 * Assume network-level opt-in as the intended action if all actions identified were only opt-ins
714 * (i.e., no delegation and skipping of the connections so far).
715 */
716 $options_to_update['is_network_connected'] = true;
717 }
718 }
719 }
720
721 if ( ! is_null( $install_timestamp ) ) {
722 $options_to_update['install_timestamp'] = $install_timestamp;
723 }
724
725 if ( ! is_null( $prev_is_premium ) ) {
726 $options_to_update['prev_is_premium'] = $prev_is_premium;
727 }
728
729 if ( ! empty( $options_to_update ) ) {
730 $this->adjust_storage( $options_to_update, $is_network_admin );
731 }
732 }
733
734 /**
735 * @author Leo Fajardo (@leorw)
736 * @since 2.3.0
737 *
738 * @param array $options
739 * @param bool $is_network_admin
740 */
741 private function adjust_storage( $options, $is_network_admin ) {
742 foreach ( $options as $name => $value ) {
743 $this->_storage->store( $name, $value, $is_network_admin ? true : null );
744 }
745 }
746
747 /**
748 * Checks whether this module has a settings menu.
749 *
750 * @author Leo Fajardo (@leorw)
751 * @since 1.2.2
752 *
753 * @return bool
754 */
755 function has_settings_menu() {
756 return ( $this->_is_network_active && fs_is_network_admin() ) ?
757 $this->_menu->has_network_menu() :
758 $this->_menu->has_menu();
759 }
760
761 /**
762 * If `true` the opt-in should be shown as a modal dialog box on the themes.php page. WordPress.org themes guidelines prohibit from redirecting the user from the themes.php page after activating a theme.
763 *
764 * @author Vova Feldman (@svovaf)
765 * @since 2.4.5
766 *
767 * @return bool
768 */
769 function show_opt_in_on_themes_page() {
770 if ( ! $this->is_free_wp_org_theme() ) {
771 return false;
772 }
773
774 if ( ! $this->has_settings_menu() ) {
775 return true;
776 }
777
778 return $this->show_settings_with_tabs();
779 }
780
781 /**
782 * If `true` the opt-in should be shown on the product's main setting page.
783 *
784 * @author Vova Feldman (@svovaf)
785 * @since 2.4.5
786 *
787 * @return bool
788 *
789 * @uses show_opt_in_on_themes_page();
790 */
791 function show_opt_in_on_setting_page() {
792 return ! $this->show_opt_in_on_themes_page();
793 }
794
795 /**
796 * If `true` the settings should be shown using tabs.
797 *
798 * @author Vova Feldman (@svovaf)
799 * @since 2.4.5
800 *
801 * @return bool
802 */
803 function show_settings_with_tabs() {
804 return ( self::NAVIGATION_TABS === $this->_navigation );
805 }
806
807 /**
808 * Check if the context module is free wp.org theme.
809 *
810 * This method is helpful because:
811 * 1. wp.org themes are limited to a single submenu item,
812 * and sub-submenu items are most likely not allowed (never verified).
813 * 2. wp.org themes are not allowed to redirect the user
814 * after the theme activation, therefore, the agreed UX
815 * is showing the opt-in as a modal dialog box after
816 * activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
817 *
818 * @author Vova Feldman (@svovaf)
819 * @since 1.2.2.7
820 *
821 * @return bool
822 */
823 function is_free_wp_org_theme() {
824 return (
825 $this->is_theme() &&
826 $this->is_org_repo_compliant() &&
827 ! $this->is_premium()
828 );
829 }
830
831 /**
832 * Checks whether this a submenu item is visible.
833 *
834 * @author Vova Feldman (@svovaf)
835 * @since 1.2.2.6
836 * @since 1.2.2.7 Even if the menu item was specified to be hidden, when it is the context page, then show the submenu item so the user will have the right context page.
837 *
838 * @param string $slug
839 * @param bool $is_tabs_visibility_check This is used to decide if the associated tab should be shown or hidden.
840 *
841 * @return bool
842 */
843 function is_submenu_item_visible( $slug, $is_tabs_visibility_check = false ) {
844 if ( $this->is_admin_page( $slug ) ) {
845 /**
846 * It is the current context page, so show the submenu item
847 * so the user will have the right context page, even if it
848 * was set to hidden.
849 */
850 return true;
851 }
852
853 if ( ! $this->has_settings_menu() ) {
854 // No menu settings at all.
855 return false;
856 }
857
858 if (
859 ! $is_tabs_visibility_check &&
860 $this->is_org_repo_compliant() &&
861 $this->show_settings_with_tabs()
862 ) {
863 /**
864 * wp.org themes are limited to a single submenu item, and
865 * sub-submenu items are most likely not allowed (never verified).
866 */
867 return false;
868 }
869
870 return $this->_menu->is_submenu_item_visible( $slug );
871 }
872
873 /**
874 * Check if a Freemius page should be accessible via the UI.
875 *
876 * @author Vova Feldman (@svovaf)
877 * @since 1.2.2.7
878 *
879 * @param string $slug
880 *
881 * @return bool
882 */
883 function is_page_visible( $slug ) {
884 if ( $this->is_admin_page( $slug ) ) {
885 return true;
886 }
887
888 return $this->_menu->is_submenu_item_visible( $slug, true, true );
889 }
890
891 /**
892 * @author Vova Feldman (@svovaf)
893 * @since 1.0.9
894 */
895 private function _version_updates_handler() {
896 if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
897 // Freemius version upgrade mode.
898 $this->_storage->sdk_last_version = $this->_storage->sdk_version;
899 $this->_storage->sdk_version = $this->version;
900
901 if ( empty( $this->_storage->sdk_last_version ) ||
902 version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
903 ) {
904 $this->_storage->sdk_upgrade_mode = true;
905 $this->_storage->sdk_downgrade_mode = false;
906 } else {
907 $this->_storage->sdk_downgrade_mode = true;
908 $this->_storage->sdk_upgrade_mode = false;
909
910 }
911
912 $this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
913 }
914
915 $plugin_version = $this->get_plugin_version();
916 if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
917 // Plugin version upgrade mode.
918 $this->_storage->plugin_last_version = $this->_storage->plugin_version;
919 $this->_storage->plugin_version = $plugin_version;
920
921 if ( empty( $this->_storage->plugin_last_version ) ||
922 version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
923 ) {
924 $this->_storage->plugin_upgrade_mode = true;
925 $this->_storage->plugin_downgrade_mode = false;
926 } else {
927 $this->_storage->plugin_downgrade_mode = true;
928 $this->_storage->plugin_upgrade_mode = false;
929 }
930
931 if ( ! empty( $this->_storage->plugin_last_version ) ) {
932 // Different version of the plugin was installed before, therefore it's an update.
933 $this->_storage->is_plugin_new_install = false;
934 }
935
936 $this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
937 }
938 }
939
940 #--------------------------------------------------------------------------------
941 #region Data Migration on SDK Update
942 #--------------------------------------------------------------------------------
943
944 /**
945 * @author Vova Feldman (@svovaf)
946 * @since 1.1.5
947 *
948 * @param string $sdk_prev_version
949 * @param string $sdk_version
950 */
951 function _sdk_version_update( $sdk_prev_version, $sdk_version ) {
952 if ( empty( $sdk_prev_version ) ) {
953 return;
954 }
955
956 if (
957 version_compare( $sdk_prev_version, '2.5.1', '<' ) &&
958 version_compare( $sdk_version, '2.5.1', '>=' )
959 ) {
960 if ( $this->is_registered( true ) ) {
961 /**
962 * Migrate to new permissions layer.
963 */
964 require_once WP_FS__DIR_INCLUDES . '/supplements/fs-migration-2.5.1.php';
965
966 $install_by_blog_id = is_multisite() ?
967 $this->get_blog_install_map() :
968 array( 0 => $this->_site );
969
970 fs_migrate_251( $this, $install_by_blog_id );
971 }
972 }
973 }
974
975 /**
976 * @author Leo Fajardo (@leorw)
977 * @since 2.0.0
978 *
979 * @param \FS_Storage $storage
980 * @param bool|int|null $blog_id
981 */
982 private static function migrate_install_plan_to_plan_id( FS_Storage $storage, $blog_id = null ) {
983 if ( empty( $storage->sdk_version ) ) {
984 // New installation of the plugin, no need to upgrade.
985 return;
986 }
987
988 if ( ! version_compare( $storage->sdk_version, '2.0.0', '<' ) ) {
989 // Previous version is >= 2.0.0, so no need to migrate.
990 return;
991 }
992
993 // Alias.
994 $module_type = $storage->get_module_type();
995 $module_slug = $storage->get_module_slug();
996
997 $installs = self::get_all_sites( $module_type, $blog_id );
998 $install = isset( $installs[ $module_slug ] ) ? $installs[ $module_slug ] : null;
999
1000 if ( ! is_object( $install ) ) {
1001 return;
1002 }
1003
1004 if ( isset( $install->plan ) && is_object( $install->plan ) ) {
1005 if ( isset( $install->plan->id ) && ! empty( $install->plan->id ) ) {
1006 $install->plan_id = self::_decrypt( $install->plan->id );
1007 }
1008
1009 unset( $install->plan );
1010
1011 $installs[ $module_slug ] = clone $install;
1012
1013 self::set_account_option_by_module(
1014 $module_type,
1015 'sites',
1016 $installs,
1017 true,
1018 $blog_id
1019 );
1020 }
1021 }
1022
1023 /**
1024 * @author Vova Feldman (@svovaf)
1025 * @since 1.2.2.7
1026 *
1027 * @param string $plugin_prev_version
1028 * @param string $plugin_version
1029 */
1030 function _after_version_update( $plugin_prev_version, $plugin_version ) {
1031 if ( $this->is_theme() ) {
1032 // Expire the cache of the previous tabs since the theme may
1033 // have setting updates.
1034 $this->_cache->expire( 'tabs' );
1035 $this->_cache->expire( 'tabs_stylesheets' );
1036 }
1037 }
1038
1039 /**
1040 * A special migration logic for the $_accounts, executed for all the plugins in the system:
1041 * - Moves some data to the network level storage.
1042 * - If the plugin's connection was skipped for all sites, set the plugin as if it was network skipped.
1043 * - If the plugin's connection was ignored for all sites, don't do anything in terms of the network connection.
1044 * - If the plugin was connected to all sites by the same super-admin, set the plugin as if was network opted-in for all sites.
1045 * - If there's at least one site that was connected by a super-admin, find the "main super-admin" (the one that installed the majority of the plugin installs) and set the plugin as if was network activated with the main super-admin, set all the sites that were skipped or opted-in with a different user to delegated mode. Then, prompt the currently logged super-admin to choose what to do with the ignored sites.
1046 * - If there are any sites in the network which the connection decision was not yet taken for, set this plugin into network activation mode so a super-admin can choose what to do with the rest of the sites.
1047 *
1048 * @author Vova Feldman (@svovaf)
1049 * @since 2.0.0
1050 */
1051 private static function migrate_accounts_to_network() {
1052 $sites = self::get_sites();
1053 $sites_count = count( $sites );
1054 $connection_status = array();
1055 $plugin_slugs = array();
1056 foreach ( $sites as $site ) {
1057 $blog_id = self::get_site_blog_id( $site );
1058
1059 self::$_accounts->migrate_to_network( $blog_id );
1060
1061 /**
1062 * Build a list of all Freemius powered plugins slugs.
1063 */
1064 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array(), $blog_id );
1065 foreach ( $id_slug_type_path_map as $module_id => $data ) {
1066 if ( WP_FS__MODULE_TYPE_PLUGIN === $data['type'] ) {
1067 $plugin_slugs[ $data['slug'] ] = true;
1068 }
1069 }
1070
1071 $installs = self::get_account_option( 'sites', WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
1072
1073 if ( is_array( $installs ) ) {
1074 foreach ( $installs as $slug => $install ) {
1075 if ( ! isset( $connection_status[ $slug ] ) ) {
1076 $connection_status[ $slug ] = array();
1077 }
1078
1079 if ( is_object( $install ) &&
1080 FS_Site::is_valid_id( $install->id ) &&
1081 FS_User::is_valid_id( $install->user_id )
1082 ) {
1083 $connection_status[ $slug ][ $blog_id ] = $install->user_id;
1084 }
1085 }
1086 }
1087 }
1088
1089 foreach ( $plugin_slugs as $slug => $true ) {
1090 if ( ! isset( $connection_status[ $slug ] ) ) {
1091 $connection_status[ $slug ] = array();
1092 }
1093
1094 foreach ( $sites as $site ) {
1095 $blog_id = self::get_site_blog_id( $site );
1096
1097 if ( isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1098 continue;
1099 }
1100
1101 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1102
1103 $is_anonymous = $storage->get( 'is_anonymous', null, $blog_id );
1104
1105 if ( ! is_null( $is_anonymous ) ) {
1106 // Since 1.1.3 is_anonymous is an array.
1107 if ( is_array( $is_anonymous ) && isset( $is_anonymous['is'] ) ) {
1108 $is_anonymous = $is_anonymous['is'];
1109 }
1110
1111 if ( is_bool( $is_anonymous ) && true === $is_anonymous ) {
1112 $connection_status[ $slug ][ $blog_id ] = 'skipped';
1113 }
1114 }
1115
1116 if ( ! isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1117 $connection_status[ $slug ][ $blog_id ] = 'ignored';
1118 }
1119 }
1120 }
1121
1122 $super_admins = array();
1123
1124 foreach ( $connection_status as $slug => $blogs_status ) {
1125 $skips = 0;
1126 $ignores = 0;
1127 $connections = 0;
1128 $opted_in_users = array();
1129 $opted_in_super_admins = array();
1130
1131 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1132
1133 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1134 if ( 'skipped' === $status_or_user_id ) {
1135 $skips ++;
1136 } else if ( 'ignored' === $status_or_user_id ) {
1137 $ignores ++;
1138 } else if ( FS_User::is_valid_id( $status_or_user_id ) ) {
1139 $connections ++;
1140
1141 if ( ! isset( $opted_in_users[ $status_or_user_id ] ) ) {
1142 $opted_in_users[ $status_or_user_id ] = array();
1143 }
1144
1145 $opted_in_users[ $status_or_user_id ][] = $blog_id;
1146
1147 if ( isset( $super_admins[ $status_or_user_id ] ) ||
1148 self::is_super_admin( $status_or_user_id )
1149 ) {
1150 // Cache super-admin data.
1151 $super_admins[ $status_or_user_id ] = true;
1152
1153 // Remember opted-in super-admins for the plugin.
1154 $opted_in_super_admins[ $status_or_user_id ] = true;
1155 }
1156 }
1157 }
1158
1159 $main_super_admin_user_id = null;
1160 $all_migrated = false;
1161 if ( $sites_count == $skips ) {
1162 // All sites were skipped -> network skip by copying the anonymous mode from any of the sites.
1163 $storage->is_anonymous_ms = $storage->is_anonymous;
1164
1165 $all_migrated = true;
1166 } else if ( $sites_count == $ignores ) {
1167 // Don't do anything, still in activation mode.
1168
1169 $all_migrated = true;
1170 } else if ( 0 < count( $opted_in_super_admins ) ) {
1171 // Find the super-admin with the majority of installs.
1172 $max_installs_by_super_admin = 0;
1173 foreach ( $opted_in_super_admins as $user_id => $true ) {
1174 $installs_count = count( $opted_in_users[ $user_id ] );
1175
1176 if ( $installs_count > $max_installs_by_super_admin ) {
1177 $max_installs_by_super_admin = $installs_count;
1178 $main_super_admin_user_id = $user_id;
1179 }
1180 }
1181
1182 if ( $sites_count == $connections && 1 == count( $opted_in_super_admins ) ) {
1183 // Super-admin opted-in for all sites in the network.
1184 $storage->is_network_connected = true;
1185
1186 $all_migrated = true;
1187 }
1188
1189 // Store network user.
1190 $storage->network_user_id = $main_super_admin_user_id;
1191
1192 $storage->network_install_blog_id = ( $sites_count == $connections ) ?
1193 // Since all sites are opted-in, associating with the main site.
1194 get_current_blog_id() :
1195 // Associating with the 1st found opted-in site.
1196 $opted_in_users[ $main_super_admin_user_id ][0];
1197
1198 /**
1199 * Make sure we migrate the plan ID of the network install, otherwise, if after the migration
1200 * the 1st page that will be loaded is the network level WP Admin and $storage->network_install_blog_id
1201 * is different than the main site of the network, the $this->_site will not be set since the plan_id
1202 * will be empty.
1203 */
1204 $storage->migrate_to_network();
1205 self::migrate_install_plan_to_plan_id( $storage, $storage->network_install_blog_id );
1206 } else {
1207 // At least one opt-in. All the opt-in were created by a non-super-admin.
1208 if ( 0 == $ignores ) {
1209 // All sites were opted-in or skipped, all by non-super-admin. So delegate all.
1210 $storage->store( 'is_delegated_connection', true, true );
1211
1212 $all_migrated = true;
1213 }
1214 }
1215
1216 if ( ! $all_migrated ) {
1217 /**
1218 * Delegate all sites that were:
1219 * 1) Opted-in by a user that is NOT the main-super-admin.
1220 * 2) Skipped and non of the sites was opted-in by a super-admin. If any site was opted-in by a super-admin, there will be a main-super-admin, and we consider the skip as if it was done by that user.
1221 */
1222 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1223 if ( $status_or_user_id == $main_super_admin_user_id ) {
1224 continue;
1225 }
1226
1227 if ( FS_User::is_valid_id( $status_or_user_id ) ||
1228 ( 'skipped' === $status_or_user_id && is_null( $main_super_admin_user_id ) )
1229 ) {
1230 $storage->store( 'is_delegated_connection', true, $blog_id );
1231 }
1232 }
1233 }
1234
1235
1236 if ( ( $connections + $skips > 0 ) ) {
1237 if ( $ignores > 0 ) {
1238 /**
1239 * If admin already opted-in or skipped in any of the network sites, and also
1240 * have sites which the connection decision was not yet taken, set this plugin
1241 * into network activation mode so the super-admin can choose what to do with
1242 * the rest of the sites.
1243 */
1244 self::set_network_upgrade_mode( $storage );
1245 }
1246 }
1247 }
1248 }
1249
1250 /**
1251 * Set a module into network upgrade mode.
1252 *
1253 * @author Vova Feldman (@svovaf)
1254 * @since 2.0.0
1255 *
1256 * @param \FS_Storage $storage
1257 *
1258 * @return bool
1259 */
1260 private static function set_network_upgrade_mode( FS_Storage $storage ) {
1261 return $storage->is_network_activation = true;
1262 }
1263
1264 /**
1265 * Will return true after upgrading to the SDK with the network level integration,
1266 * when the super-admin involvement is required regarding the rest of the sites.
1267 *
1268 * @author Vova Feldman (@svovaf)
1269 * @since 2.0.0
1270 *
1271 * @return bool
1272 */
1273 function is_network_upgrade_mode() {
1274 return $this->_storage->get( 'is_network_activation' );
1275 }
1276
1277 /**
1278 * Clear flag after the upgrade mode completion.
1279 *
1280 * @author Vova Feldman (@svovaf)
1281 * @since 2.0.0
1282 *
1283 * @return bool True if network activation was on and now completed.
1284 */
1285 private function network_upgrade_mode_completed() {
1286 if ( fs_is_network_admin() && $this->is_network_upgrade_mode() ) {
1287 $this->_storage->remove( 'is_network_activation' );
1288
1289 return true;
1290 }
1291
1292 return false;
1293 }
1294
1295 #endregion
1296
1297 /**
1298 * This action is connected to the 'plugins_loaded' hook and helps to determine
1299 * if this is a new plugin installation or a plugin update.
1300 *
1301 * There are 3 different use-cases:
1302 * 1) New plugin installation right with Freemius:
1303 * 1.1 _activate_plugin_event_hook() will be executed first
1304 * 1.2 Since $this->_storage->is_plugin_new_install is not set,
1305 * and $this->_storage->plugin_last_version is not set,
1306 * $this->_storage->is_plugin_new_install will be set to TRUE.
1307 * 1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1308 * be already set to TRUE.
1309 *
1310 * 2) Plugin update, didn't have Freemius before, and now have the SDK:
1311 * 2.1 _activate_plugin_event_hook() will not be executed, because
1312 * the activation hook do NOT fires on updates since WP 3.1.
1313 * 2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1314 * be empty, therefore, it will be set to FALSE.
1315 *
1316 * 3) Plugin update, had Freemius in prev version as well:
1317 * 3.1 _version_updates_handler() will be executed 1st, since FS was installed
1318 * before, $this->_storage->plugin_last_version will NOT be empty,
1319 * therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
1320 * 3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
1321 * already set, therefore, it will not be modified.
1322 *
1323 * Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
1324 *
1325 * NOTE:
1326 * The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
1327 * and then, the next immediate PageView is the plugin's main settings page, it will not
1328 * show the opt-in right away. The reason it will happen is because Freemius execution
1329 * will be turned off till the plugin is fully loaded at least once
1330 * (till $this->_storage->was_plugin_loaded is TRUE).
1331 *
1332 * @author Vova Feldman (@svovaf)
1333 * @since 1.1.9
1334 *
1335 */
1336 function _plugins_loaded() {
1337 // Update flag that plugin was loaded with Freemius at least once.
1338 $this->_storage->was_plugin_loaded = true;
1339
1340 /**
1341 * Bug fix - only set to false when it's a plugin, due to the
1342 * execution sequence of the theme hooks and our methods, if
1343 * this will be set for themes, Freemius will always assume
1344 * it's a theme update.
1345 *
1346 * @author Vova Feldman (@svovaf)
1347 * @since 1.2.2.2
1348 */
1349 if ( $this->is_plugin() &&
1350 ! isset( $this->_storage->is_plugin_new_install )
1351 ) {
1352 $this->_storage->is_plugin_new_install = (
1353 ! is_plugin_active( $this->_plugin_basename ) &&
1354 empty( $this->_storage->plugin_last_version )
1355 );
1356 }
1357 }
1358
1359 function _run_garbage_collector() {
1360 if ( true !== fs_get_optional_constant( 'WP_FS__ENABLE_GARBAGE_COLLECTOR', true ) ) {
1361 return;
1362 }
1363
1364 if ( ! $this->is_user_in_admin() ) {
1365 return;
1366 }
1367
1368 require_once WP_FS__DIR_INCLUDES . '/class-fs-lock.php';
1369
1370 $lock = new FS_Lock( 'garbage_collection' );
1371
1372 if ( $lock->is_locked() ) {
1373 return;
1374 }
1375
1376 // Create a 1-day lock.
1377 $lock->lock( WP_FS__TIME_24_HOURS_IN_SEC );
1378
1379 FS_Garbage_Collector::instance()->clean();
1380 }
1381
1382 /**
1383 * Opens the support forum subemenu item in a new browser page.
1384 *
1385 * @author Vova Feldman (@svovaf)
1386 * @since 2.1.4
1387 */
1388 static function _open_support_forum_in_new_page() {
1389 ?>
1390 <script type="text/javascript">
1391 (function ($) {
1392 $('.fs-submenu-item.wp-support-forum').parent().attr( { target: '_blank', rel: 'noopener noreferrer' } );
1393 })(jQuery);
1394 </script>
1395 <?php
1396 }
1397
1398 /**
1399 * @author Vova Feldman (@svovaf)
1400 * @since 1.0.9
1401 */
1402 private function register_constructor_hooks() {
1403 $this->_logger->entrance();
1404
1405 if ( is_admin() ) {
1406 add_action( 'admin_init', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
1407
1408 if ( $this->is_plugin() ) {
1409 if ( self::is_plugin_install_page() && true !== fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) {
1410 /**
1411 * Unless the `fs_allow_updater_and_dialog` URL param exists and its value is `true`, make
1412 * Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php)
1413 * so that they won't interfere with the .org plugins' functionalities on that page (e.g.
1414 * updating of a .org plugin).
1415 */
1416 add_filter( 'site_transient_update_plugins', array( 'Freemius', '_remove_fs_updates_from_plugin_install_page' ), 10, 2 );
1417 } else if ( self::is_plugins_page() || self::is_updates_page() ) {
1418 /**
1419 * On the "Plugins" and "Updates" admin pages, if there are premium or non–org-compliant plugins, modify their details dialog URLs (add a Freemius-specific param) so that the SDK can determine if the plugin information dialog should show information from Freemius.
1420 *
1421 * @author Leo Fajardo (@leorw)
1422 * @since 2.2.3
1423 */
1424 add_action( 'admin_footer', array( 'Freemius', '_prepend_fs_allow_updater_and_dialog_flag_url_param' ) );
1425 }
1426
1427 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
1428
1429 /**
1430 * @since 1.2.2
1431 *
1432 * Hook to both free and premium version activations to support
1433 * auto deactivation on the other version activation.
1434 */
1435 register_activation_hook(
1436 $plugin_dir . $this->_free_plugin_basename,
1437 array( &$this, '_activate_plugin_event_hook' )
1438 );
1439
1440 register_activation_hook(
1441 $plugin_dir . $this->premium_plugin_basename(),
1442 array( &$this, '_activate_plugin_event_hook' )
1443 );
1444 } else {
1445 add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
1446
1447 add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
1448 }
1449
1450 /**
1451 * Part of the mechanism to identify new plugin install vs. plugin update.
1452 *
1453 * @author Vova Feldman (@svovaf)
1454 * @since 1.1.9
1455 */
1456 if ( empty( $this->_storage->was_plugin_loaded ) ) {
1457 /**
1458 * During the plugin activation (not theme), 'plugins_loaded' will be already executed
1459 * when the logic gets here since the activation logic first add the activate plugins,
1460 * then triggers 'plugins_loaded', and only then include the code of the plugin that
1461 * is activated. Which means that _plugins_loaded() will NOT be executed during the
1462 * plugin activation, and that IS intentional.
1463 *
1464 * @author Vova Feldman (@svovaf)
1465 */
1466 if ( $this->is_plugin() &&
1467 $this->is_activation_mode( false ) &&
1468 0 == did_action( 'plugins_loaded' )
1469 ) {
1470 add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
1471 } else {
1472 // If was activated before, then it was already loaded before.
1473 $this->_plugins_loaded();
1474 }
1475 }
1476
1477 add_action( 'plugins_loaded', array( &$this, '_run_garbage_collector' ) );
1478
1479 if ( ! self::is_ajax() ) {
1480 if ( ! $this->is_addon() ) {
1481 add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
1482 }
1483 }
1484
1485 if ( $this->_storage->handle_gdpr_admin_notice ) {
1486 add_action( 'init', array( &$this, '_maybe_show_gdpr_admin_notice' ) );
1487 }
1488
1489 add_action( 'init', array( &$this, '_maybe_add_gdpr_optin_ajax_handler') );
1490 add_action( 'init', array( &$this, '_maybe_add_pricing_ajax_handler' ) );
1491 }
1492
1493 if ( $this->is_plugin() ) {
1494 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1495 add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
1496 } else {
1497 add_action( 'wp_initialize_site', array( $this, '_after_wp_initialize_site_callback' ), 11, 2 );
1498 }
1499
1500 register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
1501 }
1502
1503 if ( is_multisite() ) {
1504 add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
1505 add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) );
1506 add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) );
1507
1508 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1509 add_action( 'deleted_blog', array( $this, '_after_site_deleted_callback' ), 10, 2 );
1510 } else {
1511 add_action( 'wp_delete_site', array( $this, '_after_wpsite_deleted_callback' ) );
1512 }
1513
1514 add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) );
1515 add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) );
1516 add_action( 'make_ham_blog', array( &$this, '_after_site_reactivated_callback' ) );
1517 }
1518
1519 if ( $this->is_theme() &&
1520 self::is_customizer() &&
1521 $this->apply_filters( 'show_customizer_upsell', true )
1522 ) {
1523 // Register customizer upsell.
1524 add_action( 'customize_register', array( &$this, '_customizer_register' ) );
1525 }
1526
1527 add_action( 'admin_init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
1528
1529 if ( $this->is_theme() && ! $this->is_migration() ) {
1530 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1531 }
1532
1533 add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
1534 add_action( 'admin_init', array( &$this, '_add_premium_version_upgrade_selection' ) );
1535 add_action( 'admin_init', array( &$this, '_add_beta_mode_update_handler' ) );
1536 add_action( 'admin_init', array( &$this, '_add_user_change_option' ) );
1537 add_action( 'admin_init', array( &$this, '_add_email_address_update_option' ) );
1538
1539 $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
1540 $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
1541 $this->add_ajax_action( 'set_data_debug_mode', array( &$this, '_set_data_debug_mode' ) );
1542 $this->add_ajax_action( 'toggle_whitelabel_mode', array( &$this, '_toggle_whitelabel_mode_ajax_handler' ) );
1543
1544 if ( $this->_is_network_active && fs_is_network_admin() ) {
1545 $this->add_ajax_action( 'network_activate', array( &$this, '_network_activate_ajax_action' ) );
1546 }
1547
1548 $this->add_ajax_action( 'install_premium_version', array(
1549 &$this,
1550 '_install_premium_version_ajax_action'
1551 ) );
1552
1553 $this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
1554
1555 $this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
1556
1557 $this->add_action( 'sdk_version_update', array( &$this, '_sdk_version_update' ), WP_FS__DEFAULT_PRIORITY, 2 );
1558
1559 $this->add_action(
1560 'plugin_version_update',
1561 array( &$this, '_after_version_update' ),
1562 WP_FS__DEFAULT_PRIORITY,
1563 2
1564 );
1565 $this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
1566
1567 add_action( 'admin_init', array( &$this, '_add_trial_notice' ) ); // @phpstan-ignore-line
1568 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) ); // @phpstan-ignore-line
1569 add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) );
1570
1571 /**
1572 * Handle request to reset anonymous mode for `get_reconnect_url()` or reset the pending activation mode.
1573 *
1574 * @author Vova Feldman (@svovaf)
1575 * @since 1.2.1.5
1576 */
1577 if (
1578 (
1579 fs_request_is_action( 'reset_anonymous_mode' ) ||
1580 fs_request_is_action( 'reset_pending_activation_mode' )
1581 ) &&
1582 $this->get_unique_affix() === fs_request_get_raw( 'fs_unique_affix' )
1583 ) {
1584 add_action( 'admin_init', array( &$this, 'connect_again' ) );
1585 }
1586 }
1587
1588 /**
1589 * Register the required hooks right after the settings parse is completed.
1590 *
1591 * @author Vova Feldman (@svovaf)
1592 * @since 2.3.1
1593 */
1594 private function register_after_settings_parse_hooks() {
1595 if ( is_admin() &&
1596 $this->is_theme() &&
1597 $this->is_premium() &&
1598 ! $this->has_active_valid_license()
1599 ) {
1600 $this->add_ajax_action(
1601 'delete_theme_update_data',
1602 array( &$this, '_delete_theme_update_data_action' )
1603 );
1604 }
1605
1606 if ( $this->show_settings_with_tabs() ) {
1607 /**
1608 * Include the required hooks to capture the theme settings' page tabs
1609 * and cache them.
1610 *
1611 * @author Vova Feldman (@svovaf)
1612 * @since 1.2.2.7
1613 */
1614 if ( ! $this->_cache->has_valid( 'tabs' ) ) {
1615 add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
1616 // Add license activation AJAX callback.
1617 $this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
1618
1619 add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
1620 }
1621
1622 add_action(
1623 'admin_footer',
1624 array( &$this, '_add_freemius_tabs' ),
1625 /**
1626 * The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
1627 * That's why the priority is 11 while the tabs capture logic is added
1628 * with priority 10.
1629 *
1630 * @author Vova Feldman (@svovaf)
1631 */
1632 11
1633 );
1634 }
1635
1636 if ( ! self::is_ajax() ) {
1637 if ( ! $this->is_addon() || $this->is_only_premium() ) {
1638 add_action(
1639 ( $this->_is_network_active && fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu',
1640 array( &$this, '_prepare_admin_menu' ),
1641 WP_FS__LOWEST_PRIORITY
1642 );
1643 }
1644 }
1645 }
1646
1647 /**
1648 * Makes Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php) so that
1649 * they won't interfere with the .org plugins' functionalities on that page (e.g. updating of a .org plugin).
1650 *
1651 * @author Leo Fajardo (@leorw)
1652 * @since 2.2.3
1653 *
1654 * @param object $updates
1655 * @param string|null $transient
1656 *
1657 * @return object
1658 */
1659 static function _remove_fs_updates_from_plugin_install_page( $updates, $transient = null ) {
1660 if ( is_object( $updates ) && isset( $updates->response ) ) {
1661 foreach ( $updates->response as $file => $plugin ) {
1662 if ( isset( $plugin->package ) && false !== strpos( $plugin->package, 'api.freemius' ) ) {
1663 unset( $updates->response[ $file ] );
1664 }
1665 }
1666 }
1667
1668 return $updates;
1669 }
1670
1671 /**
1672 * Prepends the `fs_allow_updater_and_dialog` param to the plugin information URLs to tell the SDK to handle
1673 * the information that is shown on the plugin details dialog that is shown when the relevant link is clicked.
1674 *
1675 * @author Leo Fajardo (@leorw)
1676 * @since 2.2.3
1677 *
1678 * @return void
1679 */
1680 static function _prepend_fs_allow_updater_and_dialog_flag_url_param() {
1681 $slug_basename_map = array();
1682 foreach ( self::$_instances as $instance ) {
1683 if ( ! $instance->is_plugin() ) {
1684 continue;
1685 }
1686
1687 $slug_basename_map[ $instance->get_slug() ] = $instance->premium_plugin_basename();
1688 }
1689 ?>
1690 <script type="text/javascript">
1691 (function( $ ) {
1692 var slugBasenameMap = <?php echo json_encode( $slug_basename_map ) ?>;
1693 for ( var slug in slugBasenameMap ) {
1694 var basename = slugBasenameMap[ slug ];
1695
1696 // Try to get the plugin rows if on the "Plugins" page.
1697 var $pluginRows = $( '.wp-list-table.plugins tr[data-plugin="' + basename + '"]');
1698
1699 if ( 0 === $pluginRows.length ) {
1700 // Try to get the plugin rows if on the "Updates" page.
1701 var $pluginCheckbox = $( '#update-plugins-table input[type="checkbox"][value="' + basename + '"]' );
1702 if ( 0 !== $pluginCheckbox.length ) {
1703 $pluginRows = $pluginCheckbox.parents( 'tr:first' );
1704 }
1705 }
1706
1707 if ( 0 === $pluginRows.length ) {
1708 // No plugin rows found.
1709 continue;
1710 }
1711
1712 // Find the "View details" links and add the `fs_allow_updater_and_dialog` param to the URL.
1713 $pluginRows.find( 'a[href*="plugin-install.php?tab=plugin-information"]' ).each(function() {
1714 var $this = $( this ),
1715 href = $this.attr( 'href' ).replace( '?tab=', '?fs_allow_updater_and_dialog=true&tab=');
1716
1717 $this.attr( 'href', href );
1718 });
1719 }
1720 })( jQuery );
1721 </script>
1722 <?php
1723 }
1724
1725 /**
1726 * @author Leo Fajardo (@leorw)
1727 * @since 2.3.0
1728 */
1729 static function _maybe_add_beta_label_styles() {
1730 $has_any_beta_version = false;
1731
1732 foreach ( self::$_instances as $instance ) {
1733 if ( $instance->is_beta() ) {
1734 $has_any_beta_version = true;
1735 break;
1736 }
1737 }
1738
1739 if ( $has_any_beta_version ) {
1740 fs_enqueue_local_style( 'fs_plugins', '/admin/plugins.css' );
1741 }
1742 }
1743
1744 /**
1745 * @author Leo Fajardo (@leorw)
1746 * @since 2.3.0
1747 */
1748 static function _maybe_add_beta_label_to_plugins_and_handle_confirmation() {
1749 $beta_data = array();
1750
1751 foreach ( self::$_instances as $instance ) {
1752 if ( ! $instance->is_premium() ) {
1753 continue;
1754 }
1755
1756 /**
1757 * If there's an available beta version update, a confirmation message will be shown when the
1758 * "Update now" link on the "Plugins" or "Themes" page is clicked.
1759 */
1760 $has_beta_update = $instance->has_beta_update();
1761
1762 $is_beta = (
1763 // The "Beta" label is added separately for themes.
1764 $instance->is_plugin() &&
1765 $instance->is_beta()
1766 );
1767
1768 if ( ! $is_beta && ! $has_beta_update ) {
1769 continue;
1770 }
1771
1772 $beta_data[ $instance->get_plugin_basename() ] = array( 'is_installed_version_beta' => $is_beta );
1773
1774 if ( ! $has_beta_update ) {
1775 continue;
1776 }
1777
1778 $beta_data[ $instance->get_plugin_basename() ]['beta_version_update_confirmation_message'] = sprintf(
1779 '%s %s',
1780 sprintf(
1781 fs_esc_attr_inline(
1782 'An update to a Beta version will replace your installed version of %s with the latest Beta release - use with caution, and not on production sites. You have been warned.',
1783 'beta-version-update-caution',
1784 $instance->get_slug()
1785 ),
1786 $instance->get_plugin_title()
1787 ),
1788 fs_esc_attr_inline( 'Would you like to proceed with the update?', 'update-confirmation', $instance->get_slug() )
1789 );
1790 }
1791
1792 if ( empty( $beta_data ) ) {
1793 return;
1794 }
1795 ?>
1796 <script type="text/javascript">
1797 ( function( $ ) {
1798 var betaData = <?php echo json_encode( $beta_data ) ?>;
1799
1800 for ( var pluginBasename in betaData ) {
1801 if ( ! betaData.hasOwnProperty( pluginBasename ) ) {
1802 continue;
1803 }
1804
1805 if ( ! betaData[ pluginBasename ].is_installed_version_beta ) {
1806 continue;
1807 }
1808
1809 var $parentContainer = $( '.wp-list-table.plugins tr[data-plugin="' + pluginBasename + '"]' );
1810 if ( 0 === $parentContainer.length ) {
1811 continue;
1812 }
1813
1814 $parentContainer.find( '.plugin-title > strong:first-child').append(
1815 '<span class="fs-tag fs-info"><?php fs_esc_js_echo_inline( 'Beta', 'beta' ) ?></span>'
1816 );
1817 }
1818
1819 setTimeout( function() {
1820 // Wait a little bit before adding the event handler, otherwise, it will be overridden by the core WP logic.
1821 $( '.plugins .update-message .update-link, .themes .theme .update-message' ).on( 'click', function() {
1822 var $parentContainer = $( this ).parents( 'tr:first' );
1823 pluginBasename = ( 0 !== $parentContainer.length ) ?
1824 $parentContainer.data( 'plugin' ) :
1825 $( this ).parents( '.theme:first' ).data( 'slug' );
1826
1827 if (
1828 betaData[ pluginBasename ] &&
1829 betaData[ pluginBasename ].beta_version_update_confirmation_message &&
1830 ! confirm( betaData[ pluginBasename ].beta_version_update_confirmation_message )
1831 ) {
1832 return false;
1833 }
1834 } );
1835 }, 20 );
1836 } )( jQuery );
1837 </script>
1838 <?php
1839 }
1840
1841 /**
1842 * Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
1843 * could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
1844 * plugin will trigger inclusion of the free or premium version and if one of them is active during the
1845 * uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
1846 *
1847 * @author Leo Fajardo (@leorw)
1848 *
1849 * @since 1.2.0
1850 */
1851 private function unregister_uninstall_hook() {
1852 $uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
1853 unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
1854 unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
1855
1856 update_option( 'uninstall_plugins', $uninstallable_plugins );
1857 }
1858
1859 /**
1860 * @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
1861 *
1862 * @param bool $store_prev_path
1863 */
1864 private function clear_module_main_file_cache( $store_prev_path = true ) {
1865 if ( ! isset( $this->_storage->plugin_main_file ) ||
1866 empty( $this->_storage->plugin_main_file->path )
1867 ) {
1868 return;
1869 }
1870
1871 if ( ! $store_prev_path ) {
1872 /**
1873 * Storing the previous path is not needed when clearing the cache after an SDK version update since
1874 * the main purpose of the cache clearing in that event is to correct a wrong plugin main file path
1875 * which causes data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
1876 *
1877 * @author Leo Fajardo (@leorw)
1878 * @since 2.2.1
1879 */
1880 unset( $this->_storage->plugin_main_file->path );
1881 } else {
1882 $plugin_main_file = clone $this->_storage->plugin_main_file;
1883
1884 // Store cached path (2nd layer cache).
1885 $plugin_main_file->prev_path = $plugin_main_file->path;
1886
1887 // Clear cached path.
1888 unset( $plugin_main_file->path );
1889
1890 $this->_storage->plugin_main_file = $plugin_main_file;
1891 }
1892
1893 /**
1894 * Clear global cached path.
1895 *
1896 * @author Leo Fajardo (@leorw)
1897 * @since 1.2.2
1898 */
1899 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
1900 unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
1901 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
1902 }
1903
1904 /**
1905 * @author Leo Fajardo (@leorw)
1906 * @since 2.0.0
1907 */
1908 function _hook_action_links_and_register_account_hooks() {
1909 if ( $this->is_migration() ) {
1910 return;
1911 }
1912
1913 if (
1914 ( self::is_plugins_page() && $this->is_plugin() ) ||
1915 ( self::is_themes_page() && $this->is_theme() ) ||
1916 fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' )
1917 ) {
1918 $this->_add_tracking_links();
1919 }
1920
1921 if ( self::is_plugins_page() && $this->is_plugin() ) {
1922 $this->hook_plugin_action_links();
1923 }
1924
1925 $this->_register_account_hooks();
1926 }
1927
1928 /**
1929 * @author Vova Feldman (@svovaf)
1930 * @since 1.0.9
1931 */
1932 private function _register_account_hooks() {
1933 if ( ! is_admin() ) {
1934 return;
1935 }
1936
1937 /**
1938 * Always show the deactivation feedback form since we added
1939 * automatic free version deactivation upon premium code activation.
1940 *
1941 * @since 1.2.1.6
1942 */
1943 $this->add_ajax_action(
1944 'submit_uninstall_reason',
1945 array( &$this, '_submit_uninstall_reason_action' )
1946 );
1947
1948 $this->add_ajax_action(
1949 'cancel_subscription_or_trial',
1950 array( &$this, 'cancel_subscription_or_trial_ajax_action' )
1951 );
1952
1953 if ( ! $this->is_addon() || $this->is_parent_plugin_installed() ) {
1954 if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
1955 ( $this->is_theme() && self::is_themes_page() )
1956 ) {
1957 add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
1958 }
1959 }
1960 }
1961
1962 /**
1963 * Leverage backtrace to find caller plugin file path.
1964 *
1965 * @param bool $is_init Is initiation sequence.
1966 * @param string $main_file Since 2.5.0 expects the module's main file path to potentially purge the cached path.
1967 *
1968 * @return string
1969 * @since 1.0.6
1970 *
1971 * @author Vova Feldman (@svovaf)
1972 */
1973 private function _find_caller_plugin_file( $is_init = false, $main_file = '' ) {
1974 // Try to load the cached value of the file path.
1975 if ( isset( $this->_storage->plugin_main_file ) ) {
1976 $plugin_main_file = $this->_storage->plugin_main_file;
1977 if ( ! empty( $plugin_main_file->path ) ) {
1978 $absolute_path = $this->get_absolute_path( $plugin_main_file->path );
1979 if ( file_exists( $absolute_path ) ) {
1980 if ( $is_init && $absolute_path !== $this->get_absolute_path( $main_file ) ) {
1981 // Update cached path if not matching the actual path.
1982 $plugin_main_file->path = $main_file;
1983 $this->_storage->plugin_main_file = $plugin_main_file;
1984 }
1985
1986 return $absolute_path;
1987 }
1988 }
1989 }
1990
1991 /**
1992 * @since 1.2.1
1993 *
1994 * `clear_module_main_file_cache()` is clearing the plugin's cached path on
1995 * deactivation. Therefore, if any plugin/theme was initiating `Freemius`
1996 * with that plugin's slug, it was overriding the empty plugin path with a wrong path.
1997 *
1998 * So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
1999 * when the class instantiator isn't the module.
2000 */
2001 if ( ! $is_init ) {
2002 // Fetch prev path cache.
2003 if ( isset( $this->_storage->plugin_main_file ) &&
2004 ! empty( $this->_storage->plugin_main_file->prev_path )
2005 ) {
2006 $absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
2007 if ( file_exists( $absolute_path ) ) {
2008 return $absolute_path;
2009 }
2010 }
2011
2012 wp_die(
2013 $this->get_text_inline( 'Freemius SDK couldn\'t find the plugin\'s main file. Please contact sdk@freemius.com with the current error.', 'failed-finding-main-path' ) .
2014 " Module: {$this->_slug}; SDK: " . WP_FS__SDK_VERSION . ";",
2015 $this->get_text_inline( 'Error', 'error' ),
2016 array( 'back_link' => true )
2017 );
2018 }
2019
2020 /**
2021 * @since 1.2.1
2022 *
2023 * Only the original instantiator that calls dynamic_init can modify the module's path.
2024 */
2025 // Find caller module.
2026 $this->_storage->plugin_main_file = (object) array(
2027 'path' => $main_file,
2028 );
2029
2030 return $this->get_absolute_path( $main_file );
2031 }
2032
2033 /**
2034 * @author Leo Fajardo (@leorw)
2035 * @since 1.2.3
2036 *
2037 * @param string $path
2038 *
2039 * @return string
2040 */
2041 private function get_relative_path( $path ) {
2042 $module_root_dir = $this->get_module_root_dir_path();
2043 if ( 0 === strpos( $path, $module_root_dir ) ) {
2044 $path = substr( $path, strlen( $module_root_dir ) );
2045 }
2046
2047 return $path;
2048 }
2049
2050 /**
2051 * @author Leo Fajardo (@leorw)
2052 * @since 1.2.3
2053 *
2054 * @param string $path
2055 * @param string|bool $module_type
2056 *
2057 * @return string
2058 */
2059 private function get_absolute_path( $path, $module_type = false ) {
2060 $module_root_dir = $this->get_module_root_dir_path( $module_type );
2061 if ( 0 !== strpos( $path, $module_root_dir ) ) {
2062 $path = fs_normalize_path( $module_root_dir . $path );
2063 }
2064
2065 return $path;
2066 }
2067
2068 /**
2069 * @author Leo Fajardo (@leorw)
2070 * @since 1.2.3
2071 *
2072 * @param string|bool $module_type
2073 *
2074 * @return string
2075 */
2076 private function get_module_root_dir_path( $module_type = false ) {
2077 $is_plugin = empty( $module_type ) ?
2078 $this->is_plugin() :
2079 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
2080
2081 return fs_normalize_path( trailingslashit( $is_plugin ?
2082 WP_PLUGIN_DIR :
2083 get_theme_root( get_stylesheet() ) ) );
2084 }
2085
2086 /**
2087 * @author Leo Fajardo (@leorw)
2088 *
2089 * @param number $module_id
2090 * @param string $slug
2091 *
2092 * @return string Since 2.5.0 return the module's main file path.
2093 *
2094 * @since 1.2.2
2095 */
2096 private function store_id_slug_type_path_map( $module_id, $slug ) {
2097 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
2098
2099 $store_option = false;
2100
2101 if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
2102 $id_slug_type_path_map[ $module_id ] = array(
2103 'slug' => $slug
2104 );
2105
2106 $store_option = true;
2107 } else if (
2108 isset( $id_slug_type_path_map[ $module_id ]['slug'] ) &&
2109 $slug !== $id_slug_type_path_map[ $module_id ]['slug']
2110 ) {
2111 $id_slug_type_path_map[ $module_id ]['slug'] = $slug;
2112 $store_option = true;
2113 }
2114
2115 $find_caller = empty( $id_slug_type_path_map[ $module_id ]['path'] );
2116
2117 if ( ! $find_caller ) {
2118 /**
2119 * This verification is for cases when suddenly the same module
2120 * is installed but with a different folder name.
2121 *
2122 * @author Vova Feldman (@svovaf)
2123 * @since 1.2.3
2124 */
2125 $find_caller = ! file_exists( $this->get_absolute_path(
2126 $id_slug_type_path_map[ $module_id ]['path'],
2127 $id_slug_type_path_map[ $module_id ]['type']
2128 ) );
2129 }
2130
2131 foreach ( $id_slug_type_path_map as $id => $data ) {
2132 if ( empty( $id ) ) {
2133 // Remove maps with empty module ID.
2134 unset( $id_slug_type_path_map[ $id ] );
2135 $store_option = true;
2136 continue;
2137 }
2138
2139 /**
2140 * If the module's main file path is identical to the main file path of another module then it means that the cached path of the current module or the other one with the same path is wrong, and therefore, we need to recalculate those paths.
2141 *
2142 * @author Vova Feldman (@svovaf)
2143 * @since 2.5.0
2144 */
2145 if ( ! $find_caller ) {
2146 if ( $id == $module_id ) {
2147 continue;
2148 }
2149
2150 if (
2151 isset( $data['path'] ) &&
2152 $data['path'] === $id_slug_type_path_map[ $module_id ]['path']
2153 ) {
2154 $find_caller = true;
2155 }
2156 }
2157 }
2158
2159 if ( $find_caller ) {
2160 $caller_main_file_and_type = $this->get_caller_main_file_and_type( $module_id );
2161
2162 $id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type;
2163 $id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path;
2164
2165 $store_option = true;
2166 }
2167
2168 if ( $store_option ) {
2169 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
2170 }
2171
2172 return $id_slug_type_path_map[ $module_id ]['path'];
2173 }
2174
2175 /**
2176 * Identifies the caller type: plugin or theme.
2177 *
2178 * @author Leo Fajardo (@leorw)
2179 * @since 1.2.2
2180 *
2181 * @author Vova Feldman (@svovaf)
2182 * @since 1.2.2.3 Find the earliest module in the call stack that calls to the SDK. This fix is for cases when
2183 * add-ons are relying on loading the SDK from the parent module, and also allows themes including the
2184 * SDK an internal file instead of directly from functions.php.
2185 * @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic.
2186 *
2187 * @param number $module_id @since 2.5.0
2188 */
2189 private function get_caller_main_file_and_type( $module_id ) {
2190 self::require_plugin_essentials();
2191
2192 $all_plugins = fs_get_plugins( true );
2193 $all_plugins_paths = array();
2194
2195 // Get active plugin's main files real full names (might be symlinks).
2196 foreach ( $all_plugins as $relative_path => $data ) {
2197 if ( false === strpos( fs_normalize_path( $relative_path ), '/' ) ) {
2198 /**
2199 * Ignore plugins that don't have a folder (e.g. Hello Dolly) since they
2200 * can't really include the SDK.
2201 *
2202 * @author Vova Feldman
2203 * @since 1.2.1.7
2204 */
2205 continue;
2206 }
2207
2208 $all_plugins_paths[] = fs_normalize_path( realpath( WP_PLUGIN_DIR . '/' . $relative_path ) );
2209 }
2210
2211 $caller_file_candidate = false;
2212 $caller_map = array();
2213 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2214 $themes_dir = fs_normalize_path( get_theme_root( get_stylesheet() ) );
2215 $plugin_dir_to_skip = false;
2216
2217 for ( $i = 1, $bt = debug_backtrace(), $len = count( $bt ); $i < $len; $i ++ ) {
2218 if ( empty( $bt[ $i ]['file'] ) ) {
2219 continue;
2220 }
2221
2222 if ( $i > 1 && ! empty( $bt[ $i - 1 ]['file'] ) && $bt[ $i ]['file'] === $bt[ $i - 1 ]['file'] ) {
2223 // If file same as the prev file in the stack, skip it.
2224 continue;
2225 }
2226
2227 if ( ! empty( $bt[ $i ]['function'] ) && in_array( $bt[ $i ]['function'], array(
2228 'do_action',
2229 'apply_filter',
2230 // The string split is stupid, but otherwise, theme check
2231 // throws info notices.
2232 'requir' . 'e_once',
2233 'requir' . 'e',
2234 'includ' . 'e_once',
2235 'includ' . 'e',
2236 'install_and_activate_plugin',
2237 'try_activate_plugin',
2238 'activate_plugin'
2239 ) )
2240 ) {
2241 if ( 'activate_plugin' === $bt[ $i ]['function'] ) {
2242 /**
2243 * Store the directory of the activator plugin so that any other file that starts with it
2244 * cannot be mistakenly chosen as a candidate caller file.
2245 *
2246 * @author Leo Fajardo
2247 *
2248 * @since 2.3.0
2249 */
2250 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2251
2252 foreach ( $all_plugins_paths as $plugin_path ) {
2253 $plugin_dir = fs_normalize_path( dirname( $plugin_path ) . '/' );
2254 if ( false !== strpos( $caller_file_path, $plugin_dir ) ) {
2255 $plugin_dir_to_skip = $plugin_dir;
2256
2257 break;
2258 }
2259 }
2260 }
2261
2262 // Ignore call stack hooks and files inclusion.
2263 continue;
2264 }
2265
2266 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2267
2268 if ( ! empty( $plugin_dir_to_skip ) ) {
2269 /**
2270 * Skip if it's an activator plugin file to avoid mistakenly choosing it as a candidate caller file.
2271 *
2272 * @author Leo Fajardo
2273 *
2274 * @since 2.3.0
2275 */
2276 if ( 0 === strpos( $caller_file_path, $plugin_dir_to_skip ) ) {
2277 continue;
2278 }
2279 }
2280
2281 if ( 'functions.php' === basename( $caller_file_path ) ) {
2282 /**
2283 * 1. Assumes that theme's starting execution file is functions.php.
2284 * 2. This complex logic fixes symlink issues (e.g. with Vargant).
2285 *
2286 * @author Vova Feldman (@svovaf)
2287 * @since 1.2.2.5
2288 */
2289
2290 if ( $caller_file_path == fs_normalize_path( realpath( trailingslashit( $themes_dir ) . basename( dirname( $caller_file_path ) ) . '/' . basename( $caller_file_path ) ) ) ) {
2291 $module_type = WP_FS__MODULE_TYPE_THEME;
2292
2293 /**
2294 * Relative path of the theme, e.g.:
2295 * `my-theme/functions.php`
2296 *
2297 * @author Leo Fajardo (@leorw)
2298 */
2299 $caller_file_candidate = basename( dirname( $caller_file_path ) ) .
2300 '/' .
2301 basename( $caller_file_path );
2302
2303 continue;
2304 }
2305 }
2306
2307 $caller_file_hash = md5( $caller_file_path );
2308
2309 if ( ! isset( $caller_map[ $caller_file_hash ] ) ) {
2310 foreach ( $all_plugins_paths as $plugin_path ) {
2311 if ( empty( $plugin_path ) ) {
2312 continue;
2313 }
2314
2315 if ( false !== strpos( $caller_file_path, fs_normalize_path( dirname( $plugin_path ) . '/' ) ) ) {
2316 $caller_map[ $caller_file_hash ] = fs_normalize_path( $plugin_path );
2317 break;
2318 }
2319 }
2320 }
2321
2322 if ( isset( $caller_map[ $caller_file_hash ] ) ) {
2323 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2324 $caller_file_candidate = plugin_basename( $caller_map[ $caller_file_hash ] );
2325 }
2326 }
2327
2328 $caller_main_file_and_type = (object) array(
2329 'module_type' => $module_type,
2330 'path' => $caller_file_candidate
2331 );
2332
2333 return apply_filters( "fs_{$module_id}_caller_main_file_and_type", $caller_main_file_and_type );
2334 }
2335
2336 #----------------------------------------------------------------------------------
2337 #region Deactivation Feedback Form
2338 #----------------------------------------------------------------------------------
2339
2340 /**
2341 * Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
2342 * page.
2343 *
2344 * @author Vova Feldman (@svovaf)
2345 * @author Leo Fajardo (@leorw)
2346 *
2347 * @since 1.1.2
2348 */
2349 function _add_deactivation_feedback_dialog_box() {
2350 if (
2351 $this->is_clone() ||
2352 ( is_object( $this->_site ) && ! $this->is_registered() )
2353 ) {
2354 return;
2355 }
2356
2357 $subscription_cancellation_dialog_box_template_params = $this->apply_filters( 'show_deactivation_subscription_cancellation', true ) ?
2358 $this->_get_subscription_cancellation_dialog_box_template_params() :
2359 array();
2360
2361 /**
2362 * @since 2.3.0 Developers can optionally hide the deactivation feedback form using the 'show_deactivation_feedback_form' filter.
2363 */
2364 $show_deactivation_feedback_form = ! self::is_deactivation_snoozed();
2365 if ( $this->has_filter( 'show_deactivation_feedback_form' ) ) {
2366 $show_deactivation_feedback_form = $this->apply_filters( 'show_deactivation_feedback_form', true );
2367 } else if ( $this->is_addon() ) {
2368 /**
2369 * If the add-on's 'show_deactivation_feedback_form' is not set, try to inherit the value from the parent.
2370 */
2371 $show_deactivation_feedback_form = $this->get_parent_instance()->apply_filters( 'show_deactivation_feedback_form', true );
2372 }
2373
2374 $uninstall_confirmation_message = $this->apply_filters( 'uninstall_confirmation_message', '' );
2375
2376 if (
2377 empty( $subscription_cancellation_dialog_box_template_params ) &&
2378 ! $show_deactivation_feedback_form &&
2379 empty( $uninstall_confirmation_message )
2380 ) {
2381 return;
2382 }
2383
2384 $vars = array( 'id' => $this->_module_id );
2385
2386 if ( $show_deactivation_feedback_form ) {
2387 /* Check the type of user:
2388 * 1. Long-term (long-term)
2389 * 2. Non-registered and non-anonymous short-term (non-registered-and-non-anonymous-short-term).
2390 * 3. Short-term (short-term)
2391 */
2392 $is_long_term_user = true;
2393
2394 // Check if the site is at least 2 days old.
2395 $time_installed = $this->_storage->install_timestamp;
2396
2397 // Difference in seconds.
2398 $date_diff = time() - $time_installed;
2399
2400 // Convert seconds to days.
2401 $date_diff_days = floor( $date_diff / ( 60 * 60 * 24 ) );
2402
2403 if ( $date_diff_days < 2 ) {
2404 $is_long_term_user = false;
2405 }
2406
2407 $is_long_term_user = $this->apply_filters( 'is_long_term_user', $is_long_term_user );
2408
2409 if ( $is_long_term_user ) {
2410 $user_type = 'long-term';
2411 } else {
2412 if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
2413 $user_type = 'non-registered-and-non-anonymous-short-term';
2414 } else {
2415 $user_type = 'short-term';
2416 }
2417 }
2418
2419 $uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
2420
2421 $vars['reasons'] = $uninstall_reasons;
2422 }
2423
2424 $vars['subscription_cancellation_dialog_box_template_params'] = &$subscription_cancellation_dialog_box_template_params;
2425 $vars['show_deactivation_feedback_form'] = $show_deactivation_feedback_form;
2426 $vars['uninstall_confirmation_message'] = $uninstall_confirmation_message;
2427
2428 /**
2429 * Load the HTML template for the deactivation feedback dialog box.
2430 *
2431 * @todo Deactivation form core functions should be loaded only once! Otherwise, when there are multiple Freemius powered plugins the same code is loaded multiple times. The only thing that should be loaded differently is the various deactivation reasons object based on the state of the plugin.
2432 */
2433 fs_require_template( 'forms/deactivation/form.php', $vars );
2434 }
2435
2436 /**
2437 * @author Leo Fajardo (@leorw)
2438 * @since 1.1.2
2439 *
2440 * @param string $user_type
2441 *
2442 * @return array The uninstall reasons for the specified user type.
2443 */
2444 function _get_uninstall_reasons( $user_type = 'long-term' ) {
2445 $module_type = $this->_module_type;
2446
2447 $internal_message_template_var = array(
2448 'id' => $this->_module_id
2449 );
2450
2451 $plan = $this->get_plan();
2452
2453 if ( $this->is_registered() && is_object( $plan ) && $plan->has_technical_support() ) {
2454 $contact_support_template = fs_get_template( 'forms/deactivation/contact.php', $internal_message_template_var );
2455 } else {
2456 $contact_support_template = '';
2457 }
2458
2459 $reason_found_better_plugin = array(
2460 'id' => self::REASON_FOUND_A_BETTER_PLUGIN,
2461 'text' => sprintf( $this->get_text_inline( 'I found a better %s', 'reason-found-a-better-plugin' ), $module_type ),
2462 'input_type' => 'textfield',
2463 'input_placeholder' => sprintf( $this->get_text_inline( "What's the %s's name?", 'placeholder-plugin-name' ), $module_type ),
2464 );
2465
2466 $reason_temporary_deactivation = array(
2467 'id' => self::REASON_TEMPORARY_DEACTIVATION,
2468 'text' => sprintf(
2469 $this->get_text_inline( "It's a temporary %s - I'm troubleshooting an issue", 'reason-temporary-x' ),
2470 strtolower( $this->is_plugin() ?
2471 $this->get_text_inline( 'Deactivation', 'deactivation' ) :
2472 $this->get_text_inline( 'Theme Switch', 'theme-switch' )
2473 )
2474 ),
2475 'input_type' => '',
2476 'input_placeholder' => ''
2477 );
2478
2479 $reason_other = array(
2480 'id' => self::REASON_OTHER,
2481 'text' => $this->get_text_inline( 'Other', 'reason-other' ),
2482 'input_type' => 'textfield',
2483 'input_placeholder' => ''
2484 );
2485
2486 $long_term_user_reasons = array(
2487 array(
2488 'id' => self::REASON_NO_LONGER_NEEDED,
2489 'text' => sprintf( $this->get_text_inline( 'I no longer need the %s', 'reason-no-longer-needed' ), $module_type ),
2490 'input_type' => '',
2491 'input_placeholder' => ''
2492 ),
2493 $reason_found_better_plugin,
2494 array(
2495 'id' => self::REASON_NEEDED_FOR_A_SHORT_PERIOD,
2496 'text' => sprintf( $this->get_text_inline( 'I only needed the %s for a short period', 'reason-needed-for-a-short-period' ), $module_type ),
2497 'input_type' => '',
2498 'input_placeholder' => ''
2499 ),
2500 array(
2501 'id' => self::REASON_BROKE_MY_SITE,
2502 'text' => sprintf( $this->get_text_inline( 'The %s broke my site', 'reason-broke-my-site' ), $module_type ),
2503 'input_type' => '',
2504 'input_placeholder' => '',
2505 'internal_message' => $contact_support_template
2506 ),
2507 array(
2508 'id' => self::REASON_SUDDENLY_STOPPED_WORKING,
2509 'text' => sprintf( $this->get_text_inline( 'The %s suddenly stopped working', 'reason-suddenly-stopped-working' ), $module_type ),
2510 'input_type' => '',
2511 'input_placeholder' => '',
2512 'internal_message' => $contact_support_template
2513 )
2514 );
2515
2516 if ( $this->is_paying() ) {
2517 $long_term_user_reasons[] = array(
2518 'id' => self::REASON_CANT_PAY_ANYMORE,
2519 'text' => $this->get_text_inline( "I can't pay for it anymore", 'reason-cant-pay-anymore' ),
2520 'input_type' => 'textfield',
2521 'input_placeholder' => $this->get_text_inline( 'What price would you feel comfortable paying?', 'placeholder-comfortable-price' )
2522 );
2523 }
2524
2525 $reason_dont_share_info = array(
2526 'id' => self::REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION,
2527 'text' => $this->get_text_inline( "I don't like to share my information with you", 'reason-dont-like-to-share-my-information' ),
2528 'input_type' => '',
2529 'input_placeholder' => ''
2530 );
2531
2532 /**
2533 * If the current user has selected the "don't share data" reason in the deactivation feedback modal, inform the
2534 * user by showing additional message that he doesn't have to share data and can just choose to skip the opt-in
2535 * (the Skip button is included in the message to show). This message will only be shown if anonymous mode is
2536 * enabled and the user's account is currently not in pending activation state (similar to the way the Skip
2537 * button in the opt-in form is shown/hidden).
2538 */
2539 if ( $this->is_enable_anonymous() && ! $this->is_pending_activation() ) {
2540 $reason_dont_share_info['internal_message'] = fs_get_template( 'forms/deactivation/retry-skip.php', $internal_message_template_var );
2541 }
2542
2543 $uninstall_reasons = array(
2544 'long-term' => $long_term_user_reasons,
2545 'non-registered-and-non-anonymous-short-term' => array(
2546 array(
2547 'id' => self::REASON_DIDNT_WORK,
2548 'text' => sprintf( $this->get_text_inline( "The %s didn't work", 'reason-didnt-work' ), $module_type ),
2549 'input_type' => '',
2550 'input_placeholder' => ''
2551 ),
2552 $reason_dont_share_info,
2553 $reason_found_better_plugin
2554 ),
2555 'short-term' => array(
2556 array(
2557 'id' => self::REASON_COULDNT_MAKE_IT_WORK,
2558 'text' => $this->get_text_inline( "I couldn't understand how to make it work", 'reason-couldnt-make-it-work' ),
2559 'input_type' => '',
2560 'input_placeholder' => '',
2561 'internal_message' => $contact_support_template
2562 ),
2563 $reason_found_better_plugin,
2564 array(
2565 'id' => self::REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE,
2566 'text' => sprintf( $this->get_text_inline( "The %s is great, but I need specific feature that you don't support", 'reason-great-but-need-specific-feature' ), $module_type ),
2567 'input_type' => 'textarea',
2568 'input_placeholder' => $this->get_text_inline( 'What feature?', 'placeholder-feature' )
2569 ),
2570 array(
2571 'id' => self::REASON_NOT_WORKING,
2572 'text' => sprintf( $this->get_text_inline( 'The %s is not working', 'reason-not-working' ), $module_type ),
2573 'input_type' => 'textarea',
2574 'input_placeholder' => $this->get_text_inline( "Kindly share what didn't work so we can fix it for future users...", 'placeholder-share-what-didnt-work' )
2575 ),
2576 array(
2577 'id' => self::REASON_NOT_WHAT_I_WAS_LOOKING_FOR,
2578 'text' => $this->get_text_inline( "It's not what I was looking for", 'reason-not-what-i-was-looking-for' ),
2579 'input_type' => 'textarea',
2580 'input_placeholder' => $this->get_text_inline( "What you've been looking for?", 'placeholder-what-youve-been-looking-for' )
2581 ),
2582 array(
2583 'id' => self::REASON_DIDNT_WORK_AS_EXPECTED,
2584 'text' => sprintf( $this->get_text_inline( "The %s didn't work as expected", 'reason-didnt-work-as-expected' ), $module_type ),
2585 'input_type' => 'textarea',
2586 'input_placeholder' => $this->get_text_inline( 'What did you expect?', 'placeholder-what-did-you-expect' )
2587 )
2588 )
2589 );
2590
2591 // Randomize the reasons for the current user type.
2592 shuffle( $uninstall_reasons[ $user_type ] );
2593
2594 // Keep the following reasons as the last items in the list.
2595 $uninstall_reasons[ $user_type ][] = $reason_temporary_deactivation;
2596 $uninstall_reasons[ $user_type ][] = $reason_other;
2597
2598 $uninstall_reasons = $this->apply_filters( 'uninstall_reasons', $uninstall_reasons );
2599
2600 return $uninstall_reasons[ $user_type ];
2601 }
2602
2603 /**
2604 * Called after the user has submitted his reason for deactivating the plugin.
2605 *
2606 * @author Leo Fajardo (@leorw)
2607 * @since 1.1.2
2608 */
2609 function _submit_uninstall_reason_action() {
2610 $this->_logger->entrance();
2611
2612 $this->check_ajax_referer( 'submit_uninstall_reason' );
2613
2614 $reason_id = fs_request_get( 'reason_id' );
2615
2616 // Check if the given reason ID is an unsigned integer.
2617 if ( ! ctype_digit( $reason_id ) ) {
2618 exit;
2619 }
2620
2621 $reason_info = trim( fs_request_get( 'reason_info', '' ) );
2622 if ( ! empty( $reason_info ) ) {
2623 $reason_info = substr( $reason_info, 0, 128 );
2624 }
2625
2626 $reason = (object) array(
2627 'id' => $reason_id,
2628 'info' => $reason_info,
2629 'is_anonymous' => fs_request_get_bool( 'is_anonymous' )
2630 );
2631
2632 $this->_storage->store( 'uninstall_reason', $reason );
2633
2634 if ( self::REASON_TEMPORARY_DEACTIVATION == $reason->id ) {
2635 $snooze_period = fs_request_get( 'snooze_period' );
2636
2637 if ( is_numeric( $snooze_period ) && 0 < $snooze_period ) {
2638 self::snooze_deactivation_form( (int) $snooze_period );
2639 }
2640 }
2641
2642 /**
2643 * If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do
2644 * not support uninstall hook.
2645 *
2646 * @author Leo Fajardo (@leorw)
2647 * @since 1.2.2
2648 */
2649 if ( $this->is_theme() ) {
2650 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
2651 FS_Plugin_Updater::instance( $this )->delete_update_data();
2652 }
2653
2654 $this->_uninstall_plugin_event( false );
2655 $this->remove_sdk_reference();
2656 }
2657
2658 // Print '1' for successful operation.
2659 echo 1;
2660 exit;
2661 }
2662
2663 #--------------------------------------------------------------------------------
2664 #region Deactivation Feedback Snoozing
2665 #--------------------------------------------------------------------------------
2666
2667 /**
2668 * @author Vova Feldman (@svovaf)
2669 * @since 2.4.3
2670 *
2671 * @param int $period
2672 *
2673 * @return bool True if the value was set, false otherwise.
2674 */
2675 private static function snooze_deactivation_form( $period ) {
2676 return ( 0 < $period && self::reset_deactivation_snoozing( $period ) );
2677 }
2678
2679 /**
2680 * Check if deactivation feedback form is snoozed.
2681 *
2682 * @author Vova Feldman (@svovaf)
2683 * @since 2.4.3
2684 *
2685 * @return bool
2686 */
2687 static function is_deactivation_snoozed() {
2688 $is_snoozed = ( ! is_multisite() || fs_is_network_admin() ) ?
2689 get_transient( 'fs_snooze_period' ) :
2690 get_site_transient( 'fs_snooze_period' );
2691
2692
2693 return ( 'true' === $is_snoozed );
2694 }
2695
2696 /**
2697 * Reset deactivation snoozing. When `$period` is `0` will stop deactivation snoozing by deleting the transients. Otherwise, will set the transients for the selected period.
2698 *
2699 * @param int $period Period in seconds.
2700 *
2701 * @author Vova Feldman (@svovaf)
2702 * @since 2.4.3
2703 */
2704 private static function reset_deactivation_snoozing( $period = 0 ) {
2705 $value = ( 0 === $period ) ? null : 'true';
2706
2707 if ( ! is_multisite() || fs_is_network_admin() ) {
2708 return set_transient( 'fs_snooze_period', $value, $period );
2709 } else {
2710 return set_site_transient( 'fs_snooze_period', $value, $period );
2711 }
2712 }
2713
2714 /**
2715 * The deactivation snooze expiration UNIX timestamp (in sec).
2716 *
2717 * @author Vova Feldman (@svovaf)
2718 * @since 2.4.3
2719 *
2720 * @return int
2721 */
2722 static function deactivation_snooze_expires_at() {
2723 return ( ! is_multisite() || fs_is_network_admin() ) ?
2724 (int) get_option( '_transient_timeout_fs_snooze_period' ) :
2725 (int) get_site_option( '_site_transient_timeout_fs_snooze_period' );
2726 }
2727
2728 #endregion
2729
2730 /**
2731 * @author Leo Fajardo (@leorw)
2732 * @since 2.1.4
2733 */
2734 function cancel_subscription_or_trial_ajax_action() {
2735 $this->_logger->entrance();
2736
2737 $this->check_ajax_referer( 'cancel_subscription_or_trial' );
2738
2739 $result = $this->cancel_subscription_or_trial( fs_request_get( 'plugin_id', $this->get_id() ), false );
2740
2741 if ( $this->is_api_error( $result ) ) {
2742 $this->shoot_ajax_failure( $result->error->message );
2743 }
2744
2745 $this->shoot_ajax_success();
2746 }
2747
2748 /**
2749 * @author Leo Fajardo (@leorw)
2750 * @since 2.1.4
2751 *
2752 * @param number $plugin_id
2753 *
2754 * @return object
2755 */
2756 private function cancel_subscription_or_trial( $plugin_id ) {
2757 $fs = null;
2758 if ( $plugin_id == $this->get_id() ) {
2759 $fs = $this;
2760 } else if ( $this->is_addon_activated( $plugin_id ) ) {
2761 $fs = self::get_instance_by_id( $plugin_id );
2762 }
2763
2764 $result = null;
2765
2766 if ( ! is_null( $fs ) ) {
2767 $result = $fs->is_paid_trial() ?
2768 $fs->_cancel_trial() :
2769 $fs->_downgrade_site();
2770 }
2771
2772 return $result;
2773 }
2774
2775 /**
2776 * @author Leo Fajardo (@leorw)
2777 * @since 2.0.2
2778 */
2779 function _delete_theme_update_data_action() {
2780 FS_Plugin_Updater::instance( $this )->delete_update_data();
2781 }
2782
2783 #endregion
2784
2785 #----------------------------------------------------------------------------------
2786 #region Instance
2787 #----------------------------------------------------------------------------------
2788
2789 /**
2790 * Main singleton instance.
2791 *
2792 * @author Vova Feldman (@svovaf)
2793 * @since 1.0.0
2794 *
2795 * @param number $module_id
2796 * @param string|bool $slug
2797 * @param bool $is_init Is initiation sequence.
2798 *
2799 * @return Freemius|false
2800 */
2801 static function instance( $module_id, $slug = false, $is_init = false ) {
2802 if ( empty( $module_id ) ) {
2803 return false;
2804 }
2805
2806 /**
2807 * Load the essential static data prior to initiating FS_Plugin_Manager since there's an essential MS network migration logic that needs to be executed prior to the initiation.
2808 */
2809 self::_load_required_static();
2810
2811 if ( ! is_numeric( $module_id ) ) {
2812 if ( ! $is_init && true === $slug ) {
2813 $is_init = true;
2814 }
2815
2816 $slug = $module_id;
2817
2818 $module = FS_Plugin_Manager::instance( $slug )->get();
2819
2820 if ( is_object( $module ) ) {
2821 $module_id = $module->id;
2822 }
2823 }
2824
2825 $key = 'm_' . $module_id;
2826
2827 if ( ! isset( self::$_instances[ $key ] ) ) {
2828 self::$_instances[ $key ] = new Freemius( $module_id, $slug, $is_init );
2829 }
2830
2831 return self::$_instances[ $key ];
2832 }
2833
2834 /**
2835 * @author Vova Feldman (@svovaf)
2836 * @since 1.0.6
2837 *
2838 * @param number $addon_id
2839 *
2840 * @return bool
2841 */
2842 private static function has_instance( $addon_id ) {
2843 return isset( self::$_instances[ 'm_' . $addon_id ] );
2844 }
2845
2846 /**
2847 * @author Leo Fajardo (@leorw)
2848 * @since 1.2.2
2849 *
2850 * @param string|number $id_or_slug
2851 * @param string $module_type
2852 *
2853 * @return number|false
2854 */
2855 private static function get_module_id( $id_or_slug, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2856 if ( is_numeric( $id_or_slug ) ) {
2857 return $id_or_slug;
2858 }
2859
2860 foreach ( self::$_instances as $instance ) {
2861 // Also check the module type since there can be a plugin and a theme with the same slug.
2862 if ( ( $module_type === $instance->get_module_type() ) && ( $id_or_slug === $instance->get_slug() ) ) {
2863 return $instance->get_id();
2864 }
2865 }
2866
2867 return false;
2868 }
2869
2870 /**
2871 * @author Vova Feldman (@svovaf)
2872 * @since 1.0.6
2873 *
2874 * @param number $id
2875 *
2876 * @return false|Freemius
2877 */
2878 static function get_instance_by_id( $id ) {
2879 return isset ( self::$_instances[ 'm_' . $id ] ) ?
2880 self::$_instances[ 'm_' . $id ] :
2881 false;
2882 }
2883
2884 /**
2885 *
2886 * @author Vova Feldman (@svovaf)
2887 * @since 1.0.1
2888 *
2889 * @param string $plugin_file
2890 * @param string $module_type
2891 *
2892 * @return false|Freemius
2893 */
2894 static function get_instance_by_file( $plugin_file, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2895 $slug = self::find_slug_by_basename( $plugin_file );
2896
2897 return ( false !== $slug ) ?
2898 self::instance( self::get_module_id( $slug, $module_type ) ) :
2899 false;
2900 }
2901
2902 /**
2903 * @author Vova Feldman (@svovaf)
2904 * @since 1.0.6
2905 *
2906 * @return false|Freemius
2907 */
2908 function get_parent_instance() {
2909 return self::get_instance_by_id( $this->_plugin->parent_plugin_id );
2910 }
2911
2912 /**
2913 * @author Vova Feldman (@svovaf)
2914 * @since 1.0.6
2915 *
2916 * @param string|number $id_or_slug
2917 *
2918 * @return false|Freemius
2919 */
2920 function get_addon_instance( $id_or_slug ) {
2921 $addon_id = self::get_module_id( $id_or_slug );
2922
2923 return self::instance( $addon_id );
2924 }
2925
2926 /**
2927 * @return Freemius[]
2928 */
2929 static function _get_all_instances() {
2930 return self::$_instances;
2931 }
2932
2933 #endregion ------------------------------------------------------------------
2934
2935 /**
2936 * @author Vova Feldman (@svovaf)
2937 * @since 1.0.6
2938 *
2939 * @return bool
2940 */
2941 function is_parent_plugin_installed() {
2942 $is_active = self::has_instance( $this->_plugin->parent_plugin_id );
2943
2944 if ( $is_active ) {
2945 return true;
2946 }
2947
2948 /**
2949 * Parent module might be a theme. If that's the case, the add-on's FS
2950 * instance will be loaded prior to the theme's FS instance, therefore,
2951 * we need to check if it's active with a "look ahead".
2952 *
2953 * @author Vova Feldman
2954 * @since 1.2.2.3
2955 */
2956 global $fs_active_plugins;
2957 if ( is_object( $fs_active_plugins ) && is_array( $fs_active_plugins->plugins ) ) {
2958 $active_theme = wp_get_theme();
2959
2960 foreach ( $fs_active_plugins->plugins as $sdk => $module ) {
2961 if ( WP_FS__MODULE_TYPE_THEME === $module->type ) {
2962 if ( $module->plugin_path == $active_theme->get_stylesheet() ) {
2963 // Parent module is a theme and it's currently active.
2964 return true;
2965 }
2966 }
2967 }
2968 }
2969
2970 return false;
2971 }
2972
2973 /**
2974 * Check if add-on parent plugin in activation mode.
2975 *
2976 * @author Vova Feldman (@svovaf)
2977 * @since 1.0.7
2978 *
2979 * @return bool
2980 */
2981 function is_parent_in_activation() {
2982 $parent_fs = $this->get_parent_instance();
2983 if ( ! is_object( $parent_fs ) ) {
2984 return false;
2985 }
2986
2987 return ( $parent_fs->is_activation_mode() );
2988 }
2989
2990 /**
2991 * Is plugin in activation mode.
2992 *
2993 * @author Vova Feldman (@svovaf)
2994 * @since 1.0.7
2995 *
2996 * @param bool $and_on
2997 *
2998 * @return bool
2999 */
3000 function is_activation_mode( $and_on = true ) {
3001 return fs_is_network_admin() ?
3002 $this->is_network_activation_mode( $and_on ) :
3003 $this->is_site_activation_mode( $and_on );
3004 }
3005
3006 /**
3007 * Is plugin in activation mode.
3008 *
3009 * @author Vova Feldman (@svovaf)
3010 * @since 1.0.7
3011 *
3012 * @param bool $and_on
3013 *
3014 * @return bool
3015 */
3016 function is_site_activation_mode( $and_on = true ) {
3017 return (
3018 ( $this->is_on() || ! $and_on ) &&
3019 (
3020 ( $this->is_premium() && true === $this->_storage->require_license_activation ) ||
3021 (
3022 ( ! $this->is_registered() ||
3023 ( $this->is_only_premium() && ! $this->has_features_enabled_license() ) ) &&
3024 ( ! $this->is_enable_anonymous() ||
3025 ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) )
3026 )
3027 )
3028 );
3029 }
3030
3031 /**
3032 * Checks if the SDK in network activation mode.
3033 *
3034 * @author Leo Fajardo (@leorw)
3035 * @since 2.0.0
3036 *
3037 * @param bool $and_on
3038 *
3039 * @return bool
3040 */
3041 private function is_network_activation_mode( $and_on = true ) {
3042 if ( ! $this->_is_network_active ) {
3043 // Not network activated.
3044 return false;
3045 }
3046
3047 if ( $this->is_network_upgrade_mode() ) {
3048 // Special flag to enforce network activation mode to decide what to do with the sites that are not yet opted-in nor skipped.
3049 return true;
3050 }
3051
3052 if ( ! $this->is_site_activation_mode( $and_on ) ) {
3053 // Whether the context is single site or the network, if the plugin is no longer in activation mode then it is not in network activation mode as well.
3054 return false;
3055 }
3056
3057 if ( $this->is_network_delegated_connection() ) {
3058 // Super-admin delegated the connection to the site admins -> not activation mode.
3059 return false;
3060 }
3061
3062 if ( $this->is_network_anonymous() && true !== $this->_storage->require_license_activation ) {
3063 // Super-admin skipped the connection network wide -> not activation mode.
3064 return false;
3065 }
3066
3067 if ( $this->is_network_registered() ) {
3068 // Super-admin connected at least one site -> not activation mode.
3069 return false;
3070 }
3071
3072 return true;
3073 }
3074
3075 /**
3076 * Check if current page is the opt-in/pending-activation page.
3077 *
3078 * @author Vova Feldman (@svovaf)
3079 * @since 1.2.1.7
3080 *
3081 * @return bool
3082 */
3083 function is_activation_page() {
3084 if ( $this->_menu->is_activation_page( $this->show_opt_in_on_themes_page() ) ) {
3085 return true;
3086 }
3087
3088 if ( ! $this->is_activation_mode() ) {
3089 return false;
3090 }
3091
3092 // Check if current page is matching the activation page.
3093 return $this->is_matching_url( $this->get_activation_url() );
3094 }
3095
3096 /**
3097 * Check if URL path's are matching and that all querystring
3098 * arguments of the $sub_url exist in the $url with the same values.
3099 *
3100 * WARNING:
3101 * 1. This method doesn't check if the sub/domain are matching.
3102 * 2. Ignore case sensitivity.
3103 *
3104 * @author Vova Feldman (@svovaf)
3105 * @since 1.2.1.7
3106 *
3107 * @param string $sub_url
3108 * @param string $url If argument is not set, check if the sub_url matching the current's page URL.
3109 *
3110 * @return bool
3111 */
3112 private function is_matching_url( $sub_url, $url = '' ) {
3113 if ( empty( $url ) ) {
3114 $url = $_SERVER['REQUEST_URI'];
3115 }
3116
3117 $url = strtolower( $url );
3118 $sub_url = strtolower( $sub_url );
3119
3120 if ( parse_url( $sub_url, PHP_URL_PATH ) !== parse_url( $url, PHP_URL_PATH ) ) {
3121 // Different path - DO NOT OVERRIDE PAGE.
3122 return false;
3123 }
3124
3125 $url_params = fs_parse_url_params( $url );
3126 $sub_url_params = fs_parse_url_params( $sub_url );
3127
3128 foreach ( $sub_url_params as $key => $val ) {
3129 if ( ! isset( $url_params[ $key ] ) || $val != $url_params[ $key ] ) {
3130 // Not matching query string - DO NOT OVERRIDE PAGE.
3131 return false;
3132 }
3133 }
3134
3135 return true;
3136 }
3137
3138 /**
3139 * Get the basenames of all active plugins for specific blog. Including network activated plugins.
3140 *
3141 * @author Vova Feldman (@svovaf)
3142 * @since 2.0.0
3143 *
3144 * @param int $blog_id
3145 *
3146 * @return string[]
3147 */
3148 private static function get_active_plugins_basenames( $blog_id = 0 ) {
3149 if ( is_multisite() && $blog_id > 0 ) {
3150 $active_basenames = get_blog_option( $blog_id, 'active_plugins' );
3151 } else {
3152 $active_basenames = get_option( 'active_plugins' );
3153 }
3154
3155 if ( ! is_array( $active_basenames ) ) {
3156 $active_basenames = array();
3157 }
3158
3159 if ( is_multisite() ) {
3160 $network_active_basenames = get_site_option( 'active_sitewide_plugins' );
3161
3162 if ( is_array( $network_active_basenames ) && ! empty( $network_active_basenames ) ) {
3163 $active_basenames = array_merge( $active_basenames, array_keys( $network_active_basenames ) );
3164 }
3165 }
3166
3167 return $active_basenames;
3168 }
3169
3170 /**
3171 * @author Leo Fajardo (@leorw)
3172 * @since 2.3.0
3173 *
3174 * @param int $blog_id
3175 *
3176 * @return array
3177 */
3178 static function get_active_plugins_directories_map( $blog_id = 0 ) {
3179 $active_basenames = self::get_active_plugins_basenames( $blog_id );
3180
3181 $map = array();
3182
3183 foreach ( $active_basenames as $active_basename ) {
3184 $active_basename = fs_normalize_path( $active_basename );
3185
3186 if ( false === strpos( $active_basename, '/' ) ) {
3187 continue;
3188 }
3189
3190 $map[ dirname( $active_basename ) ] = true;
3191 }
3192
3193 return $map;
3194 }
3195
3196 /**
3197 * Get collection of all active plugins. Including network activated plugins.
3198 *
3199 * @author Vova Feldman (@svovaf)
3200 * @since 1.0.9
3201 *
3202 * @param int $blog_id Since 2.0.0
3203 *
3204 * @return array[string]array
3205 */
3206 private static function get_active_plugins( $blog_id = 0 ) {
3207 self::require_plugin_essentials();
3208
3209 $active_plugin = array();
3210 $all_plugins = fs_get_plugins();
3211 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3212
3213 foreach ( $active_plugins_basenames as $plugin_basename ) {
3214 $active_plugin[ $plugin_basename ] = $all_plugins[ $plugin_basename ];
3215 }
3216
3217 return $active_plugin;
3218 }
3219
3220 /**
3221 * Get collection of all site active plugins for a specified blog.
3222 *
3223 * @author Vova Feldman (@svovaf)
3224 * @since 2.0.0
3225 *
3226 * @param int $blog_id
3227 *
3228 * @return array[string]array
3229 */
3230 private static function get_site_active_plugins( $blog_id = 0 ) {
3231 $active_basenames = ( is_multisite() && $blog_id > 0 ) ?
3232 get_blog_option( $blog_id, 'active_plugins' ) :
3233 get_option( 'active_plugins' );
3234
3235 $active = array();
3236
3237 if ( ! is_array( $active_basenames ) ) {
3238 return $active;
3239 }
3240
3241 foreach ( $active_basenames as $basename ) {
3242 $active[ $basename ] = array(
3243 'is_active' => true,
3244 'Version' => '1.0', // Dummy version.
3245 'slug' => self::get_plugin_slug( $basename ),
3246 );
3247 }
3248
3249 return $active;
3250 }
3251
3252 /**
3253 * Get collection of all plugins with their activation status for a specified blog.
3254 *
3255 * @author Vova Feldman (@svovaf)
3256 * @since 1.1.8
3257 *
3258 * @param int $blog_id Since 2.0.0
3259 *
3260 * @return array Key is the plugin file path and the value is an array of the plugin data.
3261 */
3262 private static function get_all_plugins( $blog_id = 0 ) {
3263 self::require_plugin_essentials();
3264
3265 $all_plugins = fs_get_plugins();
3266
3267 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3268
3269 foreach ( $all_plugins as $basename => &$data ) {
3270 // By default set to inactive (next foreach update the active plugins).
3271 $data['is_active'] = false;
3272 // Enrich with plugin slug.
3273 $data['slug'] = self::get_plugin_slug( $basename );
3274 }
3275
3276 // Flag active plugins.
3277 foreach ( $active_plugins_basenames as $basename ) {
3278 if ( isset( $all_plugins[ $basename ] ) ) {
3279 $all_plugins[ $basename ]['is_active'] = true;
3280 }
3281 }
3282
3283 return $all_plugins;
3284 }
3285
3286 /**
3287 * Get collection of all plugins and if they are network level activated.
3288 *
3289 * @author Vova Feldman (@svovaf)
3290 * @since 2.0.0
3291 *
3292 * @return array Key is the plugin basename and the value is an array of the plugin data.
3293 */
3294 private static function get_network_plugins() {
3295 self::require_plugin_essentials();
3296
3297 $all_plugins = fs_get_plugins();
3298
3299 $network_active_basenames = is_multisite() ?
3300 get_site_option( 'active_sitewide_plugins' ) :
3301 array();
3302
3303 foreach ( $all_plugins as $basename => &$data ) {
3304 // By default set to inactive (next foreach update the active plugins).
3305 $data['is_active'] = false;
3306 // Enrich with plugin slug.
3307 $data['slug'] = self::get_plugin_slug( $basename );
3308 }
3309
3310 // Flag active plugins.
3311 foreach ( $network_active_basenames as $basename ) {
3312 if ( isset( $all_plugins[ $basename ] ) ) {
3313 $all_plugins[ $basename ]['is_active'] = true;
3314 }
3315 }
3316
3317 return $all_plugins;
3318 }
3319
3320 /**
3321 * Cached result of get_site_transient( 'update_plugins' )
3322 *
3323 * @author Vova Feldman (@svovaf)
3324 * @since 1.1.8
3325 *
3326 * @var object
3327 */
3328 private static $_plugins_info;
3329
3330 /**
3331 * Helper function to get specified plugin's slug.
3332 *
3333 * @author Vova Feldman (@svovaf)
3334 * @since 1.1.8
3335 *
3336 * @param $basename
3337 *
3338 * @return string
3339 */
3340 private static function get_plugin_slug( $basename ) {
3341 if ( ! isset( self::$_plugins_info ) ) {
3342 self::$_plugins_info = get_site_transient( 'update_plugins' );
3343 }
3344
3345 $slug = '';
3346
3347 if ( is_object( self::$_plugins_info ) ) {
3348 if ( isset( self::$_plugins_info->no_update ) &&
3349 isset( self::$_plugins_info->no_update[ $basename ] ) &&
3350 ! empty( self::$_plugins_info->no_update[ $basename ]->slug )
3351 ) {
3352 $slug = self::$_plugins_info->no_update[ $basename ]->slug;
3353 } else if ( isset( self::$_plugins_info->response ) &&
3354 isset( self::$_plugins_info->response[ $basename ] ) &&
3355 ! empty( self::$_plugins_info->response[ $basename ]->slug )
3356 ) {
3357 $slug = self::$_plugins_info->response[ $basename ]->slug;
3358 }
3359 }
3360
3361 if ( empty( $slug ) ) {
3362 // Try to find slug from FS data.
3363 $slug = self::find_slug_by_basename( $basename );
3364 }
3365
3366 if ( empty( $slug ) ) {
3367 // Fallback to plugin's folder name.
3368 $slug = dirname( $basename );
3369 }
3370
3371 return $slug;
3372 }
3373
3374 private static $_statics_loaded = false;
3375
3376 /**
3377 * Load static resources.
3378 *
3379 * @author Vova Feldman (@svovaf)
3380 * @since 1.0.1
3381 */
3382 private static function _load_required_static() {
3383 if ( self::$_statics_loaded ) {
3384 return;
3385 }
3386
3387 self::$_static_logger = FS_Logger::get_logger( WP_FS__SLUG, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
3388
3389 self::$_static_logger->entrance();
3390
3391 self::$_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
3392
3393 if ( is_multisite() ) {
3394 $has_skipped_migration = (
3395 // 'id_slug_type_path_map' - was never stored on older versions, therefore, not exists on the site level.
3396 null === self::$_accounts->get_option( 'id_slug_type_path_map', null, false ) &&
3397 // 'file_slug_map' stored on the site level, so it was running an SDK version before it was integrated with MS-network.
3398 null !== self::$_accounts->get_option( 'file_slug_map', null, false )
3399 );
3400
3401 /**
3402 * If the file_slug_map exists on the site level but doesn't exist on the
3403 * network level storage, it means that we need to process the storage with migration.
3404 *
3405 * The code in this `if` scope will only be executed once and only for the first site that will execute it because once we migrate the storage data, file_slug_map will be already set in the network level storage.
3406 *
3407 * @author Vova Feldman (@svovaf)
3408 * @since 2.0.0
3409 */
3410 if (
3411 ( $has_skipped_migration && true !== self::$_accounts->get_option( 'ms_migration_complete', false, true ) ) ||
3412 ( null === self::$_accounts->get_option( 'file_slug_map', null, true ) &&
3413 null !== self::$_accounts->get_option( 'file_slug_map', null, false ) )
3414 ) {
3415 self::migrate_options_to_network();
3416 }
3417 }
3418
3419 self::$_global_admin_notices = FS_Admin_Notices::instance( 'global' );
3420
3421 if ( ! WP_FS__DEMO_MODE ) {
3422 add_action( ( fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu', array(
3423 'Freemius',
3424 '_add_debug_section'
3425 ) );
3426 }
3427
3428 add_action( "wp_ajax_fs_toggle_debug_mode", array( 'Freemius', '_toggle_debug_mode' ) );
3429
3430 self::add_ajax_action_static( 'get_debug_log', array( 'Freemius', '_get_debug_log' ) );
3431
3432 self::add_ajax_action_static( 'get_db_option', array( 'Freemius', '_get_db_option' ) );
3433
3434 self::add_ajax_action_static( 'set_db_option', array( 'Freemius', '_set_db_option' ) );
3435
3436 if ( 0 == did_action( 'plugins_loaded' ) ) {
3437 add_action( 'plugins_loaded', array( 'Freemius', '_load_textdomain' ), 1 );
3438 }
3439
3440 $clone_manager = FS_Clone_Manager::instance();
3441 add_action( 'init', array( $clone_manager, '_init' ) );
3442
3443 add_action( 'admin_footer', array( 'Freemius', '_open_support_forum_in_new_page' ) );
3444
3445 if ( self::is_plugins_page() || self::is_themes_page() ) {
3446 add_action( 'admin_print_footer_scripts', array( 'Freemius', '_maybe_add_beta_label_styles' ), 9 );
3447
3448 /**
3449 * Specifically use this hook so that the JS event handlers will work properly on the "Themes"
3450 * page.
3451 *
3452 * @author Leo Fajardo (@leorw)
3453 * @since 2.3.0
3454 */
3455 add_action( 'admin_footer-' . self::get_current_page(), array( 'Freemius', '_maybe_add_beta_label_to_plugins_and_handle_confirmation') );
3456 }
3457
3458 self::$_statics_loaded = true;
3459 }
3460
3461 #--------------------------------------------------------------------------------
3462 #region Clone
3463 #--------------------------------------------------------------------------------
3464
3465 /**
3466 * @author Leo Fajardo (@leorw)
3467 * @since 2.5.0
3468 *
3469 * @param bool $only_if_manual_resolution_is_not_hidden
3470 *
3471 * @return bool
3472 */
3473 private function is_unresolved_clone( $only_if_manual_resolution_is_not_hidden = false ) {
3474 if ( ! $this->is_clone( $only_if_manual_resolution_is_not_hidden ) ) {
3475 return false;
3476 }
3477
3478 return FS_Clone_Manager::instance()->has_temporary_duplicate_mode_expired();
3479 }
3480
3481 /**
3482 * @author Leo Fajardo (@leorw)
3483 * @since 2.5.0
3484 *
3485 * @param bool $only_if_manual_resolution_is_not_hidden
3486 */
3487 function is_clone( $only_if_manual_resolution_is_not_hidden = false ) {
3488 if ( ! is_object( $this->_site ) ) {
3489 return false;
3490 }
3491
3492 $blog_id = null;
3493
3494 if (
3495 fs_is_network_admin() &&
3496 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
3497 ) {
3498 // Ensure that we're comparing the network install's URL with the relevant subsite's URL.
3499 $blog_id = $this->_storage->network_install_blog_id;
3500 }
3501
3502 $site_url = Freemius::get_unfiltered_site_url( $blog_id, true, true );
3503
3504 if ( ! $this->_site->is_clone( $site_url ) ) {
3505 return false;
3506 }
3507
3508 return (
3509 ! $only_if_manual_resolution_is_not_hidden ||
3510 ! FS_Clone_Manager::instance()->should_hide_manual_resolution()
3511 );
3512 }
3513
3514 /**
3515 * @author Leo Fajardo (@leorw)
3516 * @since 2.5.0
3517 *
3518 * @param int|null $blog_id
3519 * @param bool $strip_protocol
3520 * @param bool $add_trailing_slash
3521 *
3522 * @return string
3523 */
3524 static function get_unfiltered_site_url( $blog_id = null, $strip_protocol = false, $add_trailing_slash = false ) {
3525 $url = ( ! is_multisite() && defined( 'WP_SITEURL' ) ) ? WP_SITEURL : self::get_site_url_from_wp_option( $blog_id );
3526
3527 if ( $strip_protocol ) {
3528 $url = fs_strip_url_protocol( $url );
3529 }
3530
3531 if ( $add_trailing_slash ) {
3532 $url = trailingslashit( $url );
3533 }
3534
3535 return $url;
3536 }
3537
3538 /**
3539 * @author Leo Fajardo (@leorw)
3540 * @since 2.6.0
3541 *
3542 * @param int|null $blog_id
3543 *
3544 * @return string
3545 */
3546 private static function get_site_url_from_wp_option( $blog_id = null ) {
3547 global $wp_filter;
3548
3549 $site_url_filters = array(
3550 'site_url' => null,
3551 'pre_option_siteurl' => null,
3552 'default_option_siteurl' => null,
3553 'option_siteurl' => null,
3554 );
3555
3556 // Detach all URL-related filters to get the actual site's URL (stripped of potential manipulations by multilingual plugins).
3557 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3558 if ( ! empty( $wp_filter[ $hook_name ] ) ) {
3559 $site_url_filters[ $hook_name ] = $wp_filter[ $hook_name ];
3560 unset( $wp_filter[ $hook_name ] );
3561 }
3562 }
3563
3564 $url = get_site_url( $blog_id );
3565
3566 // Re-attach the filters back.
3567 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3568 if ( ! empty( $site_url_filter ) ) {
3569 $wp_filter[ $hook_name ] = $site_url_filter;
3570 }
3571 }
3572
3573 return $url;
3574 }
3575
3576 /**
3577 * @author Leo Fajardo (@leorw)
3578 * @since 2.5.0
3579 *
3580 * @param number $site_id
3581 */
3582 function fetch_install_by_id( $site_id ) {
3583 return $this->get_current_or_network_user_api_scope()->get( "/installs/{$site_id}.json" );
3584 }
3585
3586 /**
3587 * @author Leo Fajardo (@leorw)
3588 * @since 2.5.0
3589 *
3590 * @return string|object|bool
3591 */
3592 function _handle_long_term_duplicate() {
3593 $this->_logger->entrance();
3594
3595 $this->delete_current_install( false );
3596
3597 $license_key = false;
3598
3599 if (
3600 is_object( $this->_license ) &&
3601 ! $this->_license->is_utilized(
3602 ( WP_FS__IS_LOCALHOST_FOR_SERVER || FS_Site::is_localhost_by_address( self::get_unfiltered_site_url() ) )
3603 )
3604 ) {
3605 $license_key = $this->_license->secret_key;
3606 }
3607
3608 return $this->opt_in(
3609 false,
3610 false,
3611 false,
3612 $license_key,
3613 false,
3614 false,
3615 false,
3616 null,
3617 array(),
3618 false
3619 );
3620 }
3621
3622 #endregion
3623
3624 /**
3625 * @author Leo Fajardo (@leorw)
3626 *
3627 * @since 2.1.3
3628 */
3629 private static function migrate_options_to_network() {
3630 self::migrate_accounts_to_network();
3631
3632 // Migrate API options from site level to network level.
3633 $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
3634 $api_network_options->migrate_to_network();
3635
3636 // Migrate API cache to network level storage.
3637 FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
3638
3639 self::$_accounts->set_option( 'ms_migration_complete', true, true );
3640 }
3641
3642 #----------------------------------------------------------------------------------
3643 #region Localization
3644 #----------------------------------------------------------------------------------
3645
3646 /**
3647 * Load framework's text domain.
3648 *
3649 * @author Vova Feldman (@svovaf)
3650 * @since 1.2.1
3651 */
3652 static function _load_textdomain() {
3653 if ( ! is_admin() ) {
3654 return;
3655 }
3656
3657 global $fs_active_plugins;
3658
3659 // Works both for plugins and themes.
3660 load_plugin_textdomain(
3661 'freemius',
3662 false,
3663 $fs_active_plugins->newest->sdk_path . '/languages/'
3664 );
3665 }
3666
3667 #endregion
3668
3669 #----------------------------------------------------------------------------------
3670 #region Debugging
3671 #----------------------------------------------------------------------------------
3672
3673 /**
3674 * @author Vova Feldman (@svovaf)
3675 * @since 1.0.8
3676 */
3677 static function _add_debug_section() {
3678 if ( ! is_super_admin() ) {
3679 // Add debug page only for super-admins.
3680 return;
3681 }
3682
3683 self::$_static_logger->entrance();
3684
3685 $title = sprintf( '%s [v.%s]', fs_text_inline( 'Freemius Debug' ), WP_FS__SDK_VERSION );
3686
3687 if ( WP_FS__DEV_MODE ) {
3688 // Add top-level debug menu item.
3689 $hook = FS_Admin_Menu_Manager::add_page(
3690 $title,
3691 $title,
3692 'manage_options',
3693 'freemius',
3694 array( 'Freemius', '_debug_page_render' )
3695 );
3696 } else {
3697 // Add hidden debug page.
3698 $hook = FS_Admin_Menu_Manager::add_subpage(
3699 '',
3700 $title,
3701 $title,
3702 'manage_options',
3703 'freemius',
3704 array( 'Freemius', '_debug_page_render' )
3705 );
3706 }
3707
3708 if ( ! empty( $hook ) ) {
3709 add_action( "load-$hook", array( 'Freemius', '_debug_page_actions' ) );
3710 }
3711 }
3712
3713 /**
3714 * @author Vova Feldman (@svovaf)
3715 * @since 1.1.7.3
3716 */
3717 static function _toggle_debug_mode() {
3718 check_admin_referer( 'fs_toggle_debug_mode' );
3719
3720 if ( ! is_super_admin() ) {
3721 return;
3722 }
3723
3724 $is_on = fs_request_get( 'is_on', false, 'post' );
3725
3726 if ( fs_request_is_post() && in_array( $is_on, array( 0, 1 ) ) ) {
3727 update_option( 'fs_debug_mode', $is_on );
3728
3729 // Turn on/off storage logging.
3730 FS_Logger::_set_storage_logging( ( 1 == $is_on ) );
3731 }
3732
3733 exit;
3734 }
3735
3736 /**
3737 * @author Vova Feldman (@svovaf)
3738 * @since 1.2.1.6
3739 */
3740 static function _get_debug_log() {
3741 check_admin_referer( 'fs_get_debug_log' );
3742
3743 if ( ! is_super_admin() ) {
3744 return;
3745 }
3746
3747 $limit = min( ! empty( $_POST['limit'] ) ? absint( $_POST['limit'] ) : 200, 200 );
3748 $offset = min( ! empty( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 200, 200 );
3749
3750 $logs = FS_Logger::load_db_logs(
3751 fs_request_get( 'filters', false, 'post' ),
3752 $limit,
3753 $offset
3754 );
3755
3756 self::shoot_ajax_success( $logs );
3757 }
3758
3759 /**
3760 * @author Vova Feldman (@svovaf)
3761 * @since 1.2.1.7
3762 */
3763 static function _get_db_option() {
3764 check_admin_referer( 'fs_get_db_option' );
3765
3766 $option_name = fs_request_get( 'option_name' );
3767
3768 if ( ! is_super_admin() ||
3769 ! fs_starts_with( $option_name, 'fs_' )
3770 ) {
3771 self::shoot_ajax_failure();
3772 }
3773
3774 $value = get_option( $option_name );
3775
3776 $result = array(
3777 'name' => $option_name,
3778 );
3779
3780 if ( false !== $value ) {
3781 if ( ! is_string( $value ) ) {
3782 $value = json_encode( $value );
3783 }
3784
3785 $result['value'] = $value;
3786 }
3787
3788 self::shoot_ajax_success( $result );
3789 }
3790
3791 /**
3792 * @author Vova Feldman (@svovaf)
3793 * @since 1.2.1.7
3794 */
3795 static function _set_db_option() {
3796 check_admin_referer( 'fs_set_db_option' );
3797
3798 $option_name = fs_request_get( 'option_name' );
3799
3800 if ( ! is_super_admin() ||
3801 ! fs_starts_with( $option_name, 'fs_' )
3802 ) {
3803 self::shoot_ajax_failure();
3804 }
3805
3806 $option_value = fs_request_get_raw( 'option_value' );
3807
3808 if ( ! empty( $option_value ) ) {
3809 update_option( $option_name, $option_value );
3810 }
3811
3812 self::shoot_ajax_success();
3813 }
3814
3815 /**
3816 * @author Vova Feldman (@svovaf)
3817 * @since 1.0.8
3818 */
3819 static function _debug_page_actions() {
3820 self::_clean_admin_content_section();
3821
3822 if ( fs_request_is_action( 'restart_freemius' ) ) {
3823 check_admin_referer( 'restart_freemius' );
3824
3825 if ( ! is_multisite() ) {
3826 // Clear accounts data.
3827 self::$_accounts->clear( null, true );
3828 } else {
3829 $sites = self::get_sites();
3830 foreach ( $sites as $site ) {
3831 $blog_id = self::get_site_blog_id( $site );
3832 self::$_accounts->clear( $blog_id, true );
3833 }
3834
3835 // Clear network level storage.
3836 self::$_accounts->clear( true, true );
3837 }
3838
3839 // Clear SDK reference cache.
3840 delete_option( 'fs_active_plugins' );
3841 } else if ( fs_request_is_action( 'clear_updates_data' ) ) {
3842 check_admin_referer( 'clear_updates_data' );
3843
3844 if ( ! is_multisite() ) {
3845 set_site_transient( 'update_plugins', null );
3846 set_site_transient( 'update_themes', null );
3847 } else {
3848 $current_blog_id = get_current_blog_id();
3849
3850 $sites = self::get_sites();
3851 foreach ( $sites as $site ) {
3852 switch_to_blog( self::get_site_blog_id( $site ) );
3853
3854 set_site_transient( 'update_plugins', null );
3855 set_site_transient( 'update_themes', null );
3856 }
3857
3858 switch_to_blog( $current_blog_id );
3859 }
3860 } else if ( fs_request_is_action( 'reset_deactivation_snoozing' ) ) {
3861 check_admin_referer( 'reset_deactivation_snoozing' );
3862
3863 self::reset_deactivation_snoozing();
3864 } else if ( fs_request_is_action( 'simulate_trial' ) ) {
3865 check_admin_referer( 'simulate_trial' );
3866
3867 $fs = freemius( fs_request_get( 'module_id' ) );
3868
3869 // Update SDK install to at least 24 hours before.
3870 $fs->_storage->install_timestamp = ( time() - WP_FS__TIME_24_HOURS_IN_SEC );
3871 // Unset the trial shown timestamp.
3872 unset( $fs->_storage->trial_promotion_shown );
3873 } else if ( fs_request_is_action( 'simulate_network_upgrade' ) ) {
3874 check_admin_referer( 'simulate_network_upgrade' );
3875
3876 $fs = freemius( fs_request_get( 'module_id' ) );
3877
3878 self::set_network_upgrade_mode( $fs->_storage );
3879 } else if ( fs_request_is_action( 'delete_install' ) ) {
3880 check_admin_referer( 'delete_install' );
3881
3882 self::_delete_site_by_slug(
3883 fs_request_get( 'slug' ),
3884 fs_request_get( 'module_type' ),
3885 true,
3886 fs_request_get( 'blog_id', null )
3887 );
3888 } else if ( fs_request_is_action( 'delete_user' ) ) {
3889 check_admin_referer( 'delete_user' );
3890
3891 self::delete_user( fs_request_get( 'user_id' ) );
3892 } else if ( fs_request_is_action( 'download_logs' ) ) {
3893 check_admin_referer( 'download_logs' );
3894
3895 $download_url = FS_Logger::download_db_logs(
3896 fs_request_get( 'filters', false, 'post' )
3897 );
3898
3899 if ( false === $download_url ) {
3900 wp_die( 'Oops... there was an error while generating the logs download file. Please try again and if it doesn\'t work contact support@freemius.com.' );
3901 }
3902
3903 fs_redirect( $download_url );
3904 } else if ( fs_request_is_action( 'migrate_options_to_network' ) ) {
3905 check_admin_referer( 'migrate_options_to_network' );
3906
3907 self::migrate_options_to_network();
3908 }
3909 }
3910
3911 /**
3912 * @author Leo Fajardo (@leorw)
3913 * @since 2.5.0
3914 *
3915 * @return array
3916 */
3917 static function get_all_modules_sites() {
3918 self::$_static_logger->entrance();
3919
3920 $sites_by_type = array(
3921 WP_FS__MODULE_TYPE_PLUGIN => array(),
3922 WP_FS__MODULE_TYPE_THEME => array(),
3923 );
3924
3925 $module_types = array_keys( $sites_by_type );
3926
3927 if ( ! is_multisite() ) {
3928 foreach ( $module_types as $type ) {
3929 $sites_by_type[ $type ] = self::get_all_sites( $type );
3930
3931 foreach ( $sites_by_type[ $type ] as $slug => $install ) {
3932 $sites_by_type[ $type ][ $slug ] = array( $install );
3933 }
3934 }
3935 } else {
3936 $sites = self::get_sites();
3937
3938 foreach ( $sites as $site ) {
3939 $blog_id = self::get_site_blog_id( $site );
3940
3941 foreach ( $module_types as $type ) {
3942 $installs = self::get_all_sites( $type, $blog_id );
3943
3944 foreach ( $installs as $slug => $install ) {
3945 if ( ! isset( $sites_by_type[ $type ][ $slug ] ) ) {
3946 $sites_by_type[ $type ][ $slug ] = array();
3947 }
3948
3949 $install->blog_id = $blog_id;
3950
3951 $sites_by_type[ $type ][ $slug ][] = $install;
3952 }
3953
3954 }
3955 }
3956 }
3957
3958 return $sites_by_type;
3959 }
3960
3961 /**
3962 * @author Vova Feldman (@svovaf)
3963 * @since 1.0.8
3964 */
3965 static function _debug_page_render() {
3966 self::$_static_logger->entrance();
3967
3968 $all_modules_sites = self::get_all_modules_sites();
3969
3970 $licenses_by_module_type = self::get_all_licenses_by_module_type();
3971
3972 $vars = array(
3973 'plugin_sites' => $all_modules_sites[ WP_FS__MODULE_TYPE_PLUGIN ],
3974 'theme_sites' => $all_modules_sites[ WP_FS__MODULE_TYPE_THEME ],
3975 'users' => self::get_all_users(),
3976 'addons' => self::get_all_addons(),
3977 'account_addons' => self::get_all_account_addons(),
3978 'plugin_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_PLUGIN ],
3979 'theme_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_THEME ]
3980 );
3981
3982 fs_enqueue_local_style( 'fs_debug', '/admin/debug.css' );
3983 fs_require_once_template( 'debug.php', $vars );
3984 }
3985
3986 #endregion
3987
3988 #----------------------------------------------------------------------------------
3989 #region Connectivity Issues
3990 #----------------------------------------------------------------------------------
3991
3992 /**
3993 * Check if Freemius should be turned on for the current plugin install.
3994 *
3995 * Note:
3996 * $this->_is_on is updated in has_api_connectivity()
3997 *
3998 * @author Vova Feldman (@svovaf)
3999 * @since 1.0.9
4000 *
4001 * @return bool
4002 */
4003 function is_on() {
4004 self::$_static_logger->entrance();
4005
4006 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
4007 return false;
4008 }
4009
4010 if ( isset( $this->_is_on ) ) {
4011 return $this->_is_on;
4012 }
4013
4014 // If already installed or pending then sure it's on :)
4015 if ( $this->is_registered() || $this->is_pending_activation() ) {
4016 $this->_is_on = true;
4017
4018 return true;
4019 }
4020
4021 return false;
4022 }
4023
4024 /**
4025 * @author Vova Feldman (@svovaf)
4026 * @since 1.1.7.3
4027 *
4028 * @param bool $flush_if_no_connectivity
4029 *
4030 * @return bool
4031 */
4032 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
4033 if ( ! isset( $this->_storage->connectivity_test ) ) {
4034 // Connectivity test was never executed, or cache was cleared.
4035 return true;
4036 }
4037
4038 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
4039 if ( WP_FS__IS_HTTP_REQUEST ) {
4040 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
4041 // Domain changed.
4042 return true;
4043 }
4044
4045 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
4046 // Server IP changed.
4047 return true;
4048 }
4049 }
4050 }
4051
4052 if ( $this->_storage->connectivity_test['is_connected'] &&
4053 $this->_storage->connectivity_test['is_active']
4054 ) {
4055 // API connected and Freemius is active - no need to run connectivity check.
4056 return false;
4057 }
4058
4059 if ( $flush_if_no_connectivity ) {
4060 /**
4061 * If explicitly asked to flush when no connectivity - do it only
4062 * if at least 10 sec passed from the last API connectivity test.
4063 */
4064 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
4065 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
4066 }
4067
4068 /**
4069 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
4070 */
4071 $version = $this->get_plugin_version();
4072 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
4073 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
4074 return true;
4075 }
4076
4077 return false;
4078 }
4079
4080 /**
4081 * @author Leo Fajardo (@leorw)
4082 * @since 2.5.4
4083 *
4084 * @param bool $is_update
4085 *
4086 * @return bool
4087 */
4088 private function should_turn_fs_on( $is_update = true ) {
4089 if (
4090 empty( $this->_plugin->opt_in_moderation ) ||
4091 ! is_array( $this->_plugin->opt_in_moderation )
4092 ) {
4093 return true;
4094 }
4095
4096 $optin_config = $this->_plugin->opt_in_moderation;
4097
4098 if (
4099 WP_FS__IS_LOCALHOST &&
4100 ( ! isset( $optin_config['localhost'] ) || false !== $optin_config['localhost'] )
4101 ) {
4102 return true;
4103 }
4104
4105 $optin_config_key = $is_update ?
4106 'updates' :
4107 'new';
4108
4109 if ( ! isset( $optin_config[ $optin_config_key ] ) ) {
4110 return true;
4111 }
4112
4113 $visibility_percentage = $optin_config[ $optin_config_key ];
4114
4115 if ( 0 == $visibility_percentage ) {
4116 return false;
4117 }
4118
4119 if ( ! is_numeric( $visibility_percentage ) ) {
4120 return true;
4121 }
4122
4123 $min = 1;
4124 $max = 100;
4125
4126 if ( function_exists( 'random_int' ) ) {
4127 $random = random_int( $min, $max ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.random_intFound
4128 } else {
4129 $random = rand( $min, $max );
4130 }
4131
4132 return ( $random <= $visibility_percentage );
4133 }
4134
4135 /**
4136 * Check if there's any connectivity issue to Freemius API.
4137 *
4138 * @author Vova Feldman (@svovaf)
4139 * @since 1.0.9
4140 *
4141 * @param bool $flush_if_no_connectivity
4142 *
4143 * @return bool|null
4144 */
4145 function has_api_connectivity( $flush_if_no_connectivity = false ) {
4146 $this->_logger->entrance();
4147
4148 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
4149 return $this->_has_api_connection;
4150 }
4151
4152 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
4153 isset( $this->_storage->connectivity_test ) &&
4154 true === $this->_storage->connectivity_test['is_connected']
4155 ) {
4156 $this->clear_connectivity_info();
4157 }
4158
4159 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
4160 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
4161 /**
4162 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
4163 *
4164 * @since 1.2.1.5 If the user running the premium version then ignore the 'is_active' flag and turn Freemius on to enable license key activation.
4165 */
4166 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
4167 $this->is_premium() ||
4168 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
4169
4170 return $this->_has_api_connection;
4171 }
4172
4173 if (
4174 ! empty( $this->_storage->connectivity_test ) &&
4175 isset( $this->_storage->connectivity_test['is_active'] )
4176 ) {
4177 $is_active = $this->_storage->connectivity_test['is_active'];
4178 } else {
4179 $is_active = $this->should_turn_fs_on( $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() ) );
4180
4181 $this->store_connectivity_info( (object) array( 'is_active' => $is_active ), null );
4182 }
4183
4184 if ( $is_active ) {
4185 $this->_is_on = true;
4186 }
4187
4188 return $this->_has_api_connection;
4189 }
4190
4191 /**
4192 * @author Leo Fajardo (@leorw)
4193 * @since 2.5.4
4194 */
4195 private function clear_connectivity_info() {
4196 unset( $this->_storage->connectivity_test );
4197
4198 FS_Api::clear_force_http_flag();
4199 }
4200
4201 /**
4202 * @author Vova Feldman (@svovaf)
4203 * @since 1.1.7.4
4204 *
4205 * @param object $pong
4206 * @param bool|null $is_connected
4207 */
4208 private function store_connectivity_info( $pong, $is_connected ) {
4209 $this->_logger->entrance();
4210
4211 $version = $this->get_plugin_version();
4212
4213 if ( false === $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
4214 $is_active = false;
4215 } else {
4216 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
4217 }
4218
4219 $is_active = $this->apply_filters(
4220 'is_on',
4221 $is_active,
4222 $this->is_plugin_update(),
4223 $version
4224 );
4225
4226 $this->_storage->connectivity_test = array(
4227 'is_connected' => $is_connected,
4228 'host' => $_SERVER['HTTP_HOST'],
4229 'server_ip' => WP_FS__REMOTE_ADDR,
4230 'is_active' => $is_active,
4231 'timestamp' => WP_FS__SCRIPT_START_TIME,
4232 // Last version with connectivity attempt.
4233 'version' => $version,
4234 );
4235
4236 $this->_has_api_connection = $is_connected;
4237 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
4238 }
4239
4240 /**
4241 * @author Leo Fajardo (@leorw)
4242 * @since 2.5.4
4243 *
4244 * @param bool $is_connected
4245 */
4246 private function update_connectivity_info( $is_connected ) {
4247 $this->store_connectivity_info(
4248 // This is true since we update the connection info only after a successful opt-in or license activation which means that Freemius has already been on even before the process.
4249 (object) array( 'is_active' => true ),
4250 $is_connected
4251 );
4252 }
4253
4254 /**
4255 * Force turning Freemius on.
4256 *
4257 * @author Vova Feldman (@svovaf)
4258 * @since 1.1.8.1
4259 *
4260 * @return bool TRUE if successfully turned on.
4261 */
4262 private function turn_on() {
4263 $this->_logger->entrance();
4264
4265 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
4266 return false;
4267 }
4268
4269 $updated_connectivity = $this->_storage->connectivity_test;
4270 $updated_connectivity['is_active'] = true;
4271 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
4272 $this->_storage->connectivity_test = $updated_connectivity;
4273
4274 $this->_is_on = true;
4275
4276 return true;
4277 }
4278
4279 /**
4280 * Anonymous and unique site identifier (Hash).
4281 *
4282 * @author Vova Feldman (@svovaf)
4283 * @since 1.1.0
4284 *
4285 * @param null|int $blog_id Since 2.0.0
4286 *
4287 * @return string
4288 */
4289 function get_anonymous_id( $blog_id = null ) {
4290 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
4291
4292 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
4293 $key = self::get_unfiltered_site_url( $blog_id, true );
4294
4295 $secure_auth = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
4296 if ( empty( $secure_auth ) ||
4297 false !== strpos( $secure_auth, ' ' ) ||
4298 'put your unique phrase here' === $secure_auth
4299 ) {
4300 // Protect against default auth key.
4301 $secure_auth = md5( microtime() );
4302 }
4303
4304 /**
4305 * Base the unique identifier on the WP secure authentication key. Which
4306 * turns the key into a secret anonymous identifier. This will help us
4307 * to avoid duplicate installs generation on the backend upon opt-in.
4308 *
4309 * @author Vova Feldman (@svovaf)
4310 * @since 1.2.3
4311 */
4312 $unique_id = md5( $key . $secure_auth );
4313
4314 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
4315 }
4316
4317 $this->_logger->departure( $unique_id );
4318
4319 return $unique_id;
4320 }
4321
4322 /**
4323 * Returns anonymous network ID.
4324 *
4325 * @since 2.4.3
4326 *
4327 * @return string
4328 */
4329 function get_anonymous_network_id() {
4330 return $this->get_anonymous_id( get_network()->site_id );
4331 }
4332
4333 /**
4334 * @author Vova Feldman (@svovaf)
4335 * @since 1.1.7.4
4336 *
4337 * @return \WP_User
4338 */
4339 static function _get_current_wp_user() {
4340 self::require_pluggable_essentials();
4341 self::wp_cookie_constants();
4342
4343 return wp_get_current_user();
4344 }
4345
4346 /**
4347 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
4348 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
4349 * is network activated the cookie constants are only configured after the network
4350 * plugins activation, therefore, if we don't define those constants WP will throw
4351 * PHP warnings/notices.
4352 *
4353 * @author Vova Feldman (@svovaf)
4354 * @since 2.1.1
4355 */
4356 private static function wp_cookie_constants() {
4357 if ( defined( 'LOGGED_IN_COOKIE' ) &&
4358 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
4359 ) {
4360 return;
4361 }
4362
4363 /**
4364 * Used to guarantee unique hash cookies
4365 *
4366 * @since 1.5.0
4367 */
4368 if ( ! defined( 'COOKIEHASH' ) ) {
4369 $siteurl = get_site_option( 'siteurl' );
4370 if ( $siteurl ) {
4371 define( 'COOKIEHASH', md5( $siteurl ) );
4372 } else {
4373 define( 'COOKIEHASH', '' );
4374 }
4375 }
4376
4377 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
4378 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
4379 }
4380
4381 /**
4382 * @since 2.5.0
4383 */
4384 if ( ! defined( 'AUTH_COOKIE' ) ) {
4385 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
4386 }
4387
4388 /**
4389 * @since 2.6.0
4390 */
4391 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
4392 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
4393 }
4394 }
4395
4396 /**
4397 * @author Vova Feldman (@svovaf)
4398 * @since 2.1.0
4399 *
4400 * @return int
4401 */
4402 static function get_current_wp_user_id() {
4403 $wp_user = self::_get_current_wp_user();
4404
4405 return $wp_user->ID;
4406 }
4407
4408 /**
4409 * @author Vova Feldman (@svovaf)
4410 * @since 1.2.1.7
4411 *
4412 * @param string $email
4413 *
4414 * @return bool
4415 */
4416 static function is_valid_email( $email ) {
4417 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4418 return false;
4419 }
4420
4421 $parts = explode( '@', $email );
4422
4423 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4424 return false;
4425 }
4426
4427 $blacklist = array(
4428 'admin.',
4429 'webmaster.',
4430 'localhost.',
4431 'dev.',
4432 'development.',
4433 'test.',
4434 'stage.',
4435 'staging.',
4436 );
4437
4438 // Make sure domain is not one of the blacklisted.
4439 foreach ( $blacklist as $invalid ) {
4440 if ( 0 === strpos( $parts[1], $invalid ) ) {
4441 return false;
4442 }
4443 }
4444
4445 // Get the UTF encoded domain name.
4446 /**
4447 * @note - The check of `defined('...')` is there to account for PHP servers compiled with some older version of ICU where the constants are not defined.
4448 * @author - @swashata
4449 */
4450 $is_new_idn_available = (
4451 version_compare( PHP_VERSION, '5.6.40') > 0 &&
4452 defined( 'IDNA_DEFAULT' ) &&
4453 defined( 'INTL_IDNA_VARIANT_UTS46' )
4454 );
4455 if ( $is_new_idn_available ) {
4456 $domain = idn_to_ascii( $parts[1], IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 );
4457 } else {
4458 $domain = idn_to_ascii( $parts[1] ); // phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet
4459 }
4460
4461 $domain = $domain . '.';
4462
4463 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4464 }
4465
4466 #endregion
4467
4468 #----------------------------------------------------------------------------------
4469 #region Email
4470 #----------------------------------------------------------------------------------
4471
4472 /**
4473 * Generates and sends an HTML email with customizable sections.
4474 *
4475 * @author Leo Fajardo (@leorw)
4476 * @since 1.1.2
4477 *
4478 * @param string $to_address
4479 * @param string $subject
4480 * @param array $sections
4481 * @param array $headers
4482 *
4483 * @return bool Whether the email contents were sent successfully.
4484 */
4485 private function send_email(
4486 $to_address,
4487 $subject,
4488 $sections = array(),
4489 $headers = array()
4490 ) {
4491 $default_sections = $this->get_email_sections();
4492
4493 // Insert new sections or replace the default email sections.
4494 if ( is_array( $sections ) && ! empty( $sections ) ) {
4495 foreach ( $sections as $section_id => $custom_section ) {
4496 if ( ! isset( $default_sections[ $section_id ] ) ) {
4497 // If the section does not exist, add it.
4498 $default_sections[ $section_id ] = $custom_section;
4499 } else {
4500 // If the section already exists, override it.
4501 $current_section = $default_sections[ $section_id ];
4502
4503 // Replace the current section's title if a custom section title exists.
4504 if ( isset( $custom_section['title'] ) ) {
4505 $current_section['title'] = $custom_section['title'];
4506 }
4507
4508 // Insert new rows under the current section or replace the default rows.
4509 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4510 foreach ( $custom_section['rows'] as $row_id => $row ) {
4511 $current_section['rows'][ $row_id ] = $row;
4512 }
4513 }
4514
4515 $default_sections[ $section_id ] = $current_section;
4516 }
4517 }
4518 }
4519
4520 $vars = array( 'sections' => $default_sections );
4521 $message = fs_get_template( 'email.php', $vars );
4522
4523 // Set the type of email to HTML.
4524 $headers[] = 'Content-type: text/html; charset=UTF-8';
4525
4526 $header_string = implode( "\r\n", $headers );
4527
4528 return wp_mail(
4529 $to_address,
4530 $subject,
4531 $message,
4532 $header_string
4533 );
4534 }
4535
4536 /**
4537 * Generates the data for the sections of the email content.
4538 *
4539 * @author Leo Fajardo (@leorw)
4540 * @since 1.1.2
4541 *
4542 * @return array
4543 */
4544 private function get_email_sections() {
4545 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4546 $current_user = self::_get_current_wp_user();
4547
4548 // Retrieve the cURL version information so that we can get the version number below.
4549 $curl_version_information = curl_version();
4550
4551 $active_plugin = self::get_active_plugins();
4552
4553 // Generate the list of active plugins separated by new line.
4554 $active_plugin_string = '';
4555 foreach ( $active_plugin as $plugin ) {
4556 $active_plugin_string .= sprintf(
4557 '<a href="%s">%s</a> [v%s]<br>',
4558 $plugin['PluginURI'],
4559 $plugin['Name'],
4560 $plugin['Version']
4561 );
4562 }
4563
4564 $server_ip = WP_FS__REMOTE_ADDR;
4565
4566 // Add PHP info for deeper investigation.
4567 ob_start();
4568 phpinfo();
4569 $php_info = ob_get_clean();
4570
4571 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4572
4573 // Generate the default email sections.
4574 $sections = array(
4575 'sdk' => array(
4576 'title' => 'SDK',
4577 'rows' => array(
4578 'fs_version' => array( 'FS Version', $this->version ),
4579 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4580 )
4581 ),
4582 'plugin' => array(
4583 'title' => ucfirst( $this->get_module_type() ),
4584 'rows' => array(
4585 'name' => array( 'Name', $this->get_plugin_name() ),
4586 'version' => array( 'Version', $this->get_plugin_version() )
4587 )
4588 ),
4589 'api' => array(
4590 'title' => 'API Subdomain',
4591 'rows' => array(
4592 'dns' => array(
4593 'DNS_CNAME',
4594 function_exists( 'dns_get_record' ) ?
4595 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4596 'dns_get_record() disabled/blocked'
4597 ),
4598 'ip' => array(
4599 'IP',
4600 function_exists( 'gethostbyname' ) ?
4601 gethostbyname( $api_domain ) :
4602 'gethostbyname() disabled/blocked'
4603 ),
4604 ),
4605 ),
4606 'site' => array(
4607 'title' => 'Site',
4608 'rows' => array(
4609 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4610 'address' => array( 'Address', site_url() ),
4611 'host' => array(
4612 'HTTP_HOST',
4613 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4614 ),
4615 'hosting' => array(
4616 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4617 fs_request_get( 'hosting_company' ) :
4618 'Unknown',
4619 ),
4620 'server_addr' => array(
4621 'SERVER_ADDR',
4622 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4623 )
4624 )
4625 ),
4626 'user' => array(
4627 'title' => 'User',
4628 'rows' => array(
4629 'email' => array( 'Email', $current_user->user_email ),
4630 'first' => array( 'First', $current_user->user_firstname ),
4631 'last' => array( 'Last', $current_user->user_lastname )
4632 )
4633 ),
4634 'plugins' => array(
4635 'title' => 'Plugins',
4636 'rows' => array(
4637 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4638 )
4639 ),
4640 'php_info' => array(
4641 'title' => 'PHP Info',
4642 'rows' => array(
4643 'info' => array( $php_info )
4644 ),
4645 )
4646 );
4647
4648 // Allow the sections to be modified by other code.
4649 $sections = $this->apply_filters( 'email_template_sections', $sections );
4650
4651 return $sections;
4652 }
4653
4654 #endregion
4655
4656 #----------------------------------------------------------------------------------
4657 #region Initialization
4658 #----------------------------------------------------------------------------------
4659
4660 /**
4661 * Init plugin's Freemius instance.
4662 *
4663 * @author Vova Feldman (@svovaf)
4664 * @since 1.0.1
4665 *
4666 * @param number $id
4667 * @param string $public_key
4668 * @param bool $is_live
4669 * @param bool $is_premium
4670 */
4671 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4672 $this->_logger->entrance();
4673
4674 $this->dynamic_init( array(
4675 'id' => $id,
4676 'public_key' => $public_key,
4677 'is_live' => $is_live,
4678 'is_premium' => $is_premium,
4679 ) );
4680 }
4681
4682 /**
4683 * Dynamic initiator, originally created to support initiation
4684 * with parent_id for add-ons.
4685 *
4686 * @author Vova Feldman (@svovaf)
4687 * @since 1.0.6
4688 *
4689 * @param array $plugin_info
4690 *
4691 * @throws Freemius_Exception
4692 */
4693 function dynamic_init( array $plugin_info ) {
4694 $this->_logger->entrance();
4695
4696 $this->parse_settings( $plugin_info );
4697
4698 $this->register_after_settings_parse_hooks();
4699
4700 /**
4701 * If anonymous but there's already a user entity and the user's site is associated with a valid license or trial period, update the anonymous mode accordingly.
4702 *
4703 * @todo Remove this entire `if` block after several releases as starting from this version, the anonymous mode will already be updated accordingly after a purchase.
4704 */
4705 if ( $this->is_anonymous() ) {
4706 $is_network_level = ( $this->_is_network_active && fs_is_network_admin() );
4707
4708 if (
4709 ! $is_network_level ||
4710 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
4711 ) {
4712 if ( $this->is_paying_or_trial() ) {
4713 $this->reset_anonymous_mode( $is_network_level );
4714 }
4715 } else {
4716 $network = get_network();
4717
4718 if ( is_object( $network ) ) {
4719 $main_blog_id = $network->site_id;
4720 $first_install = $this->get_install_by_blog_id( $main_blog_id );
4721
4722 if ( is_object( $first_install ) ) {
4723 $this->_storage->network_install_blog_id = $main_blog_id;
4724 $this->_storage->network_user_id = $first_install->user_id;
4725 }
4726 }
4727 }
4728 }
4729
4730 if ( $this->should_stop_execution() ) {
4731 return;
4732 }
4733
4734 if ( ! $this->is_registered() ) {
4735 if ( $this->is_anonymous() ) {
4736 // If user skipped, no need to test connectivity.
4737 $this->_has_api_connection = true;
4738 $this->_is_on = true;
4739 } else {
4740 if ( false === $this->has_api_connectivity() ) {
4741 return;
4742 } else {
4743 if ( $this->_anonymous_mode ) {
4744 // Simulate anonymous mode.
4745 $this->_is_anonymous = true;
4746 }
4747 }
4748 }
4749 }
4750
4751 /**
4752 * This should be executed even if Freemius is off for the core module,
4753 * otherwise, the add-ons dialog box won't work properly. This is especially
4754 * relevant when the developer decided to turn FS off for existing users.
4755 *
4756 * @author Vova Feldman (@svovaf)
4757 */
4758 if ( $this->is_user_in_admin() &&
4759 'plugin-information' === fs_request_get( 'tab', false ) &&
4760 $this->should_use_freemius_updater_and_dialog() &&
4761 (
4762 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4763 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4764 )
4765 ) {
4766 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4767
4768 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4769 }
4770
4771 // Check if Freemius is on for the current plugin.
4772 // This MUST be executed after all the plugin variables has been loaded.
4773 if ( ! $this->is_registered() && ! $this->is_on() ) {
4774 return;
4775 }
4776
4777 if ( $this->has_api_connectivity() ) {
4778 if ( self::is_cron() ) {
4779 $this->hook_callback_to_sync_cron();
4780 } else if ( $this->is_user_in_admin() ) {
4781 /**
4782 * Schedule daily data sync cron if:
4783 *
4784 * 1. User opted-in (for tracking).
4785 * 2. If skipped, but later upgraded (opted-in via upgrade).
4786 *
4787 * @author Vova Feldman (@svovaf)
4788 * @since 1.1.7.3
4789 *
4790 */
4791 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
4792 $this->maybe_schedule_sync_cron();
4793 }
4794
4795 /**
4796 * Check if requested for manual blocking background sync.
4797 */
4798 if ( fs_request_has( 'background_sync' ) ) {
4799 self::require_pluggable_essentials();
4800 self::wp_cookie_constants();
4801
4802 $this->run_manual_sync();
4803 }
4804 }
4805 }
4806
4807 if ( $this->is_registered() ) {
4808 FS_Clone_Manager::instance()->maybe_resolve_new_subsite_install_automatically( $this );
4809
4810 $this->hook_callback_to_install_sync();
4811 }
4812
4813 if ( $this->is_addon() ) {
4814 if ( $this->is_parent_plugin_installed() ) {
4815 // Link to parent FS.
4816 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4817
4818 // Get parent plugin reference.
4819 $this->_parent_plugin = $this->_parent->get_plugin();
4820 }
4821 }
4822
4823 if ( $this->is_user_in_admin() ) {
4824 if ( $this->is_registered() && fs_request_has( 'purchase_completed' ) ) {
4825 $this->_admin_notices->add_sticky(
4826 sprintf(
4827 /* translators: %s: License type (e.g. you have a professional license) */
4828 $this->get_text_inline( 'You have purchased a %s license.', 'you-have-x-license' ),
4829 fs_request_get( 'purchased_plan' )
4830 ) .
4831 sprintf(
4832 $this->get_text_inline(" The %s's %sdownload link%s, license key, and installation instructions have been sent to %s. If you can't find the email after 5 min, please check your spam box.", 'post-purchase-email-sent-message' ),
4833 $this->get_module_label( true ),
4834 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? "products' " : '' ),
4835 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? 's' : '' ),
4836 sprintf(
4837 '<strong>%s</strong>',
4838 fs_request_get( 'purchase_email' )
4839 )
4840 ),
4841 'plan_purchased',
4842 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
4843 );
4844 }
4845
4846 if ( $this->is_addon() ) {
4847 if ( ! $this->is_parent_plugin_installed() ) {
4848 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4849
4850 if ( isset( $plugin_info['parent'] ) ) {
4851 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4852 }
4853
4854 $this->_admin_notices->add(
4855 ( ! empty( $parent_name ) ?
4856 sprintf( $this->get_text_x_inline( '%s cannot run without %s.', 'addonX cannot run without pluginY', 'addon-x-cannot-run-without-y' ), $this->get_plugin_name(), $parent_name ) :
4857 sprintf( $this->get_text_x_inline( '%s cannot run without the plugin.', 'addonX cannot run...', 'addon-x-cannot-run-without-parent' ), $this->get_plugin_name() )
4858 ),
4859 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4860 'error'
4861 );
4862
4863 return;
4864 } else {
4865 $is_network_admin = fs_is_network_admin();
4866
4867 if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4868 // If add-on activated and parent not, automatically install parent for the user.
4869 $this->activate_parent_account( $this->_parent );
4870 } else if (
4871 $this->_parent->is_registered() &&
4872 ! $this->is_registered() &&
4873 /**
4874 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
4875 * * Network active and in network admin - network activate add-on account.
4876 * * Network active and not in network admin - activate add-on account for the current blog.
4877 * * Not network active and not in network admin - activate add-on account for the current blog.
4878 *
4879 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
4880 *
4881 * @author Leo Fajardo (@leorw)
4882 * @since 2.3.0
4883 */
4884 ( $this->is_network_active() || ! $is_network_admin )
4885 ) {
4886 $premium_license = null;
4887
4888 if (
4889 ! $this->has_free_plan() &&
4890 $this->is_bundle_license_auto_activation_enabled() &&
4891 $this->_parent->is_activated_with_bundle_license()
4892 ) {
4893 /**
4894 * If the add-on has no free plan, try to activate the account only when there's a bundle license.
4895 *
4896 * @author Leo Fajardo (@leorw)
4897 * @since 2.4.0
4898 */
4899 $bundle_license = $this->get_active_parent_license( $this->_parent->_get_license()->secret_key, false );
4900
4901 if (
4902 is_object( $bundle_license ) &&
4903 ! empty( $bundle_license->products ) &&
4904 in_array( $this->get_id(), $bundle_license->products )
4905 ) {
4906 $premium_license = $bundle_license;
4907 }
4908 }
4909
4910 if ( $this->has_free_plan() || is_object( $premium_license) ) {
4911 // If parent plugin activated, automatically install add-on for the user.
4912 $this->_activate_addon_account(
4913 $this->_parent,
4914 ( $this->is_network_active() && $is_network_admin ) ?
4915 true :
4916 get_current_blog_id(),
4917 $premium_license
4918 );
4919 }
4920 }
4921
4922 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4923 if ( $this->is_premium() ) {
4924 // Remove add-on download admin-notice.
4925 $this->_parent->_admin_notices->remove_sticky( array(
4926 'addon_plan_upgraded_' . $this->_slug,
4927 'no_addon_license_' . $this->_slug,
4928 ) );
4929 }
4930
4931 // $this->deactivate_premium_only_addon_without_license();
4932 }
4933 }
4934
4935 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
4936
4937 // if ( $this->is_registered() ||
4938 // $this->is_anonymous() ||
4939 // $this->is_pending_activation()
4940 // ) {
4941 // $this->_init_admin();
4942 // }
4943 }
4944
4945 /**
4946 * Should be called outside `$this->is_user_in_admin()` scope
4947 * because the updater has some logic that needs to be executed
4948 * during AJAX calls.
4949 *
4950 * Currently, we need to hook to the `http_request_host_is_external` filter.
4951 * In the future, there might be additional logic added.
4952 *
4953 * @author Vova Feldman
4954 * @since 1.2.1.6
4955 */
4956 if (
4957 $this->should_use_freemius_updater_and_dialog() &&
4958 (
4959 $this->is_premium() ||
4960 /**
4961 * If not premium but the premium version is installed, also instantiate the updater so that the
4962 * plugin information dialog of the premium version will have the information from the server.
4963 *
4964 * @author Leo Fajardo (@leorw)
4965 * @since 2.2.3
4966 */
4967 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
4968 ) &&
4969 $this->has_release_on_freemius() &&
4970 ( ! $this->is_unresolved_clone( true ) )
4971 ) {
4972 FS_Plugin_Updater::instance( $this );
4973 }
4974
4975 $this->do_action( 'initiated' );
4976
4977 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
4978 if ( isset( $this->_storage->prev_is_premium ) ) {
4979 $this->apply_filters(
4980 'after_code_type_change',
4981 // New code type.
4982 $this->_plugin->is_premium
4983 );
4984 } else {
4985 // Set for code type for the first time.
4986 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
4987 }
4988 }
4989
4990 if ( ! $this->is_addon() ) {
4991 if ( $this->is_registered() ) {
4992 // Fix for upgrade from versions < 1.0.9.
4993 if ( ! isset( $this->_storage->activation_timestamp ) ) {
4994 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
4995 }
4996
4997 $this->do_action( 'after_init_plugin_registered' );
4998 } else if ( $this->is_anonymous() ) {
4999 $this->do_action( 'after_init_plugin_anonymous' );
5000 } else if ( $this->is_pending_activation() ) {
5001 $this->do_action( 'after_init_plugin_pending_activations' );
5002 }
5003 } else {
5004 if ( $this->is_registered() ) {
5005 $this->do_action( 'after_init_addon_registered' );
5006 } else if ( $this->is_anonymous() ) {
5007 $this->do_action( 'after_init_addon_anonymous' );
5008 } else if ( $this->is_pending_activation() ) {
5009 $this->do_action( 'after_init_addon_pending_activations' );
5010 }
5011 }
5012 }
5013
5014 /**
5015 * @author Leo Fajardo (@leorw)
5016 * @since 2.2.3
5017 *
5018 * @return bool
5019 */
5020 private function should_use_freemius_updater_and_dialog() {
5021 return (
5022 /**
5023 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
5024 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
5025 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
5026 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
5027 * plugin details from .org).
5028 */
5029 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
5030 (
5031 ! self::is_plugin_install_page() &&
5032 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
5033 ( 'install-plugin' !== fs_request_get( 'action' ) )
5034 )
5035 );
5036 }
5037
5038 /**
5039 * @param string[] $permissions
5040 * @param bool $is_enabled
5041 * @param int|null $blog_id
5042 *
5043 * @return true|object `true` on success, API error object on failure.
5044 */
5045 private function update_site_permissions( array $permissions, $is_enabled, $blog_id = null ) {
5046 $this->_logger->entrance();
5047
5048 $params = array(
5049 'permissions' => implode( ',', $permissions ),
5050 'is_enabled' => $is_enabled,
5051 );
5052
5053 $current_blog_id = get_current_blog_id();
5054 $is_blog_switched = false;
5055 if ( is_numeric( $blog_id ) && $current_blog_id != $blog_id ) {
5056 $is_blog_switched = $this->switch_to_blog( $blog_id );
5057 }
5058
5059 $result = $this->api_site_call( '/permissions.json', 'put', $params );
5060
5061 if ( $is_blog_switched ) {
5062 $this->switch_to_blog( $current_blog_id );
5063 }
5064
5065 if (
5066 ! $this->is_api_result_object( $result ) ||
5067 ! isset( $result->install_id )
5068 ) {
5069 $this->_logger->api_error( $result );
5070
5071 return $result;
5072 }
5073
5074 return true;
5075 }
5076
5077 /**
5078 * @param string[] $permissions
5079 * @param bool $is_enabled
5080 * @param bool $has_site_delegated_connection
5081 *
5082 * @return true|object `true` on success, API error object on failure.
5083 */
5084 private function update_network_permissions(
5085 array $permissions,
5086 $is_enabled,
5087 &$has_site_delegated_connection
5088 ) {
5089 $this->_logger->entrance();
5090
5091 $install_id_2_blog_id = array();
5092 $install_by_blog_id = $this->get_blog_install_map();
5093
5094 $has_site_delegated_connection = false;
5095
5096 foreach ( $install_by_blog_id as $blog_id => $install ) {
5097 if ( $this->is_site_delegated_connection( $blog_id ) ) {
5098 // Only update permissions of non-delegated installs.
5099 $has_site_delegated_connection = true;
5100 continue;
5101 }
5102
5103 $install_id_2_blog_id[ $install->id ] = $blog_id;
5104 }
5105
5106 if ( empty( $install_id_2_blog_id ) ) {
5107 return true;
5108 }
5109
5110 $params = array(
5111 'permissions' => implode( ',', $permissions ),
5112 'is_enabled' => $is_enabled,
5113 'install_ids' => implode( ',', array_keys( $install_id_2_blog_id ) ),
5114 );
5115
5116 // Send update to FS.
5117 $result = $this->get_current_or_network_user_api_scope()->call(
5118 "/plugins/{$this->_module_id}/installs/permissions.json",
5119 'put',
5120 $params
5121 );
5122
5123 if ( ! $this->is_api_result_object( $result, 'installs_metadata' ) ) {
5124 $this->_logger->api_error( $result );
5125
5126 return $result;
5127 }
5128
5129 return true;
5130 }
5131
5132 /**
5133 * @param mixed $result
5134 *
5135 * @return string
5136 */
5137 private function get_api_error_message( $result ) {
5138 $error_message = sprintf( $this->get_text_inline( 'There was an unexpected API error while processing your request. Please try again in a few minutes and if it still doesn\'t work, contact the %s\'s author with the following:',
5139 'unexpected-api-error' ), $this->_module_type ) . ' ';
5140
5141 if (
5142 $this->is_api_error( $result ) &&
5143 isset( $result->error )
5144 ) {
5145 $code = empty( $result->error->code ) ? '' : " Code: {$result->error->code}";
5146
5147 $error_message .= "<b>{$result->error->message}{$code}</b>";
5148 } else {
5149 $error_message .= var_export( $result, true );
5150 }
5151
5152 return $error_message;
5153 }
5154
5155 /**
5156 * @author Vova Feldman (@svovaf)
5157 * @since 2.5.1
5158 */
5159 function _toggle_permission_tracking_callback() {
5160 $this->_logger->entrance();
5161
5162 $this->check_ajax_referer( 'toggle_permission_tracking' );
5163
5164 if ( ! $this->is_registered( true ) ) {
5165 self::shoot_ajax_failure( 'User never opted-in.' );
5166 }
5167
5168 $is_enabled = fs_request_get_bool( 'is_enabled' );
5169 $permissions = fs_request_get( 'permissions' );
5170
5171 if ( ! is_string( $permissions ) ) {
5172 self::shoot_ajax_failure( 'The permissions param must be a string.' );
5173 }
5174
5175 $permissions = explode( ',', $permissions );
5176
5177 $result = $this->toggle_permission_tracking( $permissions, $is_enabled );
5178
5179 if ( true !== $result ) {
5180 self::shoot_ajax_failure( $this->get_api_error_message( $result ) );
5181 }
5182
5183 self::shoot_ajax_success();
5184 }
5185
5186 /**
5187 * @param string[] $permissions
5188 * @param bool $is_enabled
5189 * @param int|null $blog_id
5190 *
5191 * @return bool|mixed `true` if updated successfully or no update is needed.
5192 */
5193 private function toggle_permission_tracking( $permissions, $is_enabled, $blog_id = null ) {
5194 if ( ! $this->is_registered( true ) ) {
5195 // User never opted-in.
5196 return false;
5197 }
5198
5199 // Check if permissions are already set as needed.
5200 if ( FS_Permission_Manager::instance( $this )->are_permissions( $permissions, $is_enabled, $blog_id ) ) {
5201 /**
5202 * Note:
5203 * When running on the network admin, there's no need to iterate through all the installs individually since network opt-in permissions are managed for ALL non-delegated installs through a single option (per permission) on the network-level storage.
5204 */
5205 return true;
5206 }
5207
5208 $api_managed_permissions = array_intersect(
5209 $permissions,
5210 FS_Permission_Manager::get_api_managed_permission_ids()
5211 );
5212
5213 if (
5214 in_array( FS_Permission_Manager::PERMISSION_ESSENTIALS, $permissions ) &&
5215 ! in_array( FS_Permission_Manager::PERMISSION_SITE, $permissions )
5216 ) {
5217 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_SITE;
5218 }
5219
5220 if ( ! empty( $api_managed_permissions ) ) {
5221 $has_site_delegated_connection = false;
5222
5223 if (
5224 ! $is_enabled &&
5225 ! in_array( FS_Permission_Manager::PERMISSION_EXTENSIONS, $api_managed_permissions ) &&
5226 false === FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed( $blog_id )
5227 ) {
5228 /**
5229 * If we are turning off a permission and the extensions permission is off too, enrich the permissions update request to also turn off extensions tracking, as currently when opting in with extensions tracking disabled the extensions tracking is off but the API isn't aware of it.
5230 *
5231 * @todo Remove this entire `if` after implementing granular opt-in that also sends the permissions to the API when opting in.
5232 */
5233 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_EXTENSIONS;
5234 }
5235
5236 if ( is_null( $blog_id ) && fs_is_network_admin() ) {
5237 $result = $this->update_network_permissions(
5238 $api_managed_permissions,
5239 $is_enabled,
5240 $has_site_delegated_connection
5241 );
5242 } else {
5243 $result = $this->update_site_permissions(
5244 $api_managed_permissions,
5245 $is_enabled,
5246 $blog_id
5247 );
5248 }
5249
5250 if ( true !== $result ) {
5251 return $result;
5252 }
5253
5254 if ( in_array( FS_Permission_Manager::PERMISSION_SITE, $api_managed_permissions ) ) {
5255 if ( $is_enabled ) {
5256 $this->schedule_sync_cron();
5257 } else {
5258 $this->clear_sync_cron( ! $has_site_delegated_connection );
5259 }
5260 }
5261
5262 if ( in_array( FS_Permission_Manager::PERMISSION_USER, $api_managed_permissions ) ) {
5263 $this->toggle_user_permission( $is_enabled, $blog_id );
5264 }
5265 }
5266
5267 $this->update_tracking_permissions(
5268 $permissions,
5269 $is_enabled,
5270 $blog_id
5271 );
5272
5273 return true;
5274 }
5275
5276 /**
5277 * @param bool $is_enabled
5278 * @param int|null $blog_id
5279 */
5280 private function toggle_user_permission( $is_enabled, $blog_id = null ) {
5281 $network_or_blog_ids = is_numeric( $blog_id ) ?
5282 $blog_id :
5283 fs_is_network_admin();
5284
5285 if ( $is_enabled ) {
5286 $this->reset_anonymous_mode( $network_or_blog_ids );
5287 } else {
5288 $this->skip_connection( $network_or_blog_ids );
5289 }
5290 }
5291
5292 /**
5293 * Opt-in back into usage tracking.
5294 *
5295 * Note: This will only work if the user opted-in previously.
5296 *
5297 * Returns:
5298 * 1. FALSE - If the user never opted-in.
5299 * 2. TRUE - If successfully opted-in back to usage tracking.
5300 * 3. object - API result on failure.
5301 *
5302 * @author Leo Fajardo (@leorw)
5303 * @since 1.2.1.5
5304 *
5305 * @bool $is_enabled
5306 *
5307 * @return bool|object
5308 */
5309 private function toggle_site_tracking( $is_enabled, $blog_id = null ) {
5310 $this->_logger->entrance();
5311
5312 return $this->toggle_permission_tracking(
5313 FS_Permission_Manager::instance( $this )->get_site_tracking_permission_names(),
5314 $is_enabled,
5315 $blog_id
5316 );
5317 }
5318
5319 /**
5320 * If user opted-in and later disabled usage-tracking,
5321 * re-allow tracking for licensing and updates.
5322 *
5323 * @author Leo Fajardo (@leorw)
5324 * @since 1.2.1.5
5325 *
5326 * @param bool $is_context_single_site
5327 */
5328 private function reconnect_locally( $is_context_single_site = false ) {
5329 $this->_logger->entrance();
5330
5331 if ( ! $this->is_registered() ) {
5332 return;
5333 }
5334
5335 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5336 if ( $this->is_tracking_prohibited() ) {
5337 FS_Permission_Manager::instance( $this )->update_site_tracking( true );
5338 }
5339 } else {
5340 $installs_map = $this->get_blog_install_map();
5341 foreach ( $installs_map as $blog_id => $install ) {
5342 /**
5343 * @var FS_Site $install
5344 */
5345 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
5346 FS_Permission_Manager::instance( $this )->update_site_tracking( true, $blog_id );
5347 }
5348 }
5349 }
5350 }
5351
5352 /**
5353 * Update permission tracking flags. When updating in a network context, in addition to updating the network-level flags, also update the permissions on the site-level for all non-delegated sites.
5354 *
5355 * @param string[] $permissions
5356 * @param bool $is_enabled
5357 * @param int|null $blog_id
5358 *
5359 * @return array
5360 */
5361 private function update_tracking_permissions( $permissions, $is_enabled, $blog_id = null ) {
5362 // Alias.
5363 $permission_manager = FS_Permission_Manager::instance( $this );
5364
5365 $network_or_blog_ids = is_numeric( $blog_id ) ?
5366 $blog_id :
5367 fs_is_network_admin();
5368
5369 if ( true === $network_or_blog_ids ) {
5370 // Update the permission for all non-delegated sub-sites.
5371 $blog_ids = $this->get_non_delegated_blog_ids();
5372
5373 // Add the network-level to the array, to update the permission on the network-level storage.
5374 array_unshift( $blog_ids, null );
5375 }
5376 else
5377 {
5378 if ( false === $network_or_blog_ids ) {
5379 $network_or_blog_ids = null;
5380 }
5381
5382 $blog_ids = is_array( $network_or_blog_ids ) ?
5383 $network_or_blog_ids :
5384 array( $network_or_blog_ids );
5385 }
5386
5387 $result = array();
5388 foreach ( $permissions as $permission ) {
5389 $permission = trim( $permission );
5390 $is_permission_supported = true;
5391
5392 foreach ( $blog_ids as $id ) {
5393 $is_permission_supported = $permission_manager->update_permission_tracking_flag(
5394 $permission,
5395 $is_enabled,
5396 $id
5397 );
5398 }
5399
5400 if ( ! $is_permission_supported ) {
5401 $permission = 'no_match';
5402 }
5403
5404 $result[ $permission ] = $is_enabled;
5405 }
5406
5407 return $result;
5408 }
5409
5410 /**
5411 * Parse plugin's settings (as defined by the plugin dev).
5412 *
5413 * @author Vova Feldman (@svovaf)
5414 * @since 1.1.7.3
5415 *
5416 * @param array $plugin_info
5417 *
5418 * @throws \Freemius_Exception
5419 */
5420 private function parse_settings( &$plugin_info ) {
5421 $this->_logger->entrance();
5422
5423 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5424 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5425 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5426 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5427 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5428
5429 /**
5430 * @author Vova Feldman (@svovaf)
5431 * @since 1.1.9 Try to pull secret key from external config.
5432 */
5433 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5434 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5435 }
5436
5437 if ( isset( $plugin_info['parent'] ) ) {
5438 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5439 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5440 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5441 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5442 }
5443
5444 if ( false === $id ) {
5445 throw new Freemius_Exception( array(
5446 'error' => array(
5447 'type' => 'ParameterNotSet',
5448 'message' => 'Plugin id parameter is not set.',
5449 'code' => 'plugin_id_not_set',
5450 'http' => 500,
5451 )
5452 ) );
5453 }
5454 if ( false === $public_key ) {
5455 throw new Freemius_Exception( array(
5456 'error' => array(
5457 'type' => 'ParameterNotSet',
5458 'message' => 'Plugin public_key parameter is not set.',
5459 'code' => 'plugin_public_key_not_set',
5460 'http' => 500,
5461 )
5462 ) );
5463 }
5464
5465 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5466 $this->_plugin :
5467 new FS_Plugin();
5468
5469 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5470
5471 $plugin->update( array(
5472 'id' => $id,
5473 'type' => $this->get_option( $plugin_info, 'type', $this->_module_type ),
5474 'public_key' => $public_key,
5475 'slug' => $this->_slug,
5476 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5477 'parent_plugin_id' => $parent_id,
5478 'version' => $this->get_plugin_version(),
5479 'title' => $this->get_plugin_name( $premium_suffix ),
5480 'file' => $this->_plugin_basename,
5481 'is_premium' => $this->get_bool_option( $plugin_info, 'is_premium', true ),
5482 'premium_suffix' => $premium_suffix,
5483 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5484 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5485 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5486 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ),
5487 'opt_in_moderation' => $this->get_option( $plugin_info, 'opt_in', null ),
5488 ) );
5489
5490 if ( $plugin->is_updated() ) {
5491 // Update plugin details.
5492 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5493 }
5494 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5495 $this->_plugin->secret_key = $secret_key;
5496
5497 /**
5498 * If the product is network integrated and activated and the current view is in the network level Admin dashboard, if the product's network-level menu located differently from the sub-site level, then use the network menu details (when set).
5499 *
5500 * @author Vova Feldman
5501 * @since 2.4.5
5502 */
5503 if ( $this->is_network_active() && fs_is_network_admin() ) {
5504 if ( isset( $plugin_info['menu_network'] ) &&
5505 is_array( $plugin_info['menu_network'] ) &&
5506 ! empty( $plugin_info['menu_network'] )
5507 ) {
5508 $plugin_info['menu'] = $plugin_info['menu_network'];
5509 }
5510 }
5511
5512 if ( ! isset( $plugin_info['menu'] ) ) {
5513 $plugin_info['menu'] = array();
5514
5515 if ( ! empty( $this->_storage->sdk_last_version ) &&
5516 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5517 ) {
5518 // Backward compatibility to 1.1.2
5519 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5520 $plugin_info['menu_slug'] :
5521 $this->_slug;
5522 }
5523 }
5524
5525 $this->_menu = FS_Admin_Menu_Manager::instance(
5526 $this->_module_id,
5527 $this->_module_type,
5528 $this->get_unique_affix()
5529 );
5530
5531 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5532
5533 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5534 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5535 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5536 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5537 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5538 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5539 if ( $this->_is_premium_only ) {
5540 // If premium only plugin, disable anonymous mode.
5541 $this->_enable_anonymous = false;
5542 $this->_anonymous_mode = false;
5543 } else {
5544 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5545 $this->_anonymous_mode = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
5546 }
5547 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5548 $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
5549
5550 if ( ! empty( $plugin_info['trial'] ) ) {
5551 $this->_trial_days = $this->get_numeric_option(
5552 $plugin_info['trial'],
5553 'days',
5554 // Default to 0 - trial without days specification.
5555 0
5556 );
5557
5558 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5559 }
5560
5561 $this->_navigation = $this->get_option(
5562 $plugin_info,
5563 'navigation',
5564 $this->is_free_wp_org_theme() ?
5565 self::NAVIGATION_TABS :
5566 self::NAVIGATION_MENU
5567 );
5568 }
5569
5570 /**
5571 * @param string[] $options
5572 * @param string $key
5573 * @param mixed $default
5574 *
5575 * @return bool
5576 */
5577 private function get_option( &$options, $key, $default = false ) {
5578 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5579 }
5580
5581 private function get_bool_option( &$options, $key, $default = false ) {
5582 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5583 }
5584
5585 private function get_numeric_option( &$options, $key, $default = false ) {
5586 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5587 }
5588
5589 /**
5590 * Gate keeper.
5591 *
5592 * @author Vova Feldman (@svovaf)
5593 * @since 1.1.7.3
5594 *
5595 * @return bool
5596 */
5597 private function should_stop_execution() {
5598 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5599 /**
5600 * Don't execute Freemius until plugin was fully loaded at least once,
5601 * to give the opportunity for the activation hook to run before pinging
5602 * the API for connectivity test. This logic is relevant for the
5603 * identification of new plugin install vs. plugin update.
5604 *
5605 * @author Vova Feldman (@svovaf)
5606 * @since 1.1.9
5607 */
5608 return true;
5609 }
5610
5611 if ( $this->is_activation_mode() ) {
5612 if ( ! is_admin() ) {
5613 /**
5614 * If in activation mode, don't execute Freemius outside the admin dashboard.
5615 *
5616 * @author Vova Feldman (@svovaf)
5617 * @since 1.1.7.3
5618 */
5619 return true;
5620 }
5621
5622 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5623 /**
5624 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5625 * then don't start Freemius.
5626 *
5627 * @author Vova Feldman (@svovaf)
5628 * @since 1.1.6.3
5629 *
5630 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5631 */
5632 return true;
5633 }
5634
5635 if ( self::is_cron() ) {
5636 /**
5637 * If in activation mode, don't execute Freemius during wp crons
5638 * (wp crons have HTTP context - called as HTTP request).
5639 *
5640 * @author Vova Feldman (@svovaf)
5641 * @since 1.1.7.3
5642 */
5643 return true;
5644 }
5645
5646 if ( self::is_ajax() ) {
5647 /**
5648 * During activation, if running in AJAX mode, unless there's a sticky
5649 * connectivity issue notice, don't run Freemius.
5650 *
5651 * @author Vova Feldman (@svovaf)
5652 * @since 1.1.7.3
5653 */
5654 return true;
5655 }
5656 }
5657
5658 return false;
5659 }
5660
5661 /**
5662 * Triggered after code type has changed.
5663 *
5664 * @author Vova Feldman (@svovaf)
5665 * @since 1.1.9.1
5666 */
5667 function _after_code_type_change() {
5668 $this->_logger->entrance();
5669
5670 if ( $this->is_theme() ) {
5671 // Expire the cache of the previous tabs since the theme may
5672 // have setting updates after code type has changed.
5673 $this->_cache->expire( 'tabs' );
5674 $this->_cache->expire( 'tabs_stylesheets' );
5675 }
5676
5677 if ( ! $this->is_addon() ) {
5678 add_action(
5679 is_admin() ? 'admin_init' : 'init',
5680 array( &$this, '_plugin_code_type_changed' )
5681 );
5682 }
5683
5684 if ( $this->is_registered() && $this->is_premium() ) {
5685 // Purge cached payments after switching to the premium version.
5686 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5687 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5688 }
5689 }
5690
5691 /**
5692 * Handles plugin's code type change (free <--> premium).
5693 *
5694 * @author Vova Feldman (@svovaf)
5695 * @since 1.0.9
5696 */
5697 function _plugin_code_type_changed() {
5698 $this->_logger->entrance();
5699
5700 if ( $this->is_premium() ) {
5701 $this->reconnect_locally();
5702
5703 // Activated premium code.
5704 $this->do_action( 'after_premium_version_activation' );
5705
5706 // Remove all sticky messages related to download of the premium version.
5707 $this->_admin_notices->remove_sticky( array(
5708 'trial_started',
5709 'plan_upgraded',
5710 'plan_changed',
5711 'license_activated',
5712 ) );
5713
5714 $notice = '';
5715 if ( ! $this->is_only_premium() ) {
5716 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5717 }
5718
5719 $license_notice = $this->get_license_network_activation_notice();
5720 if ( ! empty( $license_notice ) ) {
5721 $notice .= ' ' . $license_notice;
5722 }
5723
5724 if ( ! empty( $notice ) ) {
5725 $this->_admin_notices->add_sticky(
5726 trim( $notice ),
5727 'premium_activated',
5728 $this->get_text_x_inline( 'W00t',
5729 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5730 );
5731 }
5732 } else {
5733 // Remove sticky message related to premium code activation.
5734 $this->_admin_notices->remove_sticky( 'premium_activated' );
5735
5736 // Activated free code (after had the premium before).
5737 $this->do_action( 'after_free_version_reactivation' );
5738
5739 if ( $this->is_paying() && ! $this->is_premium() ) {
5740 $this->add_complete_upgrade_instructions_notice(
5741 sprintf(
5742 /* translators: %s: License type (e.g. you have a professional license) */
5743 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5744 $this->get_plan_title()
5745 ),
5746 'plan_upgraded'
5747 );
5748 }
5749 }
5750
5751 if ( $this->is_registered() ) {
5752 // Schedule code type changes event.
5753 $this->schedule_install_sync();
5754 }
5755
5756 /**
5757 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5758 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5759 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5760 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5761 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5762 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5763 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5764 */
5765 $this->unregister_uninstall_hook();
5766
5767 $this->clear_module_main_file_cache();
5768
5769 // Update is_premium of latest version.
5770 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5771 }
5772
5773 #endregion
5774
5775 #----------------------------------------------------------------------------------
5776 #region Add-ons
5777 #----------------------------------------------------------------------------------
5778
5779 /**
5780 * Check if add-on installed and activated on site.
5781 *
5782 * @author Vova Feldman (@svovaf)
5783 * @since 1.0.6
5784 *
5785 * @param string|number $id_or_slug
5786 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5787 *
5788 * @return bool
5789 */
5790 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5791 $this->_logger->entrance();
5792
5793 $addon_id = self::get_module_id( $id_or_slug );
5794 $is_activated = self::has_instance( $addon_id );
5795
5796 if ( ! $is_activated ) {
5797 return false;
5798 }
5799
5800 if ( is_bool( $is_premium ) ) {
5801 // Check if the specified code version is activate.
5802 $addon = $this->get_addon_instance( $addon_id );
5803 $is_activated = ( $is_premium === $addon->is_premium() );
5804 }
5805
5806 return $is_activated;
5807 }
5808
5809 /**
5810 * Check if add-on was connected to install
5811 *
5812 * @author Vova Feldman (@svovaf)
5813 * @since 1.1.7
5814 *
5815 * @param string|number $id_or_slug
5816 *
5817 * @return bool
5818 */
5819 function is_addon_connected( $id_or_slug ) {
5820 $this->_logger->entrance();
5821
5822 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5823
5824 $addon_id = self::get_module_id( $id_or_slug );
5825 $addon = $this->get_addon( $addon_id );
5826 $slug = $addon->slug;
5827 if ( ! isset( $sites[ $slug ] ) ) {
5828 return false;
5829 }
5830
5831 $site = $sites[ $slug ];
5832
5833 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5834
5835 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5836 // The given slug do NOT belong to any of the plugin's add-ons.
5837 return false;
5838 }
5839
5840 return ( is_object( $site ) &&
5841 is_numeric( $site->id ) &&
5842 is_numeric( $site->user_id ) &&
5843 FS_Plugin_Plan::is_valid_id( $site->plan_id )
5844 );
5845 }
5846
5847 /**
5848 * Determines if add-on installed.
5849 *
5850 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
5851 *
5852 * @author Vova Feldman (@svovaf)
5853 * @since 1.0.6
5854 *
5855 * @param string|number $id_or_slug
5856 *
5857 * @return bool
5858 */
5859 function is_addon_installed( $id_or_slug ) {
5860 $this->_logger->entrance();
5861
5862 $addon_id = self::get_module_id( $id_or_slug );
5863
5864 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
5865 }
5866
5867 /**
5868 * Get add-on basename.
5869 *
5870 * @author Vova Feldman (@svovaf)
5871 * @since 1.0.6
5872 *
5873 * @param string|number $id_or_slug
5874 *
5875 * @return string
5876 */
5877 function get_addon_basename( $id_or_slug ) {
5878 $addon_id = self::get_module_id( $id_or_slug );
5879
5880 if ( $this->is_addon_activated( $addon_id ) ) {
5881 return self::instance( $addon_id )->get_plugin_basename();
5882 }
5883
5884 $addon = $this->get_addon( $addon_id );
5885 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
5886
5887 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
5888 return $premium_basename;
5889 }
5890
5891 $all_plugins = $this->get_all_plugins();
5892
5893 foreach ( $all_plugins as $basename => $data ) {
5894 if ( $addon->slug === $data['slug'] ||
5895 $addon->premium_slug === $data['slug']
5896 ) {
5897 return $basename;
5898 }
5899 }
5900
5901 $free_basename = "{$addon->slug}/{$addon->slug}.php";
5902
5903 return $free_basename;
5904 }
5905
5906 /**
5907 * Get installed add-ons instances.
5908 *
5909 * @author Vova Feldman (@svovaf)
5910 * @since 1.0.6
5911 *
5912 * @return Freemius[]
5913 */
5914 function get_installed_addons() {
5915 if ( $this->is_addon() ) {
5916 // Add-on cannot have add-ons.
5917 return array();
5918 }
5919
5920 $installed_addons = array();
5921
5922 foreach ( self::$_instances as $instance ) {
5923 if ( $instance->is_addon_of( $this->_plugin->id ) ) {
5924 $installed_addons[] = $instance;
5925 }
5926 }
5927
5928 return $installed_addons;
5929 }
5930
5931 /**
5932 * Check if any add-ons of the plugin are installed.
5933 *
5934 * @author Leo Fajardo (@leorw)
5935 * @since 1.1.1
5936 *
5937 * @return bool
5938 */
5939 function has_installed_addons() {
5940 if ( ! $this->has_addons() ) {
5941 return false;
5942 }
5943
5944 foreach ( self::$_instances as $instance ) {
5945 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
5946 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
5947 return true;
5948 }
5949 }
5950 }
5951
5952 return false;
5953 }
5954
5955 /**
5956 * Tell Freemius that the current plugin is an add-on.
5957 *
5958 * @author Vova Feldman (@svovaf)
5959 * @since 1.0.6
5960 *
5961 * @param number $parent_plugin_id The parent plugin ID
5962 */
5963 function init_addon( $parent_plugin_id ) {
5964 $this->_plugin->parent_plugin_id = $parent_plugin_id;
5965 }
5966
5967 /**
5968 * @author Vova Feldman (@svovaf)
5969 * @since 1.0.6
5970 *
5971 * @return bool
5972 */
5973 function is_addon() {
5974 return (
5975 isset( $this->_plugin->parent_plugin_id ) &&
5976 is_numeric( $this->_plugin->parent_plugin_id )
5977 );
5978 }
5979
5980 /**
5981 * @author Vova Feldman (@svovaf)
5982 * @since 2.3.2
5983 *
5984 * @param number $parent_product_id
5985 *
5986 * @return bool
5987 */
5988 function is_addon_of( $parent_product_id ) {
5989 return (
5990 $this->is_addon() &&
5991 $parent_product_id == $this->_plugin->parent_plugin_id
5992 );
5993 }
5994
5995 /**
5996 * Deactivate add-on if it's premium only and the user does't have a valid license.
5997 *
5998 * @param bool $is_after_trial_cancel
5999 *
6000 * @return bool If add-on was deactivated.
6001 */
6002 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
6003 if ( ! $this->has_free_plan() &&
6004 ! $this->has_features_enabled_license() &&
6005 ! $this->_has_premium_license()
6006 ) {
6007 if ( $this->is_registered() ) {
6008 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
6009 // if (empty($this->_storage->activation_timestamp) ||
6010 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
6011 // ) {
6012 /**
6013 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
6014 *
6015 * Retry syncing the user add-on licenses.
6016 */
6017 // Sync licenses.
6018 $this->_sync_licenses();
6019 // }
6020
6021 // Try to activate premium license.
6022 $this->_activate_license( true );
6023 }
6024
6025 if ( ! $this->has_free_plan() &&
6026 ! $this->has_features_enabled_license() &&
6027 ! $this->_has_premium_license()
6028 ) {
6029 // @todo Check if deactivate plugins also call the deactivation hook.
6030
6031 $this->_parent->_admin_notices->add_sticky(
6032 sprintf(
6033 ( $is_after_trial_cancel ?
6034 $this->_parent->get_text_inline(
6035 '%s free trial was successfully cancelled. Since the add-on is premium only it was automatically deactivated. If you like to use it in the future, you\'ll have to purchase a license.',
6036 'addon-trial-cancelled-message'
6037 ) :
6038 $this->_parent->get_text_inline(
6039 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
6040 'addon-no-license-message'
6041 )
6042 ),
6043 '<b>' . $this->_plugin->title . '</b>'
6044 ) . ' ' . sprintf(
6045 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
6046 $this->_parent->addon_url( $this->_slug ),
6047 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
6048 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
6049 ),
6050 'no_addon_license_' . $this->_slug,
6051 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
6052 ( $is_after_trial_cancel ? 'success' : 'error' )
6053 );
6054
6055 deactivate_plugins( array( $this->_plugin_basename ), true );
6056
6057 return true;
6058 }
6059 }
6060
6061 return false;
6062 }
6063
6064 #endregion
6065
6066 #----------------------------------------------------------------------------------
6067 #region Sandbox
6068 #----------------------------------------------------------------------------------
6069
6070 /**
6071 * Set Freemius into sandbox mode for debugging.
6072 *
6073 * @author Vova Feldman (@svovaf)
6074 * @since 1.0.4
6075 *
6076 * @param string $secret_key
6077 */
6078 function init_sandbox( $secret_key ) {
6079 $this->_plugin->secret_key = $secret_key;
6080
6081 // Update plugin details.
6082 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
6083 }
6084
6085 /**
6086 * Check if running payments in sandbox mode.
6087 *
6088 * @author Vova Feldman (@svovaf)
6089 * @since 1.0.4
6090 *
6091 * @return bool
6092 */
6093 function is_payments_sandbox() {
6094 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
6095 }
6096
6097 #endregion
6098
6099 /**
6100 * Check if running test vs. live plugin.
6101 *
6102 * @author Vova Feldman (@svovaf)
6103 * @since 1.0.5
6104 *
6105 * @return bool
6106 */
6107 function is_live() {
6108 return $this->_plugin->is_live;
6109 }
6110
6111 /**
6112 * Check if super-admin skipped connection for all sites in the network.
6113 *
6114 * @author Vova Feldman (@svovaf)
6115 * @since 2.0.0
6116 */
6117 function is_network_anonymous() {
6118 if ( ! $this->_is_network_active ) {
6119 return false;
6120 }
6121
6122 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
6123
6124 if ( empty( $is_anonymous_ms ) ) {
6125 return false;
6126 }
6127
6128 return $is_anonymous_ms['is'];
6129 }
6130
6131 /**
6132 * Check if super-admin opted-in for all sites in the network.
6133 *
6134 * @author Vova Feldman (@svovaf)
6135 * @since 2.0.0
6136 */
6137 function is_network_connected() {
6138 if ( ! $this->_is_network_active ) {
6139 return false;
6140 }
6141
6142 return $this->_storage->get( 'is_network_connected' );
6143 }
6144
6145 /**
6146 * Check if the user skipped connecting the account with Freemius.
6147 *
6148 * @author Vova Feldman (@svovaf)
6149 * @since 1.0.7
6150 *
6151 * @return bool
6152 */
6153 function is_anonymous() {
6154 if ( ! isset( $this->_is_anonymous ) ) {
6155 if ( $this->is_network_anonymous() ) {
6156 $this->_is_anonymous = true;
6157 } else if ( fs_is_network_admin() ) {
6158 /**
6159 * When not-network-anonymous, yet, running in the network admin, consider as anonymous only when ALL non-delegated sites are set to anonymous.
6160 */
6161 $non_delegated_sites = $this->get_non_delegated_blog_ids();
6162
6163 foreach ( $non_delegated_sites as $blog_id ) {
6164 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6165
6166 if ( empty( $is_anonymous ) || false === $is_anonymous[ 'is' ] ) {
6167 $this->_is_anonymous = false;
6168 break;
6169 }
6170 }
6171
6172 if ( false !== $this->_is_anonymous ) {
6173 $this->_is_anonymous = true;
6174 }
6175 } else {
6176 if ( ! isset( $this->_storage->is_anonymous ) ) {
6177 // Not skipped.
6178 $this->_is_anonymous = false;
6179 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
6180 // For back compatibility, since the variable was boolean before.
6181 $this->_is_anonymous = $this->_storage->is_anonymous;
6182
6183 // Upgrade stored data format to 1.1.3 format.
6184 $this->set_anonymous_mode( $this->_storage->is_anonymous );
6185 } else {
6186 // Version 1.1.3 and later.
6187 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
6188 }
6189 }
6190 }
6191
6192 return $this->_is_anonymous;
6193 }
6194
6195 /**
6196 * Check if the user skipped the connection of a specified site.
6197 *
6198 * @author Vova Feldman (@svovaf)
6199 * @since 2.0.0
6200 *
6201 * @param int $blog_id
6202 *
6203 * @return bool
6204 */
6205 function is_anonymous_site( $blog_id = 0 ) {
6206 if ( $this->is_network_anonymous() ) {
6207 return true;
6208 }
6209
6210 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6211
6212 if ( empty( $is_anonymous ) ) {
6213 return false;
6214 }
6215
6216 return $is_anonymous['is'];
6217 }
6218
6219 /**
6220 * Check if user connected his account and install pending email activation.
6221 *
6222 * @author Vova Feldman (@svovaf)
6223 * @since 1.0.7
6224 *
6225 * @return bool
6226 */
6227 function is_pending_activation() {
6228 return $this->_storage->get( 'is_pending_activation', false );
6229 }
6230
6231 /**
6232 * @author Leo Fajardo (@leorw)
6233 * @since 2.5.0
6234 */
6235 private function clear_pending_activation_mode() {
6236 // Remove the pending activation sticky notice (if it still exists).
6237 $this->_admin_notices->remove_sticky( 'activation_pending' );
6238
6239 // Clear the plugin's pending activation mode.
6240 unset( $this->_storage->is_pending_activation );
6241 }
6242
6243 /**
6244 * Check if plugin must be WordPress.org compliant.
6245 *
6246 * @since 1.0.7
6247 *
6248 * @return bool
6249 */
6250 function is_org_repo_compliant() {
6251 return $this->_is_org_compliant;
6252 }
6253
6254 #--------------------------------------------------------------------------------
6255 #region WP Cron Common
6256 #--------------------------------------------------------------------------------
6257
6258 /**
6259 * @author Vova Feldman (@svovaf)
6260 * @since 2.0.0
6261 *
6262 * @param string $name Cron name.
6263 *
6264 * @return object
6265 */
6266 private function get_cron_data( $name ) {
6267 $this->_logger->entrance( $name );
6268
6269 /**
6270 * @var object $cron_data
6271 */
6272 return $this->_storage->get( "{$name}_cron", null );
6273 }
6274
6275 /**
6276 * @author Vova Feldman (@svovaf)
6277 * @since 2.0.0
6278 *
6279 * @param string $name Cron name.
6280 */
6281 private function clear_cron_data( $name ) {
6282 $this->_logger->entrance( $name );
6283
6284 $this->_storage->remove( "{$name}_cron" );
6285 }
6286
6287 /**
6288 * @author Vova Feldman (@svovaf)
6289 * @since 2.0.0
6290 *
6291 * @param string $name Cron name.
6292 * @param int $cron_blog_id The cron executing blog ID.
6293 */
6294 private function set_cron_data( $name, $cron_blog_id = 0 ) {
6295 $this->_logger->entrance( $name );
6296
6297 $this->_storage->store( "{$name}_cron", (object) array(
6298 'version' => $this->get_plugin_version(),
6299 'blog_id' => $cron_blog_id,
6300 'sdk_version' => $this->version,
6301 'timestamp' => WP_FS__SCRIPT_START_TIME,
6302 'on' => true,
6303 ) );
6304 }
6305
6306 /**
6307 * Get the cron's executing blog ID.
6308 *
6309 * @author Vova Feldman (@svovaf)
6310 * @since 2.0.0
6311 *
6312 * @param string $name Cron name.
6313 *
6314 * @return int
6315 */
6316 private function get_cron_blog_id( $name ) {
6317 $this->_logger->entrance( $name );
6318
6319 if ( ! is_multisite() ) {
6320 // Not a multisite.
6321 return 0;
6322 }
6323
6324 $cron_data = $this->get_cron_data( $name );
6325
6326 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6327 $cron_data->blog_id :
6328 0;
6329 }
6330
6331 /**
6332 * @author Vova Feldman (@svovaf)
6333 * @since 2.0.0
6334 *
6335 * @param string $name Cron name.
6336 *
6337 * @return bool
6338 */
6339 private function is_cron_on( $name ) {
6340 $this->_logger->entrance( $name );
6341
6342 /**
6343 * @var object $cron_data
6344 */
6345 $cron_data = $this->get_cron_data( $name );
6346
6347 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6348 }
6349
6350 /**
6351 * Unix timestamp for previous cron execution or false if never executed.
6352 *
6353 * @author Vova Feldman (@svovaf)
6354 * @since 2.0.0
6355 *
6356 * @param string $name Cron name.
6357 *
6358 * @return int|false
6359 */
6360 private function cron_last_execution( $name ) {
6361 $this->_logger->entrance( $name );
6362
6363 return $this->_storage->get( "{$name}_timestamp" );
6364 }
6365
6366 /**
6367 * Set cron execution time to now.
6368 *
6369 * @author Vova Feldman (@svovaf)
6370 * @since 2.0.0
6371 *
6372 * @param string $name Cron name.
6373 */
6374 private function set_cron_execution_timestamp( $name ) {
6375 $this->_logger->entrance( $name );
6376
6377 $this->_storage->store( "{$name}_timestamp", time() );
6378 }
6379
6380 /**
6381 * Sets the keepalive time to now.
6382 *
6383 * @author Leo Fajardo (@leorw)
6384 * @since 2.2.3
6385 *
6386 * @param bool|null $use_network_level_storage
6387 */
6388 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6389 $this->_logger->entrance();
6390
6391 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6392 }
6393
6394 /**
6395 * Check if cron was executed in the last $period of seconds.
6396 *
6397 * @author Vova Feldman (@svovaf)
6398 * @since 2.0.0
6399 *
6400 * @param string $name Cron name.
6401 * @param int $period In seconds
6402 *
6403 * @return bool
6404 */
6405 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6406 $this->_logger->entrance( $name );
6407
6408 $last_execution = $this->cron_last_execution( $name );
6409
6410 if ( ! is_numeric( $last_execution ) ) {
6411 return false;
6412 }
6413
6414 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6415 }
6416
6417 /**
6418 * WP Cron is executed on a site level. When running in a multisite network environment
6419 * with the network integration activated, for optimization reasons, we are consolidating
6420 * the installs data sync cron to be executed only from a single site.
6421 *
6422 * @author Vova Feldman (@svovaf)
6423 * @since 2.0.0
6424 *
6425 * @param int $except_blog_id Target any except the excluded blog ID.
6426 *
6427 * @return int
6428 */
6429 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6430 if ( ! is_multisite() ) {
6431 return 0;
6432 }
6433
6434 if ( $this->_is_network_active ) {
6435 $network_install_blog_id = $this->_storage->network_install_blog_id;
6436
6437 if (
6438 is_numeric( $network_install_blog_id ) &&
6439 $except_blog_id != $network_install_blog_id &&
6440 self::is_site_active( $network_install_blog_id )
6441 ) {
6442 // Try to run cron from the main network blog.
6443 $install = $this->get_install_by_blog_id( $network_install_blog_id );
6444
6445 if (
6446 is_object( $install ) &&
6447 $this->is_tracking_allowed( $network_install_blog_id, $install )
6448 ) {
6449 return $network_install_blog_id;
6450 }
6451 }
6452 }
6453
6454 // Get first opted-in blog ID with active tracking.
6455 $installs = $this->get_blog_install_map();
6456 foreach ( $installs as $blog_id => $install ) {
6457 if ( $except_blog_id != $blog_id &&
6458 self::is_site_active( $blog_id ) &&
6459 $this->is_tracking_allowed( $blog_id, $install )
6460 ) {
6461 return $blog_id;
6462 }
6463 }
6464
6465 return 0;
6466 }
6467
6468 /**
6469 * @author Vova Feldman (@svovaf)
6470 * @since 2.0.0
6471 *
6472 * @param string $name Cron name.
6473 * @param string $action_tag Callback action tag.
6474 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6475 */
6476 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6477 $this->_logger->entrance( $name );
6478
6479 if ( ! $this->is_cron_on( $name ) ) {
6480 return;
6481 }
6482
6483 $clear_cron = true;
6484 if ( ! $is_network_clear && $this->_is_network_active ) {
6485 $installs = $this->get_blog_install_map();
6486
6487 foreach ( $installs as $blog_id => $install ) {
6488 /**
6489 * @var FS_Site $install
6490 */
6491 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6492 $clear_cron = false;
6493 break;
6494 }
6495 }
6496 }
6497
6498 if ( ! $clear_cron ) {
6499 return;
6500 }
6501
6502 $cron_blog_id = $this->get_cron_blog_id( $name );
6503
6504 $this->clear_cron_data( $name );
6505
6506 if ( 0 < $cron_blog_id ) {
6507 switch_to_blog( $cron_blog_id );
6508 }
6509
6510 if ( empty( $action_tag ) ) {
6511 $action_tag = $name;
6512 }
6513
6514 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6515
6516 if ( 0 < $cron_blog_id ) {
6517 restore_current_blog();
6518 }
6519 }
6520
6521 /**
6522 * Unix timestamp for next cron execution or false if not scheduled.
6523 *
6524 * @author Vova Feldman (@svovaf)
6525 * @since 2.0.0
6526 *
6527 * @param string $name Cron name.
6528 * @param string $action_tag Callback action tag.
6529 *
6530 * @return int|false
6531 */
6532 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6533 $this->_logger->entrance( $name );
6534
6535 if ( ! $this->is_cron_on( $name ) ) {
6536 return false;
6537 }
6538
6539 $cron_blog_id = $this->get_cron_blog_id( $name );
6540
6541 if ( 0 < $cron_blog_id ) {
6542 switch_to_blog( $cron_blog_id );
6543 }
6544
6545 if ( empty( $action_tag ) ) {
6546 $action_tag = $name;
6547 }
6548
6549 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6550
6551 if ( 0 < $cron_blog_id ) {
6552 restore_current_blog();
6553 }
6554
6555 return $next_scheduled;
6556 }
6557
6558 /**
6559 * @author Vova Feldman (@svovaf)
6560 * @since 2.0.0
6561 *
6562 * @param string $name Cron name.
6563 * @param string $action_tag Callback action tag.
6564 * @param string $recurrence 'single' or 'daily'.
6565 * @param int $start_at Defaults to now.
6566 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6567 * @param int $except_blog_id Target any except the excluded blog ID.
6568 */
6569 private function schedule_cron(
6570 $name,
6571 $action_tag = '',
6572 $recurrence = 'single',
6573 $start_at = WP_FS__SCRIPT_START_TIME,
6574 $randomize_start = true,
6575 $except_blog_id = 0
6576 ) {
6577 $this->_logger->entrance( $name );
6578
6579 $this->clear_cron( $name, $action_tag, true );
6580
6581 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6582
6583 if ( is_multisite() && 0 == $cron_blog_id ) {
6584 // Don't schedule cron since couldn't find a target blog.
6585 return;
6586 }
6587
6588 if ( 0 < $cron_blog_id ) {
6589 switch_to_blog( $cron_blog_id );
6590 }
6591
6592 if ( 'daily' === $recurrence ) {
6593 if ( $randomize_start ) {
6594 // Schedule first sync with a random 12 hour time range from now.
6595 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6596 }
6597
6598 // Schedule daily WP cron.
6599 wp_schedule_event(
6600 $start_at,
6601 'daily',
6602 $this->get_action_tag( $action_tag )
6603 );
6604 } else if ( 'single' === $recurrence ) {
6605 // Schedule single cron.
6606 wp_schedule_single_event(
6607 $start_at,
6608 $this->get_action_tag( $action_tag )
6609 );
6610 }
6611
6612 $this->set_cron_data( $name, $cron_blog_id );
6613
6614 if ( 0 < $cron_blog_id ) {
6615 restore_current_blog();
6616 }
6617 }
6618
6619 /**
6620 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6621 * that doesn't halt page loading.
6622 *
6623 * @author Vova Feldman (@svovaf)
6624 * @since 2.0.0
6625 *
6626 * @param string $name Cron name.
6627 * @param callable $callable The function that should be executed.
6628 */
6629 private function execute_cron( $name, $callable ) {
6630 $this->_logger->entrance( $name );
6631
6632 // Store the last time data sync was executed.
6633 $this->set_cron_execution_timestamp( $name );
6634
6635 // Check if API is temporary down.
6636 if ( FS_Api::is_temporary_down() ) {
6637 return;
6638 }
6639
6640 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6641
6642 $users_2_blog_ids = array();
6643
6644 if ( ! is_multisite() ) {
6645 // Add dummy blog.
6646 $users_2_blog_ids[0] = array( 0 );
6647 } else {
6648 $installs = $this->get_blog_install_map();
6649 foreach ( $installs as $blog_id => $install ) {
6650 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6651 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6652 $users_2_blog_ids[ $install->user_id ] = array();
6653 }
6654
6655 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6656 }
6657 }
6658 }
6659
6660 $current_blog_id = get_current_blog_id();
6661
6662 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6663 if ( 0 < $blog_ids[0] ) {
6664 $this->switch_to_blog( $blog_ids[0] );
6665 }
6666
6667 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6668
6669 foreach ( $blog_ids as $blog_id ) {
6670 $this->do_action( "after_{$name}_cron", $blog_id );
6671 }
6672 }
6673
6674 if ( is_multisite() ) {
6675 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6676
6677 $this->do_action( "after_{$name}_cron_multisite" );
6678 }
6679 }
6680
6681 #endregion
6682
6683 #----------------------------------------------------------------------------------
6684 #region Daily Sync Cron
6685 #----------------------------------------------------------------------------------
6686
6687
6688 /**
6689 * @author Vova Feldman (@svovaf)
6690 * @since 2.0.0
6691 *
6692 * @return bool
6693 */
6694 private function is_sync_cron_scheduled() {
6695 return $this->is_cron_on( 'sync' );
6696 }
6697
6698 /**
6699 * Get the sync cron's executing blog ID.
6700 *
6701 * @author Vova Feldman (@svovaf)
6702 * @since 2.0.0
6703 *
6704 * @return int
6705 */
6706 private function get_sync_cron_blog_id() {
6707 return $this->get_cron_blog_id( 'sync' );
6708 }
6709
6710 /**
6711 * @author Vova Feldman (@svovaf)
6712 * @since 1.1.7.3
6713 */
6714 private function run_manual_sync() {
6715 if ( ! $this->is_user_admin() ) {
6716 return;
6717 }
6718
6719 // Run manual sync.
6720 $this->_sync_cron();
6721
6722 // Reschedule next cron to run 24 hours from now (performance optimization).
6723 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6724 }
6725
6726 /**
6727 * Data sync cron job. Replaces the background sync non blocking HTTP request
6728 * that doesn't halt page loading.
6729 *
6730 * @author Vova Feldman (@svovaf)
6731 * @since 1.1.7.3
6732 * @since 2.0.0 Consolidate all the data sync into the same cron for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6733 */
6734 function _sync_cron() {
6735 $this->_logger->entrance();
6736
6737 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6738 }
6739
6740 /**
6741 * The actual data sync cron logic.
6742 *
6743 * @author Vova Feldman (@svovaf)
6744 * @since 2.0.0
6745 *
6746 * @param int[] $blog_ids
6747 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6748 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6749 * updates for a single site in case `execute_cron` has switched to a different blog.
6750 */
6751 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6752 if ( $this->is_registered() ) {
6753 if ( $this->has_paid_plan() ) {
6754 // Initiate background plan sync.
6755 $this->_sync_license( true, false, $current_blog_id );
6756
6757 if ( $this->is_paying() ) {
6758 // Check for premium plugin updates.
6759 $this->check_updates( true );
6760 }
6761 } else {
6762 // Sync install(s) (only if something changed locally).
6763 if ( 1 < count( $blog_ids ) ) {
6764 $this->sync_installs();
6765 } else {
6766 $this->sync_install();
6767 }
6768
6769 $this->maybe_sync_install_user();
6770 }
6771 }
6772 }
6773
6774 /**
6775 * Check if sync was executed in the last $period of seconds.
6776 *
6777 * @author Vova Feldman (@svovaf)
6778 * @since 1.1.7.3
6779 *
6780 * @param int $period In seconds
6781 *
6782 * @return bool
6783 */
6784 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6785 return $this->is_cron_executed( 'sync', $period );
6786 }
6787
6788 /**
6789 * @author Vova Feldman (@svovaf)
6790 * @since 1.1.7.3
6791 *
6792 * @return bool
6793 */
6794 private function is_sync_cron_on() {
6795 return $this->is_cron_on( 'sync' );
6796 }
6797
6798 /**
6799 * @author Leo Fajardo (@leorw)
6800 * @since 2.5.0
6801 */
6802 private function maybe_schedule_sync_cron() {
6803 $next_schedule = $this->next_sync_cron();
6804
6805 // The event is properly scheduled, so no need to reschedule it.
6806 if (
6807 is_numeric( $next_schedule ) &&
6808 $next_schedule > time()
6809 ) {
6810 return;
6811 }
6812
6813 $this->schedule_sync_cron();
6814 }
6815
6816 /**
6817 * @author Vova Feldman (@svovaf)
6818 * @since 1.1.7.3
6819 *
6820 * @param int $start_at Defaults to now.
6821 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6822 * @param int $except_blog_id Since 2.0.0 when running in a multisite network environment, the cron execution is consolidated. This param allows excluding excluded specified blog ID from being the cron executor.
6823 */
6824 private function schedule_sync_cron(
6825 $start_at = WP_FS__SCRIPT_START_TIME,
6826 $randomize_start = true,
6827 $except_blog_id = 0
6828 ) {
6829 $this->schedule_cron(
6830 'sync',
6831 'data_sync',
6832 'daily',
6833 $start_at,
6834 $randomize_start,
6835 $except_blog_id
6836 );
6837 }
6838
6839 /**
6840 * Add the actual sync function to the cron job hook.
6841 *
6842 * @author Vova Feldman (@svovaf)
6843 * @since 1.1.7.3
6844 */
6845 private function hook_callback_to_sync_cron() {
6846 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6847 }
6848
6849 /**
6850 * @author Vova Feldman (@svovaf)
6851 * @since 1.1.7.3
6852 *
6853 * @param bool $is_network_clear Since 2.0.0 If set to TRUE, clear sync cron even if there are installs that are still connected.
6854 */
6855 private function clear_sync_cron( $is_network_clear = false ) {
6856 $this->_logger->entrance();
6857
6858 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6859 }
6860
6861 /**
6862 * Unix timestamp for next sync cron execution or false if not scheduled.
6863 *
6864 * @author Vova Feldman (@svovaf)
6865 * @since 1.1.7.3
6866 *
6867 * @return int|false
6868 */
6869 function next_sync_cron() {
6870 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6871 }
6872
6873 /**
6874 * Unix timestamp for previous sync cron execution or false if never executed.
6875 *
6876 * @author Vova Feldman (@svovaf)
6877 * @since 1.1.7.3
6878 *
6879 * @return int|false
6880 */
6881 function last_sync_cron() {
6882 return $this->cron_last_execution( 'sync' );
6883 }
6884
6885 #endregion Daily Sync Cron ------------------------------------------------------------------
6886
6887 #----------------------------------------------------------------------------------
6888 #region Async Install Sync
6889 #----------------------------------------------------------------------------------
6890
6891 /**
6892 * @author Vova Feldman (@svovaf)
6893 * @since 1.1.7.3
6894 *
6895 * @return bool
6896 */
6897 private function is_install_sync_scheduled() {
6898 return $this->is_cron_on( 'install_sync' );
6899 }
6900
6901 /**
6902 * Get the sync cron's executing blog ID.
6903 *
6904 * @author Vova Feldman (@svovaf)
6905 * @since 2.0.0
6906 *
6907 * @return int
6908 */
6909 private function get_install_sync_cron_blog_id() {
6910 return $this->get_cron_blog_id( 'install_sync' );
6911 }
6912
6913 /**
6914 * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
6915 *
6916 * @author Vova Feldman (@svovaf)
6917 * @since 1.1.7.3
6918 *
6919 * @param int $except_blog_id Since 2.0.0 when running in a multisite network environment, the cron execution is consolidated. This param allows excluding excluded specified blog ID from being the cron executor.
6920 */
6921 private function schedule_install_sync( $except_blog_id = 0 ) {
6922 if ( $this->is_clone() ) {
6923 return;
6924 }
6925
6926 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
6927 }
6928
6929 /**
6930 * Unix timestamp for previous install sync cron execution or false if never executed.
6931 *
6932 * @todo There's some very strange bug that $this->_storage->install_sync_timestamp value is not being updated. But for sure the sync event is working.
6933 *
6934 * @author Vova Feldman (@svovaf)
6935 * @since 1.1.7.3
6936 *
6937 * @return int|false
6938 */
6939 function last_install_sync() {
6940 return $this->cron_last_execution( 'install_sync' );
6941 }
6942
6943 /**
6944 * Unix timestamp for next install sync cron execution or false if not scheduled.
6945 *
6946 * @author Vova Feldman (@svovaf)
6947 * @since 1.1.7.3
6948 *
6949 * @return int|false
6950 */
6951 function next_install_sync() {
6952 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
6953 }
6954
6955 /**
6956 * Add the actual install sync function to the cron job hook.
6957 *
6958 * @author Vova Feldman (@svovaf)
6959 * @since 1.1.7.3
6960 */
6961 private function hook_callback_to_install_sync() {
6962 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
6963 }
6964
6965 /**
6966 * @author Vova Feldman (@svovaf)
6967 * @since 1.1.7.3
6968 *
6969 * @param bool $is_network_clear Since 2.0.0 If set to TRUE, clear sync cron even if there are installs that are still connected.
6970 */
6971 private function clear_install_sync_cron( $is_network_clear = false ) {
6972 $this->_logger->entrance();
6973
6974 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
6975 }
6976
6977 /**
6978 * @author Vova Feldman (@svovaf)
6979 * @since 1.1.7.3
6980 * @since 2.0.0 Consolidate all the data sync into the same cron for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6981 */
6982 public function _run_sync_install() {
6983 $this->_logger->entrance();
6984
6985 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
6986 }
6987
6988 /**
6989 * The actual install(s) sync cron logic.
6990 *
6991 * @author Vova Feldman (@svovaf)
6992 * @since 2.0.0
6993 *
6994 * @param int[] $blog_ids
6995 * @param int|null $current_blog_id
6996 */
6997 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
6998 if ( $this->is_registered() ) {
6999 if ( 1 < count( $blog_ids ) ) {
7000 $this->sync_installs( array(), true );
7001 } else {
7002 $this->sync_install( array(), true );
7003 }
7004
7005 $this->maybe_sync_install_user();
7006 }
7007 }
7008
7009 #endregion Async Install Sync ------------------------------------------------------------------
7010
7011 /**
7012 * Show a notice that activation is currently pending.
7013 *
7014 * @todo Add some sort of mechanism to allow users to update the email address they would like to opt-in with when $is_suspicious_email is true.
7015 *
7016 * @author Vova Feldman (@svovaf)
7017 * @since 1.0.7
7018 *
7019 * @param bool|string $email_address
7020 * @param bool $is_pending_trial Since 1.2.1.5
7021 * @param bool $is_suspicious_email Since 2.5.0 Set to true when there's an indication that email address the user opted in with is fake/dummy/placeholder.
7022 * @param bool $has_upgrade_context Since 2.5.3
7023 * @param bool $support_email_address Since 2.5.3
7024 */
7025 function _add_pending_activation_notice(
7026 $email_address = false,
7027 $is_pending_trial = false,
7028 $is_suspicious_email = false,
7029 $has_upgrade_context = false,
7030 $support_email_address = false
7031 ) {
7032 if ( ! is_string( $email_address ) ) {
7033 $current_user = self::_get_current_wp_user();
7034 $email_address = $current_user->user_email;
7035 }
7036
7037 $formatted_message_args = array(
7038 "<b>{$this->get_plugin_name()}</b>",
7039 "<b>{$email_address}</b>",
7040 );
7041
7042 if ( ! $has_upgrade_context || ! fs_is_network_admin() ) {
7043 /* translators: %3$s: action (e.g.: "start the trial" or "complete the opt-in") */
7044 $formatted_message = $this->get_text_inline( 'You should receive a confirmation email for %1$s to your mailbox at %2$s. Please make sure you click the button in that email to %3$s.', 'pending-activation-message' );
7045
7046 $formatted_message_args[] = $is_pending_trial ?
7047 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
7048 $this->get_text_inline( 'complete the opt-in', 'complete-the-opt-in' );
7049
7050 $notice_title = $this->get_text_inline( 'Thanks!', 'thanks' );
7051 } else {
7052 /* translators: %3$s: What the user is expected to receive via email (e.g.: "the installation instructions" or "a license key") */
7053 $formatted_message = $this->get_text_inline( 'You should receive %3$s for %1$s to your mailbox at %2$s in the next 5 minutes.' );
7054
7055 if ( $this->has_release_on_freemius() ) {
7056 $formatted_message_args[] = $this->get_text_x_inline(
7057 'the installation instructions',
7058 'Part of the message telling the user what they should receive via email.',
7059 'the-installation-instructions-phrase'
7060 );
7061 } else {
7062 $formatted_message_args[] = $this->get_text_x_inline(
7063 'a license key',
7064 'Part of the message telling the user what they should receive via email.',
7065 'a-license-key-phrase'
7066 );
7067
7068 $formatted_message .= ( ' ' . sprintf(
7069 /* translators: %s: activation link (e.g.: <a>Click here</a>) */
7070 $this->get_text_inline( '%s to activate the license once you get it.', 'license-activation-link-message' ),
7071 sprintf(
7072 '<b><a href="%s">%s</a></b>',
7073 $this->get_activation_url( array(
7074 'fs_action' => 'reset_pending_activation_mode',
7075 'require_license' => 'true',
7076 'fs_unique_affix' => $this->get_unique_affix(),
7077 ) ),
7078 $this->get_text_x_inline( 'Click here', 'Part of an activation link message.', 'click-here' )
7079 )
7080 ) );
7081 }
7082
7083 $formatted_message_args[] = ( ! empty( $support_email_address ) ) ?
7084 ( "<b>{$support_email_address}</b>" ) :
7085 $this->get_text_x_inline(
7086 "the product's support email address",
7087 'Part of the message that tells the user to check their spam folder for a specific email.',
7088 'product-support-email-address-phrase'
7089 );
7090
7091 $formatted_message .= ( ' ' . $this->get_text_inline( 'If you didn\'t get the email, try checking your spam folder or search for emails from %4$s.', 'check-spam-folder-message' ) );
7092
7093 $notice_title = $this->get_text_inline( 'Thanks for upgrading.', 'after-upgrade-thank-you-message' );
7094 }
7095
7096 $this->_admin_notices->add_sticky(
7097 vsprintf( $formatted_message, $formatted_message_args ),
7098 'activation_pending',
7099 $notice_title
7100 );
7101 }
7102
7103 /**
7104 * Check if currently in plugin activation.
7105 *
7106 * @author Vova Feldman (@svovaf)
7107 * @since 1.1.4
7108 *
7109 * @return bool
7110 */
7111 function is_plugin_activation() {
7112 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7113
7114 return !empty($result);
7115 }
7116
7117 /**
7118 *
7119 * NOTE: admin_menu action executed before admin_init.
7120 *
7121 * @author Vova Feldman (@svovaf)
7122 * @since 1.0.7
7123 */
7124 function _admin_init_action() {
7125 $is_migration = $this->is_migration();
7126
7127 /**
7128 * Automatically redirect to connect/activation page after plugin activation.
7129 *
7130 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
7131 */
7132 if ( $this->is_plugin_activation() ) {
7133 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7134
7135 if ( isset( $_GET['activate-multi'] ) ) {
7136 /**
7137 * Don't redirect if activating multiple plugins at once (bulk activation).
7138 */
7139 } else if (
7140 self::is_deactivation_snoozed() &&
7141 (
7142 // Either running the free code base.
7143 ! $this->is_premium() ||
7144 // Or if has a free version.
7145 ! $this->is_only_premium() ||
7146 // If premium only, don't redirect if license is activated.
7147 ( $this->is_registered() && ! $this->can_use_premium_code() )
7148 )
7149 ) {
7150 /**
7151 * Don't redirect if activating during the deactivation snooze period (aka troubleshooting), unless activating a paid product version that the admin didn't enter its license key yet.
7152 */
7153 } else if ( ! $is_migration ) {
7154 $this->_redirect_on_activation_hook();
7155 return;
7156 }
7157 }
7158
7159 if ( $is_migration ) {
7160 return;
7161 }
7162
7163 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
7164 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
7165
7166 $this->skip_connection( fs_is_network_admin() );
7167
7168 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
7169 }
7170
7171 if ( $this->is_network_activation_mode() &&
7172 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
7173 ) {
7174 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
7175
7176 $this->delegate_connection();
7177
7178 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
7179 }
7180
7181 $this->_add_upgrade_action_link();
7182
7183 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
7184 (
7185 ( true === $this->_storage->require_license_activation ) ||
7186 // Not registered nor anonymous.
7187 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
7188 // OR, network level and in network upgrade mode.
7189 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
7190 )
7191 ) {
7192 if ( ! $this->is_pending_activation() ) {
7193 if ( ! $this->is_activation_page() ) {
7194 /**
7195 * If a user visits any other admin page before activating the premium-only theme with a valid
7196 * license, reactivate the previous theme.
7197 *
7198 * @author Leo Fajardo (@leorw)
7199 * @since 1.2.2
7200 */
7201 if ( $this->is_theme() &&
7202 ! $this->has_settings_menu() &&
7203 ! isset( $_REQUEST['fs_action'] ) &&
7204 $this->can_activate_previous_theme()
7205 ) {
7206 if ( $this->is_only_premium() ) {
7207 $this->activate_previous_theme();
7208 return;
7209 }
7210
7211 if ( true === $this->_storage->require_license_activation ) {
7212 $this->_storage->require_license_activation = false;
7213 }
7214 }
7215
7216 if ( ! fs_is_network_admin() &&
7217 $this->is_network_activation_mode() &&
7218 ! $this->is_delegated_connection()
7219 ) {
7220 return;
7221 }
7222
7223 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
7224 if ( ! $this->_anonymous_mode &&
7225 ( ! $this->is_addon() || ! $this->_parent->is_anonymous() ) ) {
7226 // Show notice for new plugin installations.
7227 $this->_admin_notices->add(
7228 sprintf(
7229 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
7230 sprintf( '<b><a href="%s">%s</a></b>',
7231 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
7232 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
7233 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
7234 )
7235 ),
7236 '',
7237 'update-nag'
7238 );
7239 }
7240 } else {
7241 if ( $this->should_add_sticky_optin_notice() ) {
7242 $this->add_sticky_optin_admin_notice();
7243 }
7244
7245 if ( $this->has_filter( 'optin_pointer_element' ) ) {
7246 // Don't show admin nag if plugin update.
7247 wp_enqueue_script( 'wp-pointer' );
7248 wp_enqueue_style( 'wp-pointer' );
7249
7250 $this->_enqueue_connect_essentials();
7251
7252 add_action( 'admin_print_footer_scripts', array(
7253 $this,
7254 '_add_connect_pointer_script'
7255 ) );
7256 }
7257 }
7258 }
7259 }
7260
7261 if ( $this->show_opt_in_on_themes_page() &&
7262 $this->is_activation_page()
7263 ) {
7264 $this->_show_theme_activation_optin_dialog();
7265 }
7266 }
7267 }
7268
7269 /**
7270 * @author Vova Feldman (@svovaf)
7271 * @since 2.0.0
7272 *
7273 * @return bool
7274 */
7275 private function should_add_sticky_optin_notice() {
7276 if ( $this->is_addon() && $this->_parent->is_anonymous() ) {
7277 return false;
7278 }
7279
7280 if ( fs_is_network_admin() ) {
7281 if ( ! $this->_is_network_active ) {
7282 return false;
7283 }
7284
7285 if ( ! $this->is_network_activation_mode() ) {
7286 return false;
7287 }
7288
7289 return ! isset( $this->_storage->sticky_optin_added_ms );
7290 }
7291
7292 if ( ! $this->is_activation_mode() ) {
7293 return false;
7294 }
7295
7296 // If running from a blog admin and delegated the connection.
7297 return ! isset( $this->_storage->sticky_optin_added );
7298 }
7299
7300 /**
7301 * @author Leo Fajardo (@leorw)
7302 * @since 2.0.0
7303 */
7304 private function add_sticky_optin_admin_notice() {
7305 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7306 $this->_storage->sticky_optin_added = true;
7307 } else {
7308 $this->_storage->sticky_optin_added_ms = true;
7309 }
7310
7311 // Show notice for new plugin installations.
7312 $this->_admin_notices->add_sticky(
7313 sprintf(
7314 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7315 $this->_module_type,
7316 sprintf( '<b><a href="%s">%s</a></b>',
7317 $this->get_activation_url(),
7318 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7319 )
7320 ),
7321 'connect_account',
7322 '',
7323 'update-nag'
7324 );
7325 }
7326
7327 /**
7328 * Enqueue connect requires scripts and styles.
7329 *
7330 * @author Vova Feldman (@svovaf)
7331 * @since 1.1.4
7332 */
7333 function _enqueue_connect_essentials() {
7334 wp_enqueue_script( 'jquery' );
7335 wp_enqueue_script( 'json2' );
7336
7337 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.js' );
7338 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7339 }
7340
7341 /**
7342 * Add connect / opt-in pointer.
7343 *
7344 * @author Vova Feldman (@svovaf)
7345 * @since 1.1.4
7346 */
7347 function _add_connect_pointer_script() {
7348 $vars = array( 'id' => $this->_module_id );
7349 $pointer_content = fs_get_template( 'connect.php', $vars );
7350 ?>
7351 <script type="text/javascript">// <![CDATA[
7352 jQuery(document).ready(function ($) {
7353 if ('undefined' !== typeof(jQuery().pointer)) {
7354
7355 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7356
7357 if (element.length > 0) {
7358 var optin = $(element).pointer($.extend(true, {}, {
7359 content : <?php echo json_encode( $pointer_content ) ?>,
7360 position : {
7361 edge : 'left',
7362 align: 'center'
7363 },
7364 buttons : function () {
7365 // Don't show pointer buttons.
7366 return '';
7367 },
7368 pointerWidth: 482
7369 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7370
7371 <?php
7372 echo $this->apply_filters( 'optin_pointer_execute', "
7373
7374 optin.pointer('open');
7375
7376 // Tag the opt-in pointer with custom class.
7377 $('.wp-pointer #fs_connect')
7378 .parents('.wp-pointer.wp-pointer-top')
7379 .addClass('fs-opt-in-pointer');
7380
7381 ", 'element', 'optin' ) ?>
7382 }
7383 }
7384 });
7385 // ]]></script>
7386 <?php
7387 }
7388
7389 /**
7390 * Return current page's URL.
7391 *
7392 * @author Vova Feldman (@svovaf)
7393 * @since 1.0.7
7394 *
7395 * @return string
7396 */
7397 static function current_page_url() {
7398 $url = 'http';
7399
7400 if ( isset( $_SERVER["HTTPS"] ) ) {
7401 if ( $_SERVER["HTTPS"] == "on" ) {
7402 $url .= "s";
7403 }
7404 }
7405 $url .= "://";
7406 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7407 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7408 } else {
7409 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7410 }
7411
7412 return esc_url( $url );
7413 }
7414
7415 /**
7416 * Check if the current page is the plugin's main admin settings page.
7417 *
7418 * @author Vova Feldman (@svovaf)
7419 * @since 1.0.7
7420 *
7421 * @return bool
7422 */
7423 function _is_plugin_page() {
7424 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7425 fs_is_plugin_page( $this->_slug );
7426 }
7427
7428 /* Events
7429 ------------------------------------------------------------------------------------------------------------------*/
7430 /**
7431 * Delete site install from Database.
7432 *
7433 * @author Vova Feldman (@svovaf)
7434 * @since 1.0.1
7435 *
7436 * @param bool $store
7437 * @param int|null $blog_id Since 2.0.0
7438 *
7439 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7440 */
7441 function _delete_site( $store = true, $blog_id = null ) {
7442 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7443 }
7444
7445 /**
7446 * Delete site install from Database.
7447 *
7448 * @author Vova Feldman (@svovaf)
7449 * @since 1.2.2.7
7450 *
7451 * @param string $slug
7452 * @param string $module_type
7453 * @param bool $store
7454 * @param int|null $blog_id Since 2.0.0
7455 *
7456 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7457 */
7458 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7459 $sites = self::get_all_sites( $module_type, $blog_id );
7460
7461 $install_id = false;
7462
7463 if ( isset( $sites[ $slug ] ) ) {
7464 if ( is_object( $sites[ $slug ] ) ) {
7465 $install_id = $sites[ $slug ]->id;
7466 }
7467
7468 unset( $sites[ $slug ] );
7469
7470 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7471 }
7472
7473 return $install_id;
7474 }
7475
7476 /**
7477 * Delete user.
7478 *
7479 * @author Vova Feldman (@svovaf)
7480 * @since 2.0.0
7481 *
7482 * @param number $user_id
7483 * @param bool $store
7484 *
7485 * @return false|int The user ID if deleted. Otherwise, FALSE (when install not exist).
7486 */
7487 private static function delete_user( $user_id, $store = true ) {
7488 $users = self::get_all_users();
7489
7490 if ( ! is_array( $users ) || ! isset( $users[ $user_id ] ) ) {
7491 return false;
7492 }
7493
7494 unset( $users[ $user_id ] );
7495
7496 self::$_accounts->set_option( 'users', $users, $store );
7497
7498 return $user_id;
7499 }
7500
7501 /**
7502 * Delete plugin's plans information.
7503 *
7504 * @param bool $store Flush to Database if true.
7505 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7506 *
7507 * @author Vova Feldman (@svovaf)
7508 * @since 1.0.9
7509 */
7510 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7511 $this->_logger->entrance();
7512
7513 $plans = self::get_all_plans( $this->_module_type );
7514
7515 $plans_to_keep = array();
7516
7517 if ( $keep_associated_plans ) {
7518 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7519 foreach ( $plans_ids_to_keep as $plan_id ) {
7520 $plan = self::_get_plan_by_id( $plan_id );
7521 if ( is_object( $plan ) ) {
7522 $plans_to_keep[] = self::_encrypt_entity( $plan );
7523 }
7524 }
7525 }
7526
7527 if ( ! empty( $plans_to_keep ) ) {
7528 $plans[ $this->_slug ] = $plans_to_keep;
7529 } else {
7530 unset( $plans[ $this->_slug ] );
7531 }
7532
7533 $this->set_account_option( 'plans', $plans, $store );
7534 }
7535
7536 /**
7537 * Delete all plugin licenses.
7538 *
7539 * @author Vova Feldman (@svovaf)
7540 * @since 1.0.9
7541 *
7542 * @param bool $store
7543 */
7544 private function _delete_licenses( $store = true ) {
7545 $this->_logger->entrance();
7546
7547 $all_licenses = self::get_all_licenses();
7548
7549 unset( $all_licenses[ $this->_module_id ] );
7550
7551 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7552 }
7553
7554 /**
7555 * Check if Freemius was added on new plugin installation.
7556 *
7557 * @author Vova Feldman (@svovaf)
7558 * @since 1.1.5
7559 *
7560 * @return bool
7561 */
7562 function is_plugin_new_install() {
7563 return isset( $this->_storage->is_plugin_new_install ) &&
7564 $this->_storage->is_plugin_new_install;
7565 }
7566
7567 /**
7568 * Check if it's the first plugin release that is running Freemius.
7569 *
7570 * @author Vova Feldman (@svovaf)
7571 * @since 1.2.1.5
7572 *
7573 * @return bool
7574 */
7575 function is_first_freemius_powered_version() {
7576 return empty( $this->_storage->plugin_last_version );
7577 }
7578
7579 /**
7580 * @author Leo Fajardo (@leorw)
7581 * @since 1.2.2
7582 *
7583 * @return bool|string
7584 */
7585 private function get_previous_theme_slug() {
7586 return isset( $this->_storage->previous_theme ) ?
7587 $this->_storage->previous_theme :
7588 false;
7589 }
7590
7591 /**
7592 * @author Leo Fajardo (@leorw)
7593 * @since 1.2.2
7594 *
7595 * @return bool
7596 */
7597 private function can_activate_previous_theme() {
7598 return $this->can_activate_theme( $this->get_previous_theme_slug() );
7599 }
7600
7601 /**
7602 * @author Leo Fajardo (@leorw)
7603 * @since 2.5.0
7604 *
7605 * @return bool
7606 */
7607 private function can_activate_theme( $slug ) {
7608 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7609 $theme_instance = wp_get_theme( $slug );
7610
7611 return $theme_instance->exists();
7612 }
7613
7614 return false;
7615 }
7616
7617 /**
7618 * @author Leo Fajardo (@leorw)
7619 * @since 1.2.2
7620 */
7621 private function activate_previous_theme() {
7622 switch_theme( $this->get_previous_theme_slug() );
7623 unset( $this->_storage->previous_theme );
7624
7625 global $pagenow;
7626 if ( 'themes.php' === $pagenow ) {
7627 /**
7628 * Refresh the active theme information.
7629 *
7630 * @author Leo Fajardo (@leorw)
7631 * @since 1.2.2
7632 */
7633 fs_redirect( $this->admin_url( $pagenow ) );
7634 }
7635 }
7636
7637 /**
7638 * @author Leo Fajardo (@leorw)
7639 * @since 1.2.2
7640 *
7641 * @return string
7642 */
7643 function get_previous_theme_activation_url() {
7644 if ( ! $this->can_activate_previous_theme() ) {
7645 return '';
7646 }
7647
7648 /**
7649 * Activation URL
7650 *
7651 * @author Leo Fajardo (@leorw)
7652 * @since 1.2.2
7653 */
7654 return wp_nonce_url(
7655 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7656 'switch-theme_' . $this->get_previous_theme_slug()
7657 );
7658 }
7659
7660 /**
7661 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7662 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7663 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7664 * theme doesn't exist, then there will be no close button.
7665 *
7666 * @author Leo Fajardo (@leorw)
7667 * @since 1.2.2
7668 *
7669 * @param string $slug_or_name Old theme's slug or name.
7670 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7671 */
7672 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7673 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7674 $old_theme->get_stylesheet() :
7675 $slug_or_name;
7676
7677 $this->_activate_plugin_event_hook();
7678 }
7679
7680 /**
7681 * Plugin activated hook.
7682 *
7683 * @author Vova Feldman (@svovaf)
7684 * @since 1.0.1
7685 *
7686 * @uses FS_Api
7687 */
7688 function _activate_plugin_event_hook() {
7689 $this->_logger->entrance( 'slug = ' . $this->_slug );
7690
7691 if ( ! $this->is_user_admin() ) {
7692 return;
7693 }
7694
7695 $this->unregister_uninstall_hook();
7696
7697 // Clear API cache on activation.
7698 FS_Api::clear_cache();
7699
7700 $is_premium_version_activation = $this->is_plugin() ?
7701 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7702 $this->is_premium();
7703
7704 if ( $is_premium_version_activation && $this->is_pending_activation() ) {
7705 $this->clear_pending_activation_mode();
7706 }
7707
7708 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7709
7710 if ( $this->is_plugin() ) {
7711 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7712 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7713 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7714 $other_version_basename = $is_premium_version_activation ?
7715 $this->_free_plugin_basename :
7716 $this->premium_plugin_basename();
7717
7718 if ( ! $this->_is_network_active ) {
7719 /**
7720 * Themes are always network activated, but the ACTUAL activation is per site.
7721 *
7722 * During the activation, the plugin isn't yet active, therefore,
7723 * _is_network_active will be set to false even if it's a network level
7724 * activation. So we need to fix that by looking at the is_network_admin() value.
7725 *
7726 * @author Vova Feldman
7727 */
7728 $this->_is_network_active = (
7729 $this->_is_multisite_integrated &&
7730 fs_is_network_admin()
7731 );
7732 }
7733
7734 /**
7735 * If the other module version is active, deactivate it.
7736 *
7737 * is_plugin_active() checks if the plugin is active on the site or the network level and
7738 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7739 *
7740 * @author Leo Fajardo (@leorw)
7741 * @since 1.2.2
7742 */
7743 if (
7744 is_plugin_active( $other_version_basename ) &&
7745 $this->apply_filters( 'deactivate_on_activation', true )
7746 ) {
7747 deactivate_plugins( $other_version_basename );
7748 }
7749 }
7750
7751 if ( $this->is_registered() ) {
7752 if ( $is_premium_version_activation ) {
7753 $this->reconnect_locally();
7754 }
7755
7756
7757 // Schedule re-activation event and sync.
7758 // $this->sync_install( array(), true );
7759 $this->schedule_install_sync();
7760
7761 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7762 if ( $is_premium_version_activation ) {
7763 $this->_admin_notices->add(
7764 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7765 $this->get_text_x_inline( 'W00t',
7766 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7767 );
7768 }
7769 } else if ( $this->is_anonymous() ) {
7770 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7771 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7772 $network = true;
7773 } else {
7774 $plugin_version = isset( $this->_storage->is_anonymous ) ?
7775 $this->_storage->is_anonymous['version'] :
7776 null;
7777 $network = false;
7778 }
7779
7780 /**
7781 * Reset "skipped" click cache on the following:
7782 * 1. Freemius DEV mode.
7783 * 2. WordPress DEBUG mode.
7784 * 3. If a plugin and the user skipped the exact same version before.
7785 *
7786 * @since 1.2.2.7 Ulrich Pogson (@grapplerulrich) asked to not reset the SKIPPED flag if the exact same THEME version was activated before unless the developer is running with WP_DEBUG on, or Freemius debug mode on (WP_FS__DEV_MODE).
7787 *
7788 * @todo 4. If explicitly asked to retry after every activation.
7789 */
7790 if ( WP_FS__DEV_MODE ||
7791 (
7792 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7793 $this->get_plugin_version() == $plugin_version
7794 )
7795 ) {
7796 $this->reset_anonymous_mode( $network );
7797 }
7798 }
7799
7800 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7801
7802 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7803 /**
7804 * When activating an add-on, try to also activate a license.
7805 *
7806 * @author Leo Fajardo (@leorw)
7807 * @since 2.3.0
7808 */
7809 if ( ! $this->_is_network_active ) {
7810 $this->maybe_activate_addon_license();
7811 } else {
7812 $this->maybe_network_activate_addon_license();
7813 }
7814
7815 /**
7816 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7817 *
7818 * @author Leo Fajardo (@leorw)
7819 * @since 2.3.0
7820 */
7821 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7822
7823 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7824 $this->_storage->require_license_activation = false;
7825 }
7826 }
7827
7828 if (
7829 $is_premium_version_activation &&
7830 (
7831 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7832 (
7833 $this->is_registered() &&
7834 ! $is_trial_or_has_features_enabled_license
7835 )
7836 )
7837 ) {
7838 $this->_storage->require_license_activation = true;
7839 }
7840
7841 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7842 /**
7843 * If no previous version of plugin's version exist, it means that it's either
7844 * the first time that the plugin installed on the site, or the plugin was installed
7845 * before but didn't have Freemius integrated.
7846 *
7847 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7848 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7849 * only after immediate activation.
7850 *
7851 * @since 1.1.4
7852 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7853 */
7854 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7855 }
7856
7857 /**
7858 * Also flush when activating the premium version so that even if Freemius was off before, the API
7859 * connectivity test can be run again.
7860 *
7861 * @author Leo Fajardo (@leorw)
7862 * @since 2.2.3.1
7863 */
7864 $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation );
7865
7866 if ( ! $this->_anonymous_mode &&
7867 ( false !== $has_api_connectivity ) &&
7868 ! $this->_isAutoInstall
7869 ) {
7870 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7871 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7872 }
7873
7874 /**
7875 * Activation hook is executed after the plugin's main file is loaded, therefore,
7876 * after the plugin was loaded. The logic is located at activate_plugin()
7877 * ./wp-admin/includes/plugin.php.
7878 *
7879 * @author Vova Feldman (@svovaf)
7880 * @since 1.1.9
7881 */
7882 $this->_storage->was_plugin_loaded = true;
7883 }
7884
7885 /**
7886 * @author Leo Fajardo (@leorw)
7887 * @since 2.3.0
7888 */
7889 private function maybe_activate_addon_license() {
7890 $parent_fs = $this->get_parent_instance();
7891
7892 if (
7893 ! is_object( $parent_fs ) ||
7894 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7895 ) {
7896 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7897 return;
7898 }
7899
7900 $license = $this->get_active_parent_license();
7901 if ( ! is_object( $license ) ) {
7902 return;
7903 }
7904
7905 if (
7906 $this->is_bundle_license_auto_activation_enabled() &&
7907 ! empty( $license->products )
7908 ) {
7909 $this->activate_bundle_license( $license );
7910
7911 return;
7912 }
7913
7914 if ( ! $this->is_registered() ) {
7915 // Opt in with a license key.
7916 $this->opt_in(
7917 $parent_fs->get_current_or_network_user()->email,
7918 false,
7919 false,
7920 $license->secret_key
7921 );
7922 } else {
7923 // Activate the license.
7924 $install = $this->api_site_call(
7925 '/',
7926 'put',
7927 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7928 );
7929
7930 if ( ! FS_Api::is_api_error( $install ) ) {
7931 $this->_sync_addon_license( $this->get_id(), true );
7932 }
7933 }
7934 }
7935
7936 /**
7937 * @author Leo Fajardo (@leorw)
7938 * @since 2.3.0
7939 *
7940 * @param FS_Plugin_License $license
7941 */
7942 private function maybe_network_activate_addon_license( $license = null ) {
7943 $parent_fs = $this->get_parent_instance();
7944 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7945 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7946 return;
7947 }
7948
7949 $license = ( ! is_null( $license ) ) ?
7950 $license :
7951 $this->get_active_parent_license();
7952
7953 if ( ! is_object( $license ) ) {
7954 return;
7955 }
7956
7957 if (
7958 $this->is_bundle_license_auto_activation_enabled() &&
7959 ! empty( $license->products )
7960 ) {
7961 $this->activate_bundle_license( $license );
7962
7963 return;
7964 }
7965
7966 if ( ! $this->is_network_registered() ) {
7967 $sites = $this->get_sites_for_network_level_optin();
7968
7969 if ( count( $sites ) > $license->left() ) {
7970 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
7971 return;
7972 }
7973
7974 // Opt in with a license key.
7975 $this->opt_in(
7976 $parent_fs->get_user()->email,
7977 false,
7978 false,
7979 $license->secret_key,
7980 false,
7981 false,
7982 false,
7983 null,
7984 $sites
7985 );
7986 } else {
7987 $blog_2_install_map = array();
7988 $site_ids = array();
7989
7990 $all_sites = Freemius::get_sites();
7991
7992 foreach ( $all_sites as $site ) {
7993 $blog_id = Freemius::get_site_blog_id( $site );
7994 $install = $this->get_install_by_blog_id( $blog_id );
7995
7996 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
7997 // Skip license activation for installs that are already associated with a license.
7998 continue;
7999 }
8000
8001 if ( is_object( $install ) ) {
8002 $blog_2_install_map[ $blog_id ] = $install;
8003 } else {
8004 $site_ids[] = $blog_id;
8005 }
8006 }
8007
8008 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
8009 return;
8010 }
8011
8012 $user = $this->get_current_or_network_user();
8013
8014 if ( ! empty( $blog_2_install_map ) ) {
8015 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
8016
8017 if ( true !== $result ) {
8018 return;
8019 }
8020 }
8021
8022 if ( ! empty( $site_ids ) ) {
8023 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
8024 }
8025 }
8026 }
8027
8028 /**
8029 * Tries to activate a bundle license for all supported products if the current product is activated with a bundle license. This is called after activating an available license (not via the license activation dialog but by clicking on a license activation button) for a product via its "Account" page.
8030 *
8031 * @author Leo Fajardo (@leorw)
8032 * @since 2.4.0
8033 *
8034 * @param FS_Plugin_License $license
8035 * @param array $sites
8036 * @param int $blog_id
8037 */
8038 private function maybe_activate_bundle_license( FS_Plugin_License $license = null, $sites = array(), $blog_id = 0 ) {
8039 if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
8040 $license = $this->_license;
8041 }
8042
8043 if ( ! is_object( $license ) ) {
8044 return;
8045 }
8046
8047 $parent_license = ( ! empty( $license->products ) ) ?
8048 $license :
8049 $this->get_active_parent_license( $license->secret_key );
8050
8051 if ( is_object( $parent_license ) ) {
8052 $this->activate_bundle_license( $parent_license, $sites, $blog_id );
8053 }
8054 }
8055
8056 /**
8057 * Try to activate a bundle license for all the bundle products installed on the site.
8058 * (1) If a child product install already has a license, the bundle license won't be activated.
8059 * (2) On multi-site networks, if the attempt to activate the bundle license is triggered from the network admin, the bundle license activation will only work for non-delegated sites and only if none of them is associated with a license. Even if one of the sites has the product installed with a license key, skip the bundle license activation for the product.
8060 * (3) On multi-site networks, if the attempt to activate the bundle license is triggered from a site-level admin, only activate the license if the product is site-level activated or delegated, and the product installation is not yet associated with a license.
8061 *
8062 * @author Leo Fajardo (@leorw)
8063 * @since 2.4.0
8064 *
8065 * @param FS_Plugin_License $license
8066 * @param array $sites
8067 * @param int $current_blog_id
8068 */
8069 private function activate_bundle_license( $license, $sites = array(), $current_blog_id = 0 ) {
8070 $is_network_admin = fs_is_network_admin();
8071
8072 $installs_by_blog_map = array();
8073 $site_info_by_blog_map = array();
8074
8075 /**
8076 * Try to activate the license for all supported products.
8077 *
8078 * @author Leo Fajardo
8079 */
8080 foreach ( $license->products as $product_id ) {
8081 $fs = self::get_instance_by_id( $product_id );
8082
8083 if ( ! is_object( $fs ) ) {
8084 continue;
8085 }
8086
8087 if ( ! $fs->has_paid_plan() ) {
8088 continue;
8089 }
8090
8091 if (
8092 ! $fs->is_addon() &&
8093 ! FS_Plan_Manager::instance()->has_paid_plan( $fs->_plans )
8094 ) {
8095 /**
8096 * The parent product can be free-only but can have its `has_paid_plan` flag set to `true` when
8097 * there is a context bundle.
8098 */
8099 continue;
8100 }
8101
8102 if ( $current_blog_id > 0 ) {
8103 $fs->switch_to_blog( $current_blog_id );
8104 }
8105
8106 if ( $fs->has_active_valid_license() ) {
8107 continue;
8108 }
8109
8110 if ( ! $is_network_admin || $current_blog_id > 0 ) {
8111 if ( $fs->is_network_active() && ! $fs->is_delegated_connection( $current_blog_id ) ) {
8112 // Do not try to activate the license in the site level if the product is network active and the connection was not delegated.
8113 continue;
8114 }
8115 } else {
8116 if ( ! $fs->is_network_active() ) {
8117 // Do not try to activate the license in the network level if the product is not network active.
8118 continue;
8119 }
8120
8121 if ( $fs->is_network_delegated_connection() ) {
8122 // Do not try to activate the license in the network level if the activation has been delegated to site admins.
8123 continue;
8124 }
8125
8126 $has_install_with_license = false;
8127
8128 // Collection of sites that have an install entity that is not activated with a license or non-delegated sites that have no install entity, or both types of site.
8129 $filtered_sites = array();
8130
8131 if ( empty( $sites ) ) {
8132 $all_sites = self::get_sites();
8133
8134 foreach ( $all_sites as $site ) {
8135 $sites[] = array( 'blog_id' => self::get_site_blog_id( $site ) );
8136 }
8137 } else {
8138 // Populate the map here to avoid calling `$fs->get_site_info( $site );` in the other `for` loop below.
8139 foreach ( $sites as $site ) {
8140 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8141 continue;
8142 }
8143
8144 $site_info_by_blog_map[ $site['blog_id'] ] = $site;
8145 }
8146 }
8147
8148 foreach ( $sites as $site ) {
8149 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8150 continue;
8151 }
8152
8153 $blog_id = $site['blog_id'];
8154
8155 if ( ! isset( $installs_by_blog_map[ $blog_id ] ) ) {
8156 $installs_by_blog_map[ $blog_id ] = self::get_all_sites( $fs->get_module_type(), $blog_id );
8157 }
8158
8159 $installs = $installs_by_blog_map[ $blog_id ];
8160 $install = null;
8161
8162 if ( isset( $installs[ $fs->get_slug() ] ) ) {
8163 $install = $installs[ $fs->get_slug() ];
8164
8165 if (
8166 is_object( $install ) &&
8167 (
8168 ! FS_Site::is_valid_id( $install->id ) ||
8169 ! FS_User::is_valid_id( $install->user_id ) ||
8170 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
8171 )
8172 ) {
8173 $install = null;
8174 }
8175 }
8176
8177 if (
8178 is_object( $install ) &&
8179 FS_Plugin_License::is_valid_id( $install->license_id )
8180 ) {
8181 $has_install_with_license = true;
8182 break;
8183 }
8184
8185 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
8186 // Site activation delegated, don't activate bundle license on the site in the network admin.
8187 continue;
8188 }
8189
8190 if ( ! isset( $site_info_by_blog_map[ $blog_id ] ) ) {
8191 $site_info_by_blog_map[ $blog_id ] = $fs->get_site_info( $site );
8192 }
8193
8194 $filtered_sites[] = $site_info_by_blog_map[ $blog_id ];
8195 }
8196
8197 if ( $has_install_with_license || empty( $filtered_sites ) ) {
8198 // Do not try to activate the license at the network level if there's any install with a license or there's no site to activate the license on.
8199 continue;
8200 }
8201
8202 $sites = $filtered_sites;
8203 }
8204
8205 $fs->activate_migrated_license(
8206 $license->secret_key,
8207 null,
8208 null,
8209 $sites,
8210 ( $current_blog_id > 0 ? $current_blog_id : null )
8211 );
8212 }
8213 }
8214
8215 /**
8216 * Returns a parent license that can be activated for the context product.
8217 *
8218 * @author Leo Fajardo (@leorw)
8219 * @since 2.3.0
8220 *
8221 * @param string|null $license_key
8222 * @param bool $flush
8223 *
8224 * @return FS_Plugin_License
8225 */
8226 function get_active_parent_license( $license_key = null, $flush = true ) {
8227 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
8228
8229 $fs = $this;
8230
8231 if ( $this->is_addon() ) {
8232 $parent_instance = $this->get_parent_instance();
8233
8234 if ( is_object( $parent_instance ) && $parent_instance->is_registered() ) {
8235 $fs = $parent_instance;
8236 }
8237 }
8238
8239 $foreign_licenses = $fs->get_foreign_licenses_info(
8240 self::get_all_licenses( $this->get_parent_id() )
8241 );
8242
8243 if ( ! empty ( $foreign_licenses ) ) {
8244 $foreign_licenses = array(
8245 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
8246 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
8247 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
8248 );
8249
8250 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
8251 }
8252
8253 $result = $fs->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, $flush );
8254
8255 if (
8256 ! $this->is_api_result_object( $result, 'licenses' ) ||
8257 ! is_array( $result->licenses ) ||
8258 empty( $result->licenses )
8259 ) {
8260 return null;
8261 }
8262
8263 $parent_license = null;
8264
8265 if ( empty( $license_key ) ) {
8266 $parent_license = $result->licenses[0];
8267 } else {
8268 foreach ( $result->licenses as $license ) {
8269 if ( $license_key === $license->secret_key ) {
8270 $parent_license = $license;
8271 break;
8272 }
8273 }
8274 }
8275
8276 if ( ! is_null( $parent_license ) ) {
8277 $parent_license = new FS_Plugin_License( $parent_license );
8278 }
8279
8280 return $parent_license;
8281 }
8282
8283 /**
8284 * @author Leo Fajardo (@leorw)
8285 * @since 2.3.0
8286 *
8287 * @return array
8288 */
8289 function get_sites_for_network_level_optin() {
8290 $sites = array();
8291 $all_sites = self::get_sites();
8292
8293 foreach ( $all_sites as $site ) {
8294 $blog_id = self::get_site_blog_id( $site );
8295
8296 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
8297 ! $this->is_installed_on_site( $blog_id )
8298 ) {
8299 $sites[] = $this->get_site_info( $site );
8300 }
8301 }
8302
8303 return $sites;
8304 }
8305
8306 /**
8307 * Delete account.
8308 *
8309 * @author Vova Feldman (@svovaf)
8310 * @since 1.0.3
8311 *
8312 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8313 */
8314 function delete_account_event( $check_user = true ) {
8315 $this->_logger->entrance( 'slug = ' . $this->_slug );
8316
8317 if ( $check_user && ! $this->is_user_admin() ) {
8318 return;
8319 }
8320
8321 $this->do_action( 'before_account_delete' );
8322
8323 // Clear all admin notices.
8324 $this->_admin_notices->clear_all_sticky( false );
8325
8326 $this->_delete_site( false );
8327
8328 $delete_network_common_data = true;
8329
8330 if ( $this->_is_network_active ) {
8331 $installs = $this->get_blog_install_map();
8332
8333 // Don't delete common network data unless no other installs left.
8334 $delete_network_common_data = empty( $installs );
8335 }
8336
8337 if ( $delete_network_common_data ) {
8338 $this->_delete_plans( false );
8339
8340 $this->_delete_licenses( false );
8341
8342 // Delete add-ons related to plugin's account.
8343 $this->_delete_account_addons( false );
8344 }
8345
8346 // @todo Delete plans and licenses of add-ons.
8347
8348 self::$_accounts->store();
8349
8350 /**
8351 * IMPORTANT:
8352 * Clear crons must be executed before clearing all storage.
8353 * Otherwise, the cron will not be cleared.
8354 */
8355 if ( $delete_network_common_data ) {
8356 $this->clear_sync_cron();
8357 }
8358
8359 $this->clear_install_sync_cron();
8360
8361 // Clear all storage data.
8362 $this->_storage->clear_all( true, array(
8363 'is_delegated_connection',
8364 'connectivity_test',
8365 'is_on',
8366 ), false );
8367
8368 // Send delete event.
8369 $this->get_api_site_scope()->call( '/', 'delete' );
8370
8371 $this->do_action( 'after_account_delete' );
8372 }
8373
8374 /**
8375 * Delete network level account.
8376 *
8377 * @author Vova Feldman (@svovaf)
8378 * @since 2.0.0
8379 *
8380 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8381 */
8382 function delete_network_account_event( $check_user = true ) {
8383 $this->_logger->entrance( 'slug = ' . $this->_slug );
8384
8385 if ( $check_user && ! $this->is_user_admin() ) {
8386 return;
8387 }
8388
8389 $this->do_action( 'before_network_account_delete' );
8390
8391 // Clear all admin notices.
8392 $this->_admin_notices->clear_all_sticky();
8393
8394 $this->_delete_plans( false, false );
8395
8396 $this->_delete_licenses( false );
8397
8398 // Delete add-ons related to plugin's account.
8399 $this->_delete_account_addons( false );
8400
8401 // @todo Delete plans and licenses of add-ons.
8402
8403 self::$_accounts->store( true );
8404
8405 /**
8406 * IMPORTANT:
8407 * Clear crons must be executed before clearing all storage.
8408 * Otherwise, the cron will not be cleared.
8409 */
8410 $this->clear_sync_cron( true );
8411 $this->clear_install_sync_cron( true );
8412
8413 $sites = self::get_sites();
8414
8415 $install_ids = array();
8416 foreach ( $sites as $site ) {
8417 $blog_id = self::get_site_blog_id( $site );
8418
8419 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8420 continue;
8421 }
8422
8423 $install_id = $this->_delete_site( true, $blog_id );
8424
8425 // Clear all storage data.
8426 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8427
8428 if ( FS_Site::is_valid_id( $install_id ) ) {
8429 $install_ids[] = $install_id;
8430 }
8431
8432 switch_to_blog( $blog_id );
8433
8434 $this->do_action( 'after_account_delete' );
8435
8436 restore_current_blog();
8437 }
8438
8439 $this->_storage->clear_all( true, array(
8440 'connectivity_test',
8441 'is_on',
8442 ), true );
8443
8444 // Send delete event.
8445 if ( ! empty( $install_ids ) ) {
8446 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8447 }
8448
8449 $this->do_action( 'after_network_account_delete' );
8450 }
8451
8452 /**
8453 * Plugin deactivation hook.
8454 *
8455 * @author Vova Feldman (@svovaf)
8456 * @since 1.0.1
8457 */
8458 function _deactivate_plugin_hook() {
8459 $this->_logger->entrance( 'slug = ' . $this->_slug );
8460
8461 if ( ! $this->is_user_admin() ) {
8462 return;
8463 }
8464
8465 $is_network_deactivation = fs_is_network_admin();
8466 $storage_keys_for_removal = array();
8467
8468 $this->_admin_notices->clear_all_sticky();
8469
8470 $storage_keys_for_removal[] = 'sticky_optin_added';
8471 if ( isset( $this->_storage->sticky_optin_added ) ) {
8472 unset( $this->_storage->sticky_optin_added );
8473 }
8474
8475 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8476 // Remember that plugin was already installed.
8477 $this->_storage->is_plugin_new_install = false;
8478 }
8479
8480 // Hook to plugin uninstall.
8481 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8482
8483 $this->clear_module_main_file_cache();
8484 $this->clear_sync_cron( $this->_is_network_active );
8485 $this->clear_install_sync_cron();
8486
8487 if ( $this->is_registered() ) {
8488 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8489 FS_Plugin_Updater::instance( $this )->delete_update_data();
8490 }
8491
8492 if ( $is_network_deactivation ) {
8493 // Send deactivation event.
8494 $this->sync_installs( array(
8495 'is_active' => false,
8496 ) );
8497 } else {
8498 // Send deactivation event.
8499 $this->sync_install( array(
8500 'is_active' => false,
8501 ) );
8502 }
8503 } else {
8504 if ( false === $this->has_api_connectivity() && ! $this->is_premium() ) {
8505 // Reset connectivity test cache.
8506 $this->clear_connectivity_info();
8507
8508 $storage_keys_for_removal[] = 'connectivity_test';
8509 }
8510 }
8511
8512 if ( $is_network_deactivation ) {
8513 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8514 unset( $this->_storage->sticky_optin_added_ms );
8515 }
8516
8517 if ( ! empty( $storage_keys_for_removal ) ) {
8518 $sites = self::get_sites();
8519
8520 foreach ( $sites as $site ) {
8521 $blog_id = self::get_site_blog_id( $site );
8522
8523 foreach ( $storage_keys_for_removal as $key ) {
8524 $this->_storage->remove( $key, false, $blog_id );
8525 }
8526
8527 $this->_storage->save( $blog_id );
8528 }
8529 }
8530 }
8531
8532 // Clear API cache on deactivation.
8533 FS_Api::clear_cache();
8534
8535 $this->remove_sdk_reference();
8536 }
8537
8538 /**
8539 * @author Vova Feldman (@svovaf)
8540 * @since 1.1.6
8541 */
8542 private function remove_sdk_reference() {
8543 global $fs_active_plugins;
8544
8545 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8546 if ( $this->_plugin_basename == $data->plugin_path ) {
8547 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8548 break;
8549 }
8550 }
8551
8552 fs_fallback_to_newest_active_sdk();
8553 }
8554
8555 /**
8556 * @author Vova Feldman (@svovaf)
8557 * @since 1.1.3
8558 *
8559 * @param bool $is_anonymous
8560 * @param bool|int $network_or_blog_id Since 2.0.0
8561 */
8562 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8563 // Store information regarding skip to try and opt-in the user
8564 // again in the future.
8565 $skip_info = array(
8566 'is' => $is_anonymous,
8567 'timestamp' => WP_FS__SCRIPT_START_TIME,
8568 'version' => $this->get_plugin_version(),
8569 );
8570
8571 if ( true === $network_or_blog_id ) {
8572 $this->_storage->is_anonymous_ms = $skip_info;
8573 } else {
8574 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8575 }
8576
8577 $this->network_upgrade_mode_completed();
8578
8579 // Update anonymous mode cache.
8580 $this->_is_anonymous = $is_anonymous;
8581 }
8582
8583 /**
8584 * @author Vova Feldman (@svovaf)
8585 * @since 2.5.1
8586 *
8587 * @param bool|int $network_or_blog_id
8588 */
8589 private function unset_anonymous_mode( $network_or_blog_id = 0 ) {
8590 if ( true === $network_or_blog_id ) {
8591 unset( $this->_storage->is_anonymous_ms );
8592 } else {
8593 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8594 }
8595 }
8596
8597 /**
8598 * @author Vova Feldman (@svovaf)
8599 * @since 2.0.0
8600 *
8601 * @param int $blog_id Site ID.
8602 * @param int $user_id User ID.
8603 * @param string $domain Site domain.
8604 * @param string $path Site path.
8605 * @param int $network_id Network ID. Only relevant on multi-network installations.
8606 * @param array $meta Metadata. Used to set initial site options.
8607 *
8608 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8609 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8610 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8611 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8612 */
8613 public function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8614 $this->_logger->entrance();
8615
8616 if ( ! $this->_is_network_active ) {
8617 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
8618 return;
8619 }
8620
8621 $site = null;
8622 $new_blog_id = $blog_id;
8623
8624 if ( $this->is_premium() &&
8625 $this->is_network_connected() &&
8626 is_object( $this->_license ) &&
8627 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8628 $this->is_license_network_active( $blog_id )
8629 ) {
8630 /**
8631 * Running the premium version, the license was network activated, and the license can also be activated on the current site -> so try to opt-in with the license key.
8632 */
8633 $current_blog_id = get_current_blog_id();
8634 $license = clone $this->_license;
8635
8636 $this->switch_to_blog( $blog_id );
8637
8638 // Opt-in with network user.
8639 $this->install_with_user(
8640 $this->get_network_user(),
8641 $license->secret_key,
8642 false,
8643 false,
8644 false
8645 );
8646
8647 if ( is_object( $this->_site ) ) {
8648 if ( $this->_site->license_id == $license->id ) {
8649 /**
8650 * If the license was activated successfully, sync the license data from the remote server.
8651 */
8652 $this->_license = $license;
8653 $this->sync_site_license();
8654 }
8655 }
8656
8657 $site = $this->_site;
8658
8659 $this->switch_to_blog( $current_blog_id );
8660
8661 if ( is_object( $site ) ) {
8662 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id, $site );
8663
8664 // Already connected (with or without a license), so no need to continue.
8665 return;
8666 }
8667 }
8668
8669 if ( $this->is_network_anonymous() ) {
8670 /**
8671 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8672 */
8673 $this->skip_site_connection( $blog_id );
8674 } else if ( $this->is_network_delegated_connection() ) {
8675 /**
8676 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8677 */
8678 $this->delegate_site_connection( $blog_id );
8679 } else if ( $this->is_network_connected() ) {
8680 /**
8681 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8682 */
8683 $current_blog_id = get_current_blog_id();
8684
8685 $this->switch_to_blog( $blog_id );
8686
8687 // Opt-in with network user.
8688 $this->install_with_user(
8689 $this->get_network_user(),
8690 false,
8691 false,
8692 false,
8693 false
8694 );
8695
8696 $site = $this->_site;
8697
8698 $this->switch_to_blog( $current_blog_id );
8699 } else {
8700 /**
8701 * If the super-admin mixed different options (connect, skip, delegated):
8702 * a) If at least one site connection was delegated, then automatically delegate connection.
8703 * b) Otherwise, it means that at least one site was skipped and at least one site was connected. For a simplified UX in the initial release of the multisite network integration, skip the connection for the newly created site. If the super-admin will want to opt-in they can still do that from the network level Account page.
8704 */
8705 $has_delegated_site = false;
8706
8707 $sites = self::get_sites();
8708 foreach ( $sites as $wp_site ) {
8709 $blog_id = self::get_site_blog_id( $wp_site );
8710
8711 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8712 $has_delegated_site = true;
8713 break;
8714 }
8715 }
8716
8717 if ( $has_delegated_site ) {
8718 $this->delegate_site_connection( $blog_id );
8719 } else {
8720 $this->skip_site_connection( $blog_id );
8721 }
8722 }
8723
8724 /**
8725 * Store the new blog's information even if there's no install so that when a clone install is stored in the new blog's storage, we can try to resolve it automatically.
8726 *
8727 * @author Leo Fajardo (@leorw)
8728 * @since 2.5.0
8729 */
8730 FS_Clone_Manager::instance()->store_blog_install_info( $new_blog_id, $site );
8731 }
8732
8733 /**
8734 * @author Vova Feldman (@svovaf)
8735 * @since 2.5.0
8736 *
8737 * @param \WP_Site $new_site
8738 * @param array $args
8739 */
8740 public function _after_wp_initialize_site_callback( WP_Site $new_site, $args ) {
8741 $this->_logger->entrance();
8742
8743 $this->_after_new_blog_callback(
8744 $new_site->id,
8745 // Dummy user ID (not in use).
8746 0,
8747 $new_site->domain,
8748 $new_site->path,
8749 $new_site->network_id,
8750 // Dummy meta, not in use.
8751 array()
8752 );
8753 }
8754
8755 /**
8756 * @author Vova Feldman (@svovaf)
8757 * @since 1.1.3
8758 *
8759 * @param bool|int|int[] $network_or_blog_ids Since 2.0.0.
8760 */
8761 private function reset_anonymous_mode( $network_or_blog_ids = false ) {
8762 if ( true === $network_or_blog_ids ) {
8763 $this->unset_anonymous_mode( true );
8764
8765 if ( fs_is_network_admin() ) {
8766 $this->_is_anonymous = null;
8767 }
8768
8769 // Rest anonymous mode for all non-delegated sub-sites.
8770 $blog_ids = $this->get_non_delegated_blog_ids();
8771 }
8772 else
8773 {
8774 if ( false === $network_or_blog_ids ) {
8775 $network_or_blog_ids = 0;
8776 }
8777
8778 $blog_ids = is_array( $network_or_blog_ids ) ?
8779 $network_or_blog_ids :
8780 array( $network_or_blog_ids );
8781
8782 foreach ( $blog_ids as $blog_id ) {
8783 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8784 $this->_is_anonymous = null;
8785 }
8786 }
8787 }
8788
8789 foreach ( $blog_ids as $blog_id ) {
8790 $this->unset_anonymous_mode( $blog_id );
8791 }
8792
8793 /**
8794 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8795 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8796 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8797 *
8798 * @author Leo Fajardo (@leorw)
8799 * @since 1.2.2
8800 */
8801 if ( ! $this->_is_network_active ) {
8802 $this->_is_anonymous = null;
8803 }
8804 }
8805
8806 /**
8807 * @author Leo Fajardo (@leorw)
8808 * @since 2.5.3
8809 */
8810 private function update_license_required_permissions_if_anonymous() {
8811 if ( ! $this->is_anonymous() ) {
8812 return;
8813 }
8814
8815 $this->reset_anonymous_mode( fs_is_network_admin() );
8816
8817 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
8818 'essentials' => true,
8819 'events' => true,
8820 'diagnostic' => false,
8821 'extensions' => false,
8822 'site' => false,
8823 ) );
8824 }
8825
8826 /**
8827 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8828 * deleting the account in the network level, the URL of the page to redirect to is correct.
8829 *
8830 * @author Leo Fajardo (@leorw)
8831 *
8832 * @since 2.1.3
8833 */
8834 private function maybe_set_slug_and_network_menu_exists_flag() {
8835 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8836 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8837 $this->_menu->get_slug() :
8838 $this->_slug
8839 );
8840 }
8841 }
8842
8843 /**
8844 * Clears the anonymous mode and redirects to the opt-in screen.
8845 *
8846 * @author Vova Feldman (@svovaf)
8847 * @since 1.1.7
8848 */
8849 function connect_again() {
8850 if ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) {
8851 return;
8852 }
8853
8854 if ( $this->is_anonymous() ) {
8855 $this->reset_anonymous_mode( fs_is_network_admin() );
8856 }
8857
8858 $activation_url_params = array();
8859
8860 if ( $this->is_pending_activation() ) {
8861 $this->clear_pending_activation_mode();
8862
8863 if ( fs_request_get_bool( 'require_license' ) ) {
8864 $activation_url_params['require_license'] = true;
8865 }
8866 }
8867
8868 $this->maybe_set_slug_and_network_menu_exists_flag();
8869
8870 fs_redirect( $this->get_activation_url( $activation_url_params ) );
8871 }
8872
8873 /**
8874 * Skip account connect, and set anonymous mode.
8875 *
8876 * @author Vova Feldman (@svovaf)
8877 * @since 1.1.1
8878 *
8879 * @param bool|int|int[] $network_or_blog_ids Since 2.5.1
8880 */
8881 function skip_connection( $network_or_blog_ids = false ) {
8882 $this->_logger->entrance();
8883
8884 $this->_admin_notices->remove_sticky( 'connect_account' );
8885
8886 if ( true === $network_or_blog_ids ) {
8887 $this->set_anonymous_mode( true, true );
8888
8889 if ( fs_is_network_admin() ) {
8890 $this->_is_anonymous = null;
8891 }
8892
8893 // Rest anonymous mode for all non-delegated sub-sites.
8894 $blog_ids = $this->get_non_delegated_blog_ids();
8895 }
8896 else
8897 {
8898 if ( false === $network_or_blog_ids ) {
8899 $network_or_blog_ids = 0;
8900 }
8901
8902 $blog_ids = is_array( $network_or_blog_ids ) ?
8903 $network_or_blog_ids :
8904 array( $network_or_blog_ids );
8905
8906 foreach ( $blog_ids as $blog_id ) {
8907 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8908 $this->_is_anonymous = null;
8909 }
8910 }
8911 }
8912
8913 foreach ( $blog_ids as $blog_id ) {
8914 $this->skip_site_connection( $blog_id );
8915 }
8916
8917 $this->network_upgrade_mode_completed();
8918 }
8919
8920 /**
8921 * Skip connection for specific site in the network.
8922 *
8923 * @author Vova Feldman (@svovaf)
8924 * @since 2.0.0
8925 *
8926 * @param int|null $blog_id
8927 * @param bool $send_skip
8928 */
8929 private function skip_site_connection( $blog_id = null ) {
8930 $this->_logger->entrance();
8931
8932 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8933
8934 $this->set_anonymous_mode( true, $blog_id );
8935 }
8936
8937 /**
8938 * Plugin version update hook.
8939 *
8940 * @author Vova Feldman (@svovaf)
8941 * @since 1.0.4
8942 */
8943 private function update_plugin_version_event() {
8944 $this->_logger->entrance();
8945
8946 if ( ! $this->is_registered() ) {
8947 return;
8948 }
8949
8950 $this->schedule_install_sync();
8951 // $this->sync_install( array(), true );
8952 }
8953
8954 /**
8955 * Generate an MD5 signature of a plugins collection.
8956 * This helper methods used to identify changes in a plugins collection.
8957 *
8958 * @author Vova Feldman (@svovaf)
8959 * @since 2.0.0
8960 *
8961 * @param array [string]array $plugins
8962 *
8963 * @return string
8964 */
8965 private function get_plugins_thumbprint( $plugins ) {
8966 ksort( $plugins );
8967
8968 $thumbprint = '';
8969 foreach ( $plugins as $basename => $data ) {
8970 $thumbprint .= $data['slug'] . ',' .
8971 $data['Version'] . ',' .
8972 ( $data['is_active'] ? '1' : '0' ) . ';';
8973 }
8974
8975 return md5( $thumbprint );
8976 }
8977
8978 /**
8979 * Return a list of modified plugins since the last sync.
8980 *
8981 * Note:
8982 * There's no point to store a plugins counter since even if the number of
8983 * plugins didn't change, we still need to check if the versions are all the
8984 * same and the activity state is similar.
8985 *
8986 * @author Vova Feldman (@svovaf)
8987 * @since 1.1.8
8988 *
8989 * @return array|false
8990 */
8991 private function get_plugins_data_for_api() {
8992 // Alias.
8993 $site_active_plugins_option_name = 'active_plugins';
8994 $network_plugins_option_name = 'all_plugins';
8995
8996 /**
8997 * Collection of all site level active plugins.
8998 */
8999 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
9000
9001 if ( ! is_object( $site_active_plugins_cache ) ) {
9002 $site_active_plugins_cache = (object) array(
9003 'timestamp' => '',
9004 'md5' => '',
9005 'plugins' => array(),
9006 );
9007 }
9008
9009 $time = time();
9010
9011 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
9012 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
9013 ) {
9014 // Don't send plugin updates if last update was in the past 5 min.
9015 return false;
9016 }
9017
9018 // Write timestamp to lock the logic.
9019 $site_active_plugins_cache->timestamp = $time;
9020 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
9021
9022 // Reload options from DB.
9023 self::$_accounts->load( true );
9024 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
9025
9026 if ( $time != $site_active_plugins_cache->timestamp ) {
9027 // If timestamp is different, then another thread captured the lock.
9028 return false;
9029 }
9030
9031 /**
9032 * Collection of all plugins (network level).
9033 */
9034 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
9035
9036 if ( ! is_object( $network_plugins_cache ) ) {
9037 $network_plugins_cache = (object) array(
9038 'timestamp' => '',
9039 'md5' => '',
9040 'plugins' => array(),
9041 );
9042 }
9043
9044 // Check if there's a change in plugins.
9045 $network_plugins = self::get_network_plugins();
9046 $site_active_plugins = self::get_site_active_plugins();
9047
9048 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
9049 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
9050
9051 // Check if plugins status changed (version or active/inactive).
9052 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
9053 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
9054
9055 if ( ! $network_plugins_changed &&
9056 ! $site_active_plugins_changed
9057 ) {
9058 // No changes.
9059 return array();
9060 }
9061
9062 $plugins_update_data = array();
9063
9064 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
9065 if ( ! isset( $network_plugins[ $basename ] ) ) {
9066 // Plugin uninstalled.
9067 $uninstalled_plugin_data = $data;
9068 $uninstalled_plugin_data['is_active'] = false;
9069 $uninstalled_plugin_data['is_uninstalled'] = true;
9070 $plugins_update_data[] = $uninstalled_plugin_data;
9071
9072 unset( $network_plugins[ $basename ] );
9073
9074 unset( $network_plugins_cache->plugins[ $basename ] );
9075 unset( $site_active_plugins_cache->plugins[ $basename ] );
9076
9077 continue;
9078 }
9079
9080 $was_active = $data['is_active'] ||
9081 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9082 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
9083 $is_active = $network_plugins[ $basename ]['is_active'] ||
9084 ( isset( $site_active_plugins[ $basename ] ) &&
9085 $site_active_plugins[ $basename ]['is_active'] );
9086
9087 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9088 isset( $site_active_plugins[ $basename ] )
9089 ) {
9090 // Plugin was site level activated.
9091 $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
9092 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
9093 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9094 ! isset( $site_active_plugins[ $basename ] )
9095 ) {
9096 // Plugin was site level deactivated.
9097 unset( $site_active_plugins_cache->plugins[ $basename ] );
9098 }
9099
9100 $prev_version = $data['version'];
9101 $current_version = $network_plugins[ $basename ]['Version'];
9102
9103 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
9104 // Plugin activated or deactivated, or version changed.
9105
9106 if ( $was_active !== $is_active ) {
9107 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
9108 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
9109 }
9110 }
9111
9112 if ( $prev_version !== $current_version ) {
9113 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
9114 }
9115
9116 $updated_plugin_data = $data;
9117 $updated_plugin_data['is_active'] = $is_active;
9118 $updated_plugin_data['version'] = $current_version;
9119 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
9120 $plugins_update_data[] = $updated_plugin_data;
9121 }
9122 }
9123
9124 // Find new plugins that weren't yet seen before.
9125 foreach ( $network_plugins as $basename => $data ) {
9126 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
9127 // New plugin.
9128 $new_plugin = array(
9129 'slug' => $data['slug'],
9130 'version' => $data['Version'],
9131 'title' => $data['Name'],
9132 'is_active' => $data['is_active'],
9133 'is_uninstalled' => false,
9134 );
9135
9136 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
9137
9138 $is_site_level_active = (
9139 isset( $site_active_plugins[ $basename ] ) &&
9140 $site_active_plugins[ $basename ]['is_active']
9141 );
9142
9143 /**
9144 * If not network active, set the activity status based on the site-level plugin status.
9145 */
9146 if ( ! $new_plugin['is_active'] ) {
9147 $new_plugin['is_active'] = $is_site_level_active;
9148 }
9149
9150 $plugins_update_data[] = $new_plugin;
9151
9152 if ( isset( $site_active_plugins[ $basename ] ) ) {
9153 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
9154 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
9155 }
9156 }
9157 }
9158
9159 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
9160 $site_active_plugins_cache->timestamp = $time;
9161 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
9162
9163 $network_plugins_cache->md5 = $network_plugins_thumbprint;
9164 $network_plugins_cache->timestamp = $time;
9165 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
9166
9167 return $plugins_update_data;
9168 }
9169
9170 /**
9171 * Return a list of modified themes since the last sync.
9172 *
9173 * Note:
9174 * There's no point to store a themes counter since even if the number of
9175 * themes didn't change, we still need to check if the versions are all the
9176 * same and the activity state is similar.
9177 *
9178 * @author Vova Feldman (@svovaf)
9179 * @since 1.1.8
9180 *
9181 * @return array|false
9182 */
9183 private function get_themes_data_for_api() {
9184 // Alias.
9185 $option_name = 'all_themes';
9186
9187 $all_cached_themes = self::$_accounts->get_option( $option_name );
9188
9189 if ( ! is_object( $all_cached_themes ) ) {
9190 $all_cached_themes = (object) array(
9191 'timestamp' => '',
9192 'md5' => '',
9193 'themes' => array(),
9194 );
9195 }
9196
9197 $time = time();
9198
9199 if ( ! empty( $all_cached_themes->timestamp ) &&
9200 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
9201 ) {
9202 // Don't send theme updates if last update was in the past 5 min.
9203 return false;
9204 }
9205
9206 // Write timestamp to lock the logic.
9207 $all_cached_themes->timestamp = $time;
9208 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9209
9210 // Reload options from DB.
9211 self::$_accounts->load( true );
9212 $all_cached_themes = self::$_accounts->get_option( $option_name );
9213
9214 if ( $time != $all_cached_themes->timestamp ) {
9215 // If timestamp is different, then another thread captured the lock.
9216 return false;
9217 }
9218
9219 // Get active theme.
9220 $active_theme = wp_get_theme();
9221 $active_theme_stylesheet = $active_theme->get_stylesheet();
9222
9223 // Check if there's a change in themes.
9224 $all_themes = wp_get_themes();
9225
9226 // Check if themes changed.
9227 ksort( $all_themes );
9228
9229 $themes_signature = '';
9230 foreach ( $all_themes as $slug => $data ) {
9231 $is_active = ( $slug === $active_theme_stylesheet );
9232 $themes_signature .= $slug . ',' .
9233 $data->version . ',' .
9234 ( $is_active ? '1' : '0' ) . ';';
9235 }
9236
9237 // Check if themes status changed (version or active/inactive).
9238 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
9239
9240 $themes_update_data = array();
9241
9242 if ( $themes_changed ) {
9243 // Change in themes, report changes.
9244
9245 // Update existing themes info.
9246 foreach ( $all_cached_themes->themes as $slug => $data ) {
9247 $is_active = ( $slug === $active_theme_stylesheet );
9248
9249 if ( ! isset( $all_themes[ $slug ] ) ) {
9250 // Plugin uninstalled.
9251 $uninstalled_theme_data = $data;
9252 $uninstalled_theme_data['is_active'] = false;
9253 $uninstalled_theme_data['is_uninstalled'] = true;
9254 $themes_update_data[] = $uninstalled_theme_data;
9255
9256 unset( $all_themes[ $slug ] );
9257 unset( $all_cached_themes->themes[ $slug ] );
9258 } else if ( $data['is_active'] !== $is_active ||
9259 $data['version'] !== $all_themes[ $slug ]->version
9260 ) {
9261 // Plugin activated or deactivated, or version changed.
9262
9263 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
9264 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
9265
9266 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
9267 }
9268 }
9269
9270 // Find new themes that weren't yet seen before.
9271 foreach ( $all_themes as $slug => $data ) {
9272 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
9273 $is_active = ( $slug === $active_theme_stylesheet );
9274
9275 // New plugin.
9276 $new_plugin = array(
9277 'slug' => $slug,
9278 'version' => $data->version,
9279 'title' => $data->name,
9280 'is_active' => $is_active,
9281 'is_uninstalled' => false,
9282 );
9283
9284 $themes_update_data[] = $new_plugin;
9285 $all_cached_themes->themes[ $slug ] = $new_plugin;
9286 }
9287 }
9288
9289 $all_cached_themes->md5 = md5( $themes_signature );
9290 $all_cached_themes->timestamp = time();
9291 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9292 }
9293
9294 return $themes_update_data;
9295 }
9296
9297 /**
9298 * Get site data for API install request.
9299 *
9300 * @author Vova Feldman (@svovaf)
9301 * @since 1.1.2
9302 *
9303 * @param string[] $override
9304 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9305 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9306 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, title, and URL).
9307 *
9308 * @return array
9309 */
9310 private function get_install_data_for_api(
9311 array $override,
9312 $include_plugins = true,
9313 $include_themes = true,
9314 $include_blog_data = true
9315 ) {
9316 // Alias.
9317 $permissions = FS_Permission_Manager::instance( $this );
9318
9319 if ( $permissions->is_extensions_tracking_allowed() ) {
9320 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
9321 /**
9322 * @since 1.1.8 Also send plugin updates.
9323 */
9324 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9325 $plugins = $this->get_plugins_data_for_api();
9326 if ( ! empty( $plugins ) ) {
9327 $override['plugins'] = $plugins;
9328 }
9329 }
9330 }
9331
9332 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
9333 /**
9334 * @since 1.1.8 Also send themes updates.
9335 */
9336 if ( $include_themes && ! isset( $override['themes'] ) ) {
9337 $themes = $this->get_themes_data_for_api();
9338 if ( ! empty( $themes ) ) {
9339 $override['themes'] = $themes;
9340 }
9341 }
9342 }
9343 }
9344
9345 $versions = $this->get_versions();
9346
9347 $blog_data = array();
9348 if ( $include_blog_data ) {
9349 $blog_data['url'] = self::get_unfiltered_site_url();
9350
9351 if ( $permissions->is_diagnostic_tracking_allowed() ) {
9352 $blog_data = array_merge( $blog_data, array(
9353 'language' => self::get_sanitized_language(),
9354 'title' => get_bloginfo( 'name' ),
9355 ) );
9356 }
9357 }
9358
9359 return array_merge( $versions, $blog_data, array(
9360 'version' => $this->get_plugin_version(),
9361 'is_premium' => $this->is_premium(),
9362 // Special params.
9363 'is_active' => true,
9364 'is_uninstalled' => false,
9365 ), $override );
9366 }
9367
9368 /**
9369 * Update installs details.
9370 *
9371 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
9372 *
9373 * @author Vova Feldman (@svovaf)
9374 * @since 2.0.0
9375 *
9376 * @param string[] string $override
9377 * @param bool $only_diff
9378 * @param bool $is_keepalive
9379 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9380 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9381 *
9382 * @return array
9383 */
9384 private function get_installs_data_for_api(
9385 array $override,
9386 $only_diff = false,
9387 $is_keepalive = false,
9388 $include_plugins = true,
9389 $include_themes = true
9390 ) {
9391 /**
9392 * @since 1.1.8 Also send plugin updates.
9393 */
9394 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9395 // $plugins = $this->get_plugins_data_for_api();
9396 // if ( ! empty( $plugins ) ) {
9397 // $override['plugins'] = $plugins;
9398 // }
9399 // }
9400 /**
9401 * @since 1.1.8 Also send themes updates.
9402 */
9403 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9404 // $themes = $this->get_themes_data_for_api();
9405 // if ( ! empty( $themes ) ) {
9406 // $override['themes'] = $themes;
9407 // }
9408 // }
9409
9410 // Common properties.
9411 $versions = $this->get_versions();
9412 $common = array_merge( $versions, array(
9413 'version' => $this->get_plugin_version(),
9414 'is_premium' => $this->is_premium(),
9415 ), $override );
9416
9417
9418 $is_common_diff_for_any_site = false;
9419 $common_diff_union = array();
9420
9421 $installs_data = array();
9422
9423 $sites = self::get_sites();
9424
9425 $subsite_data_for_api_by_install_id = array();
9426 $install_url_by_install_id = array();
9427 $subsite_registration_date_by_install_id = array();
9428
9429 foreach ( $sites as $site ) {
9430 $blog_id = self::get_site_blog_id( $site );
9431
9432 $install = $this->get_install_by_blog_id( $blog_id );
9433
9434 if ( is_object( $install ) ) {
9435 if ( $install->user_id != $this->_user->id ) {
9436 // Install belongs to a different owner.
9437 continue;
9438 }
9439
9440 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
9441 // Don't send updates regarding opted-out installs.
9442 continue;
9443 }
9444
9445 $install_data = $this->get_site_info( $site, true );
9446
9447 if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $install_data['blog_id'] ) ) {
9448 continue;
9449 }
9450
9451 $uid = $install_data['uid'];
9452 $url = $install_data['url'];
9453 $registration_date = $install_data['registration_date'];
9454
9455 if ( isset( $subsite_data_for_api_by_install_id[ $install->id ] ) ) {
9456 $clone_subsite_data = $subsite_data_for_api_by_install_id[ $install->id ];
9457 $clone_install_url = $install_url_by_install_id[ $install->id ];
9458 $clone_subsite_registration_date = $subsite_registration_date_by_install_id[ $install->id ];
9459
9460 $skip = false;
9461
9462 if (
9463 ! empty( $install_data['registration_date'] ) &&
9464 ! empty( $clone_subsite_registration_date )
9465 ) {
9466 /**
9467 * If the current subsite was created after the other subsite that is also linked to the same install ID, we assume that it's a clone (not the original), and therefore, would skip its processing.
9468 *
9469 * @author Leo Fajardo (@leorw)
9470 * @since 2.5.1
9471 */
9472 $skip = ( strtotime( $install_data['registration_date'] ) > strtotime( $clone_subsite_registration_date ) );
9473 } else if (
9474 /**
9475 * If we already have an install with the same URL as the subsite it's stored in, skip the current subsite. Otherwise, replace the existing install's data with the current subsite's install's data if the URLs match.
9476 *
9477 * @author Leo Fajardo (@leorw)
9478 * @since 2.5.0
9479 */
9480 fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) ||
9481 fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $url ) )
9482 ) {
9483 $skip = true;
9484 }
9485
9486 if ( $skip ) {
9487 // Store the skipped subsite's ID so that the clone resolution manager can try to resolve the clone install that is stored in that subsite later on.
9488 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
9489 continue;
9490 }
9491 }
9492
9493 unset( $install_data['blog_id'] );
9494 unset( $install_data['uid'] );
9495 unset( $install_data['url'] );
9496 unset( $install_data['registration_date'] );
9497
9498 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9499 $install_data['is_uninstalled'] = $install->is_uninstalled;
9500
9501 $common_diff = null;
9502 $is_common_diff = false;
9503 if ( $only_diff ) {
9504 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9505 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9506
9507 $is_common_diff = ! empty( $common_diff );
9508
9509 if ( $is_common_diff ) {
9510 foreach ( $common_diff as $k => $v ) {
9511 if ( ! isset( $common_diff_union[ $k ] ) ) {
9512 $common_diff_union[ $k ] = $v;
9513 }
9514 }
9515 }
9516
9517 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9518 }
9519
9520 if ( ! empty( $install_data ) || $is_common_diff || $is_keepalive ) {
9521 // Add install ID and site unique ID.
9522 $install_data['id'] = $install->id;
9523 $install_data['uid'] = $uid;
9524 $install_data['url'] = $url;
9525
9526 $subsite_data_for_api_by_install_id[ $install->id ] = $install_data;
9527 $install_url_by_install_id[ $install->id ] = $install->url;
9528 $subsite_registration_date_by_install_id[ $install->id ] = $registration_date;
9529 }
9530 }
9531 }
9532
9533 restore_current_blog();
9534
9535 $installs_data = array_merge(
9536 $installs_data,
9537 array_values( $subsite_data_for_api_by_install_id )
9538 );
9539
9540 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9541 if ( ! $only_diff ) {
9542 $installs_data[] = $common;
9543 } else if ( ! empty( $common_diff_union ) ) {
9544 $installs_data[] = $common_diff_union;
9545 }
9546 }
9547
9548 foreach ( $installs_data as &$data ) {
9549 $data = (object) $data;
9550 }
9551
9552 return $installs_data;
9553 }
9554
9555 /**
9556 * Compare site actual data to the stored install data and return the differences for an API data sync.
9557 *
9558 * @author Vova Feldman (@svovaf)
9559 * @since 2.0.0
9560 *
9561 * @param array $site
9562 * @param FS_Site $install
9563 * @param string[] string $override
9564 *
9565 * @return array
9566 */
9567 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9568 $diff = array();
9569 $special = array();
9570 $special_override = false;
9571
9572 foreach ( $site as $p => $v ) {
9573 if ( property_exists( $install, $p ) ) {
9574 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9575 $install->{$p} != $v
9576 ) {
9577 $val = self::get_api_sanitized_property( $p, $v );
9578
9579 if ( $install->{$p} != $val ) {
9580 $install->{$p} = $val;
9581 $diff[ $p ] = $val;
9582 }
9583 }
9584 } else {
9585 $special[ $p ] = $v;
9586
9587 if ( isset( $override[ $p ] ) ||
9588 'plugins' === $p ||
9589 'themes' === $p
9590 ) {
9591 $special_override = true;
9592 }
9593 }
9594 }
9595
9596 if ( $special_override || 0 < count( $diff ) ) {
9597 // Add special params only if has at least one
9598 // standard param, or if explicitly requested to
9599 // override a special param or a param which is not exist
9600 // in the install object.
9601 $diff = array_merge( $diff, $special );
9602 }
9603
9604 return $diff;
9605 }
9606
9607 /**
9608 * @author Leo Fajardo (@leorw)
9609 * @since 2.5.1
9610 */
9611 private function send_pending_clone_update_once() {
9612 $this->_logger->entrance();
9613
9614 if ( ! empty( $this->_storage->clone_id ) ) {
9615 return;
9616 }
9617
9618 $install_clone = $this->get_api_site_scope()->call(
9619 '/clones',
9620 'post',
9621 array( 'site_url' => self::get_unfiltered_site_url() )
9622 );
9623
9624 if ( $this->is_api_result_entity( $install_clone ) ) {
9625 $this->_storage->clone_id = $install_clone->id;
9626 }
9627 }
9628
9629 /**
9630 * @author Leo Fajardo (@leorw)
9631 * @since 2.5.1
9632 *
9633 * @param string $resolution_type
9634 * @param FS_Site $clone_context_install
9635 */
9636 function send_clone_resolution_update( $resolution_type, $clone_context_install ) {
9637 $this->_logger->entrance();
9638
9639 if ( empty( $this->_storage->clone_id ) ) {
9640 return;
9641 }
9642
9643 $new_install_id = null;
9644 $current_site = null;
9645
9646 $flush = false;
9647
9648 /**
9649 * If the current site is now different from the context install before the clone resolution, we need to override `$this->_site` so that the API call below will be made with the right install scope entity.
9650 */
9651 if ( $clone_context_install->id != $this->_site->id ) {
9652 $new_install_id = $this->_site->id;
9653 $current_site = $this->_site;
9654 $this->_site = $clone_context_install;
9655
9656 $flush = true;
9657 }
9658
9659 $this->get_api_site_scope( $flush )->call(
9660 "/clones/{$this->_storage->clone_id}",
9661 'put',
9662 array(
9663 'resolution' => $resolution_type,
9664 'new_install_id' => $new_install_id,
9665 )
9666 );
9667
9668 if ( is_object( $current_site ) ) {
9669 /**
9670 * Ensure that the install scope entity is updated back to the previous install entity.
9671 */
9672 $this->_site = $current_site;
9673
9674 // Restore the previous install scope entity of the API.
9675 $this->get_api_site_scope( true );
9676 }
9677 }
9678
9679 /**
9680 * Update install only if changed.
9681 *
9682 * @author Vova Feldman (@svovaf)
9683 * @since 1.0.9
9684 *
9685 * @param string[] string $override
9686 * @param bool $flush
9687 * @param bool $is_two_way_sync @since 2.5.0 If true and there's a successful API request, the install sync cron will be cleared.
9688 *
9689 * @return false|object|string
9690 */
9691 private function send_install_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9692 $this->_logger->entrance();
9693
9694 $check_properties = $this->get_install_data_for_api( $override );
9695
9696 if ( $flush ) {
9697 $params = $check_properties;
9698 } else {
9699 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9700 }
9701
9702 if ( empty( $params ) ) {
9703 $keepalive_only_update = $this->should_send_keepalive_update();
9704
9705 if ( ! $keepalive_only_update ) {
9706 /**
9707 * There are no updates to send including keepalive.
9708 *
9709 * @author Leo Fajardo (@leorw)
9710 * @since 2.2.3
9711 */
9712 return false;
9713 }
9714 }
9715
9716 if ( $is_two_way_sync ) {
9717 /**
9718 * Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9719 *
9720 * @author Leo Fajardo (@leorw)
9721 * @since 2.2.3
9722 */
9723 if ( ! is_multisite() ) {
9724 // Update last install sync timestamp.
9725 $this->set_cron_execution_timestamp( 'install_sync' );
9726 }
9727
9728 $params['uid'] = $this->get_anonymous_id();
9729 }
9730
9731 $this->set_keepalive_timestamp();
9732
9733 // Send updated values to FS.
9734 $site = $this->api_site_call( '/', 'put', $params, true );
9735
9736 if ( $is_two_way_sync && $this->is_api_result_entity( $site ) ) {
9737 /**
9738 * Clear scheduled install sync after a two-way sync call.
9739 *
9740 * @author Leo Fajardo (@leorw)
9741 * @since 2.2.3
9742 */
9743 if ( ! is_multisite() ) {
9744 // I successfully sent install update, clear scheduled sync if exist.
9745 $this->clear_install_sync_cron();
9746 }
9747 }
9748
9749 return $site;
9750 }
9751
9752 /**
9753 * Update installs only if changed.
9754 *
9755 * @author Vova Feldman (@svovaf)
9756 * @since 2.0.0
9757 *
9758 * @param string[] string $override
9759 * @param bool $flush
9760 * @param bool $is_two_way_sync @since 2.5.0 If true and there's a successful API request, the install sync cron will be cleared.
9761 *
9762 * @return false|object|string
9763 */
9764 private function send_installs_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9765 $this->_logger->entrance();
9766
9767 /**
9768 * Pass `true` to use the network level storage since the update is for many installs.
9769 *
9770 * @author Leo Fajardo (@leorw)
9771 * @since 2.2.3
9772 */
9773 $should_send_keepalive = $this->should_send_keepalive_update( true );
9774
9775 $installs_data = $this->get_installs_data_for_api( $override, ! $flush, $should_send_keepalive );
9776
9777 if ( empty( $installs_data ) ) {
9778 return false;
9779 }
9780
9781 if ( $is_two_way_sync ) {
9782 // Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9783 $this->set_cron_execution_timestamp( 'install_sync' );
9784 }
9785
9786 /**
9787 * Pass `true` to use the network level storage since the update is for many installs.
9788 *
9789 * @author Leo Fajardo (@leorw)
9790 * @since 2.2.3
9791 */
9792 $this->set_keepalive_timestamp( true );
9793
9794 // Send updated values to FS.
9795 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9796
9797 if ( $is_two_way_sync && $this->is_api_result_object( $result, 'installs' ) ) {
9798 // I successfully sent a two-way installs update, clear the scheduled install sync if it exists.
9799 $this->clear_install_sync_cron();
9800 }
9801
9802 return $result;
9803 }
9804
9805 /**
9806 * @author Leo Fajardo (@leorw)
9807 *
9808 * @param bool|null $use_network_level_storage
9809 *
9810 * @return bool
9811 */
9812 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9813 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9814
9815 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9816 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9817 return true;
9818 } else {
9819 // If updated 7 days ago or less, "flip a coin", if the value is 7 trigger a keepalive and update the last time it was triggered.
9820 return ( 7 == rand( 1, 7 ) );
9821 }
9822 }
9823
9824 /**
9825 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9826 *
9827 * @author Leo Fajardo (@leorw)
9828 * @since 2.3.2
9829 */
9830 private function maybe_sync_install_user() {
9831 if ( $this->_user->id == $this->_site->user_id ) {
9832 return;
9833 }
9834
9835 // Fetch user data and store if found.
9836 $this->sync_user_by_current_install();
9837 }
9838
9839 /**
9840 * Update install only if changed.
9841 *
9842 * @author Vova Feldman (@svovaf)
9843 * @since 1.0.9
9844 *
9845 * @param string[] string $override
9846 * @param bool $flush
9847 */
9848 function sync_install( $override = array(), $flush = false ) {
9849 $this->_logger->entrance();
9850
9851 $site = $this->send_install_update( $override, $flush, true );
9852
9853 if ( false === $site ) {
9854 // No sync required.
9855 return;
9856 }
9857
9858 if ( ! $this->is_api_result_entity( $site ) ) {
9859 // Failed to sync, don't update locally.
9860 return;
9861 }
9862
9863 $this->_site = new FS_Site( $site );
9864
9865 $this->_store_site( true );
9866 }
9867
9868 /**
9869 * Update install only if changed.
9870 *
9871 * @author Vova Feldman (@svovaf)
9872 * @since 1.0.9
9873 *
9874 * @param string[] string $override
9875 * @param bool $flush
9876 */
9877 private function sync_installs( $override = array(), $flush = false ) {
9878 $this->_logger->entrance();
9879
9880 $result = $this->send_installs_update( $override, $flush, true );
9881
9882 if ( false === $result ) {
9883 // No sync required.
9884 return;
9885 }
9886
9887 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9888 // Failed to sync, don't update locally.
9889 return;
9890 }
9891
9892 $address_to_blog_map = $this->get_address_to_blog_map();
9893
9894 foreach ( $result->installs as $install ) {
9895 $this->_site = new FS_Site( $install );
9896
9897 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9898 $blog_id = $address_to_blog_map[ $address ];
9899
9900 $this->_store_site( true, $blog_id );
9901 }
9902 }
9903
9904 /**
9905 * Track install's custom event.
9906 *
9907 * IMPORTANT:
9908 * Custom event tracking is currently only supported for specific clients.
9909 * If you are not one of them, please don't use this method. If you will,
9910 * the API will simply ignore your request based on the plugin ID.
9911 *
9912 * Need custom tracking for your plugin or theme?
9913 * If you are interested in custom event tracking please contact yo@freemius.com
9914 * for further details.
9915 *
9916 * @author Vova Feldman (@svovaf)
9917 * @since 1.2.1
9918 *
9919 * @param string $name Event name.
9920 * @param array $properties Associative key/value array with primitive values only
9921 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9922 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9923 *
9924 * @return object|false Event data or FALSE on failure.
9925 *
9926 * @throws \Freemius_InvalidArgumentException
9927 */
9928 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9929 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9930
9931 if ( ! $this->is_registered() ) {
9932 return false;
9933 }
9934
9935 $event = array( 'type' => $name );
9936
9937 if ( is_numeric( $process_at ) && $process_at > time() ) {
9938 $event['process_at'] = $process_at;
9939 }
9940
9941 if ( $once ) {
9942 $event['once'] = true;
9943 }
9944
9945 if ( ! empty( $properties ) ) {
9946 // Verify associative array values are primitive.
9947 foreach ( $properties as $k => $v ) {
9948 if ( ! is_scalar( $v ) ) {
9949 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9950 }
9951 }
9952
9953 $event['properties'] = $properties;
9954 }
9955
9956 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9957
9958 return $this->is_api_error( $result ) ?
9959 false :
9960 $result;
9961 }
9962
9963 /**
9964 * Track install's custom event only once, but it still triggers the API call.
9965 *
9966 * IMPORTANT:
9967 * Custom event tracking is currently only supported for specific clients.
9968 * If you are not one of them, please don't use this method. If you will,
9969 * the API will simply ignore your request based on the plugin ID.
9970 *
9971 * Need custom tracking for your plugin or theme?
9972 * If you are interested in custom event tracking please contact yo@freemius.com
9973 * for further details.
9974 *
9975 * @author Vova Feldman (@svovaf)
9976 * @since 1.2.1
9977 *
9978 * @param string $name Event name.
9979 * @param array $properties Associative key/value array with primitive values only
9980 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9981 *
9982 * @return object|false Event data or FALSE on failure.
9983 *
9984 * @throws \Freemius_InvalidArgumentException
9985 *
9986 * @user Freemius::track_event()
9987 */
9988 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9989 return $this->track_event( $name, $properties, $process_at, true );
9990 }
9991
9992 /**
9993 * Plugin uninstall hook.
9994 *
9995 * @author Vova Feldman (@svovaf)
9996 * @since 1.0.1
9997 *
9998 * @param bool $check_user Enforce checking if user have plugins activation privileges.
9999 */
10000 function _uninstall_plugin_event( $check_user = true ) {
10001 $this->_logger->entrance( 'slug = ' . $this->_slug );
10002
10003 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
10004 return;
10005 }
10006
10007 $params = array();
10008 $uninstall_reason = null;
10009 if ( isset( $this->_storage->uninstall_reason ) ) {
10010 $uninstall_reason = $this->_storage->uninstall_reason;
10011 $params['reason_id'] = $uninstall_reason->id;
10012 $params['reason_info'] = $uninstall_reason->info;
10013 }
10014
10015 if ( ! $this->is_registered() ) {
10016 // Send anonymous uninstall event only if user submitted a feedback.
10017 if ( isset( $uninstall_reason ) ) {
10018 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
10019 $this->opt_in( false, false, false, false, true );
10020 } else {
10021 $params['uid'] = $this->get_anonymous_id();
10022 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
10023 }
10024 }
10025 } else {
10026 $params = array_merge( $params, array(
10027 'is_active' => false,
10028 'is_uninstalled' => true,
10029 ) );
10030
10031 if ( $this->_is_network_active ) {
10032 // Send uninstall event.
10033 $this->send_installs_update( $params );
10034 } else {
10035 // Send uninstall event and handle the result.
10036 $this->sync_install( $params );
10037 }
10038 }
10039
10040 // @todo Decide if we want to delete plugin information from db.
10041 }
10042
10043 /**
10044 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
10045 *
10046 * @author Vova Feldman (@svovaf)
10047 * @since 2.2.1
10048 *
10049 * @param string $is_premium
10050 * @param string $caller
10051 *
10052 * @return void
10053 */
10054 function set_basename( $is_premium, $caller ) {
10055 $basename = plugin_basename( $caller );
10056
10057 $current_basename = $is_premium ?
10058 $this->_premium_plugin_basename :
10059 $this->_free_plugin_basename;
10060
10061 if ( $current_basename == $basename ) {
10062 // Basename value set correctly.
10063 return;
10064 }
10065
10066 if ( $is_premium ) {
10067 $this->_premium_plugin_basename = $basename;
10068 } else {
10069 $this->_free_plugin_basename = $basename;
10070 }
10071
10072 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
10073
10074 register_activation_hook(
10075 $plugin_dir . $basename,
10076 array( &$this, '_activate_plugin_event_hook' )
10077 );
10078 }
10079
10080 /**
10081 * @author Vova Feldman (@svovaf)
10082 * @since 1.1.1
10083 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
10084 *
10085 * @return string
10086 */
10087 function premium_plugin_basename() {
10088 if ( ! isset( $this->_premium_plugin_basename ) ) {
10089 $this->_premium_plugin_basename = $this->is_premium() ?
10090 // The product is premium, so use the current basename.
10091 $this->_plugin_basename :
10092 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
10093 }
10094
10095 return $this->_premium_plugin_basename;
10096 }
10097
10098 /**
10099 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
10100 *
10101 * @author Vova Feldman (@svovaf)
10102 * @since 1.0.2
10103 */
10104 public static function _uninstall_plugin_hook() {
10105 self::_load_required_static();
10106
10107 self::$_static_logger->entrance();
10108
10109 if ( ! current_user_can( 'activate_plugins' ) ) {
10110 return;
10111 }
10112
10113 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
10114
10115 self::$_static_logger->info( 'plugin = ' . $plugin_file );
10116
10117 define( 'WP_FS__UNINSTALL_MODE', true );
10118
10119 $fs = self::get_instance_by_file( $plugin_file );
10120
10121 if ( is_object( $fs ) ) {
10122 $fs->remove_sdk_reference();
10123
10124 self::require_plugin_essentials();
10125
10126 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
10127 is_plugin_active( $fs->premium_plugin_basename() )
10128 ) {
10129 // Deleting Free or Premium plugin version while the other version still installed.
10130 return;
10131 }
10132
10133 if (
10134 ! $fs->is_clone() &&
10135 /**
10136 * If there's a context install, run this method only when there's also a context user (e.g., when cloning a subsite of a multisite network into a single-site installation, it's possible for an install to be associated with a non-existing user entity; we want Freemius to be off in this case, while we are trying to recover the user).
10137 *
10138 * @author Leo Fajardo
10139 */
10140 ( ! is_object( $fs->_site ) || $fs->is_registered() )
10141 ) {
10142 $fs->_uninstall_plugin_event();
10143 }
10144
10145 $fs->do_action( 'after_uninstall' );
10146 }
10147 }
10148
10149 #----------------------------------------------------------------------------------
10150 #region Plugin Information
10151 #----------------------------------------------------------------------------------
10152
10153 /**
10154 * Load WordPress core plugin.php essential module.
10155 *
10156 * @author Vova Feldman (@svovaf)
10157 * @since 1.1.1
10158 */
10159 private static function require_plugin_essentials() {
10160 if ( ! function_exists( 'get_plugins' ) ) {
10161 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
10162
10163 require_once ABSPATH . 'wp-admin/includes/plugin.php';
10164 }
10165 }
10166
10167 /**
10168 * Load WordPress core pluggable.php module.
10169 *
10170 * @author Vova Feldman (@svovaf)
10171 * @since 1.1.2
10172 */
10173 private static function require_pluggable_essentials() {
10174 if ( ! function_exists( 'wp_get_current_user' ) ) {
10175 require_once ABSPATH . 'wp-includes/pluggable.php';
10176 }
10177 }
10178
10179 /**
10180 * Return plugin data.
10181 *
10182 * @author Vova Feldman (@svovaf)
10183 * @since 1.0.1
10184 *
10185 * @param bool $reparse_plugin_metadata
10186 *
10187 * @return array
10188 */
10189 function get_plugin_data( $reparse_plugin_metadata = false ) {
10190 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
10191 self::require_plugin_essentials();
10192
10193 if ( $this->is_plugin() ) {
10194 /**
10195 * @author Vova Feldman (@svovaf)
10196 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
10197 *
10198 * @link https://github.com/Freemius/wordpress-sdk/issues/77
10199 */
10200 $plugin_data = get_plugin_data(
10201 $this->_plugin_main_file_path,
10202 false,
10203 false
10204 );
10205 } else {
10206 $theme_data = wp_get_theme();
10207
10208 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
10209 $parent_theme = $theme_data->parent();
10210
10211 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
10212 $theme_data = $parent_theme;
10213 }
10214 }
10215
10216 $plugin_data = array(
10217 'Name' => $theme_data->get( 'Name' ),
10218 'Version' => $theme_data->get( 'Version' ),
10219 'Author' => $theme_data->get( 'Author' ),
10220 'Description' => $theme_data->get( 'Description' ),
10221 'PluginURI' => $theme_data->get( 'ThemeURI' ),
10222 );
10223 }
10224
10225 $this->_plugin_data = $plugin_data;
10226 }
10227
10228 return $this->_plugin_data;
10229 }
10230
10231 /**
10232 * @author Vova Feldman (@svovaf)
10233 * @since 1.0.1
10234 * @since 1.2.2.5 If slug not set load slug by module ID.
10235 *
10236 * @return string Plugin slug.
10237 */
10238 function get_slug() {
10239 if ( ! isset( $this->_slug ) ) {
10240 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
10241 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
10242 }
10243
10244 return $this->_slug;
10245 }
10246
10247 /**
10248 * @author Leo Fajardo (@leorw)
10249 * @since 2.2.1
10250 *
10251 * @return string
10252 */
10253 function get_premium_slug() {
10254 return ( is_object( $this->_plugin ) && ! empty( $this->_plugin->premium_slug ) ) ?
10255 $this->_plugin->premium_slug :
10256 "{$this->_slug}-premium";
10257 }
10258
10259 /**
10260 * Retrieve the desired folder name for the product.
10261 *
10262 * @author Vova Feldman (@svovaf)
10263 * @since 1.2.1.7
10264 *
10265 * @return string Plugin slug.
10266 */
10267 function get_target_folder_name() {
10268 return $this->can_use_premium_code() ?
10269 $this->_plugin->premium_slug :
10270 $this->_slug;
10271 }
10272
10273 /**
10274 * @author Vova Feldman (@svovaf)
10275 * @since 1.0.1
10276 *
10277 * @return number Plugin ID.
10278 */
10279 function get_id() {
10280 return $this->_plugin->id;
10281 }
10282
10283 /**
10284 * @author Leo Fajardo (@leorw)
10285 * @since 2.2.4
10286 *
10287 * @return number|null Bundle ID.
10288 */
10289 function get_bundle_id() {
10290 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
10291 $this->_plugin->bundle_id :
10292 null;
10293 }
10294
10295 /**
10296 * @author Vova Feldman (@svovaf)
10297 * @since 2.3.1
10298 *
10299 * @return string|null Bundle public key.
10300 */
10301 function get_bundle_public_key() {
10302 return isset( $this->_plugin->bundle_public_key ) ?
10303 $this->_plugin->bundle_public_key :
10304 null;
10305 }
10306
10307 /**
10308 * Get whether the SDK has been initiated in the context of a Bundle.
10309 *
10310 * This will return true, if `bundle_id` is present in the SDK init parameters.
10311 *
10312 * ```php
10313 * $my_fs = fs_dynamic_init( array(
10314 * // ...
10315 * 'bundle_id' => 'XXXX', // Will return true since we have bundle id.
10316 * 'bundle_public_key' => 'pk_XXXX',
10317 * ) );
10318 * ```
10319 *
10320 * @author Swashata Ghosh (@swashata)
10321 * @since 2.5.0
10322 *
10323 * @return bool True if we are running in bundle context, false otherwise.
10324 */
10325 private function has_bundle_context() {
10326 return ! is_null( $this->get_bundle_id() );
10327 }
10328
10329 /**
10330 * @author Vova Feldman (@svovaf)
10331 * @since 1.2.1.5
10332 *
10333 * @return string Freemius SDK version
10334 */
10335 function get_sdk_version() {
10336 return $this->version;
10337 }
10338
10339 /**
10340 * @author Vova Feldman (@svovaf)
10341 * @since 1.2.1.5
10342 *
10343 * @return number Parent plugin ID (if parent exist).
10344 */
10345 function get_parent_id() {
10346 return $this->is_addon() ?
10347 $this->get_parent_instance()->get_id() :
10348 $this->_plugin->id;
10349 }
10350
10351 /**
10352 * @author Vova Feldman (@svovaf)
10353 * @since 2.3.1
10354 *
10355 * @return string
10356 */
10357 function get_usage_tracking_terms_url() {
10358 return $this->apply_filters(
10359 'usage_tracking_terms_url',
10360 "https://freemius.com/product/opt-in/{$this->_plugin->id}/{$this->_slug}/"
10361 );
10362 }
10363
10364 /**
10365 * @todo (For LiteSDK) We can refactor this and other related functions giving links to several landing pages on freemius.com to come from a separate class like `FS_Terms_Pages`. This would get a `FS_WP_Hook` (hypothetical) instance as a dependency and use it to hook into the `license_activation_terms_url` or related filters. The entry level instance from `ms_fs()` would hold a public read-only variable `my_fs()->terms_pages` which would be an instance of `FS_Terms_Pages` and would hold all the links to the terms pages.
10366 * @since 2.5.8
10367 *
10368 * @return string
10369 */
10370 function get_license_activation_terms_url() {
10371 return $this->apply_filters(
10372 'license_activation_terms_url',
10373 "https://freemius.com/product/license-activation/{$this->_plugin->id}/{$this->_slug}/"
10374 );
10375 }
10376
10377 /**
10378 * @author Vova Feldman (@svovaf)
10379 * @since 2.3.1
10380 *
10381 * @return string
10382 */
10383 function get_eula_url() {
10384 return $this->apply_filters(
10385 'eula_url',
10386 "https://freemius.com/product/{$this->_plugin->id}/{$this->_slug}/legal/eula/"
10387 );
10388 }
10389
10390 /**
10391 * @author Vova Feldman (@svovaf)
10392 * @since 1.0.1
10393 *
10394 * @return string Plugin public key.
10395 */
10396 function get_public_key() {
10397 return $this->_plugin->public_key;
10398 }
10399
10400 /**
10401 * Will be available only on sandbox mode.
10402 *
10403 * @author Vova Feldman (@svovaf)
10404 * @since 1.0.4
10405 *
10406 * @return mixed Plugin secret key.
10407 */
10408 function get_secret_key() {
10409 return $this->_plugin->secret_key;
10410 }
10411
10412 /**
10413 * @author Vova Feldman (@svovaf)
10414 * @since 1.1.1
10415 *
10416 * @return bool
10417 */
10418 function has_secret_key() {
10419 return ! empty( $this->_plugin->secret_key );
10420 }
10421
10422 /**
10423 * @author Vova Feldman (@svovaf)
10424 * @since 1.0.9
10425 *
10426 * @param string|bool $premium_suffix
10427 *
10428 * @return string
10429 */
10430 function get_plugin_name( $premium_suffix = false ) {
10431 $this->_logger->entrance();
10432
10433 /**
10434 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10435 *
10436 * @author Vova Feldman
10437 */
10438 if ( ! isset( $this->_plugin_name ) ) {
10439 // Name is not yet set.
10440 $this->set_name( $premium_suffix );
10441 } else if (
10442 ! empty( $premium_suffix ) &&
10443 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10444 ) {
10445 // Name is already set, but there's a change in the premium suffix.
10446 $this->set_name( $premium_suffix );
10447 }
10448
10449 return $this->_plugin_name;
10450 }
10451
10452 /**
10453 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10454 *
10455 * @author Vova Feldman (@svovaf)
10456 * @since 2.2.1
10457 *
10458 * @param string $premium_suffix
10459 */
10460 private function set_name( $premium_suffix = '' ) {
10461 $plugin_data = $this->get_plugin_data();
10462
10463 // Get name.
10464 $this->_plugin_name = $plugin_data['Name'];
10465
10466 if ( is_string( $premium_suffix ) ) {
10467 $premium_suffix = trim( $premium_suffix );
10468
10469 if ( ! empty( $premium_suffix ) ) {
10470 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10471 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10472 $suffix_len = strlen( $suffix );
10473
10474 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10475 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10476 ) {
10477 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10478 }
10479 }
10480 }
10481
10482 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10483 }
10484
10485 /**
10486 * @author Vova Feldman (@svovaf)
10487 * @since 1.0.0
10488 *
10489 * @param bool $reparse_plugin_metadata
10490 *
10491 * @return string
10492 */
10493 function get_plugin_version( $reparse_plugin_metadata = false ) {
10494 $this->_logger->entrance();
10495
10496 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10497
10498 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10499
10500 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10501 }
10502
10503 /**
10504 * @author Vova Feldman (@svovaf)
10505 * @since 1.2.1.7
10506 *
10507 * @return string
10508 */
10509 function get_plugin_title() {
10510 $this->_logger->entrance();
10511
10512 $title = $this->_plugin->title;
10513
10514 return $this->apply_filters( 'plugin_title', $title );
10515 }
10516
10517 /**
10518 * @author Vova Feldman (@svovaf)
10519 * @since 1.2.2.7
10520 *
10521 * @param bool $lowercase
10522 *
10523 * @return string
10524 */
10525 function get_module_label( $lowercase = false ) {
10526 $label = $this->is_addon() ?
10527 $this->get_text_inline( 'Add-On', 'addon' ) :
10528 ( $this->is_plugin() ?
10529 $this->get_text_inline( 'Plugin', 'plugin' ) :
10530 $this->get_text_inline( 'Theme', 'theme' ) );
10531
10532 if ( $lowercase ) {
10533 $label = strtolower( $label );
10534 }
10535
10536 return $label;
10537 }
10538
10539 /**
10540 * @author Vova Feldman (@svovaf)
10541 * @since 1.0.4
10542 *
10543 * @return string
10544 */
10545 function get_plugin_basename() {
10546 if ( ! isset( $this->_plugin_basename ) ) {
10547 if ( $this->is_plugin() ) {
10548 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10549 } else {
10550 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10551 }
10552 }
10553
10554 return $this->_plugin_basename;
10555 }
10556
10557 function get_plugin_folder_name() {
10558 $this->_logger->entrance();
10559
10560 $plugin_folder = $this->_plugin_basename;
10561
10562 while ( '.' !== dirname( $plugin_folder ) ) {
10563 $plugin_folder = dirname( $plugin_folder );
10564 }
10565
10566 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10567
10568 return $plugin_folder;
10569 }
10570
10571 #endregion ------------------------------------------------------------------
10572
10573 /* Account
10574 ------------------------------------------------------------------------------------------------------------------*/
10575
10576 /**
10577 * Find plugin's slug by plugin's basename.
10578 *
10579 * @author Vova Feldman (@svovaf)
10580 * @since 1.0.9
10581 *
10582 * @param string $plugin_base_name
10583 *
10584 * @return false|string
10585 */
10586 private static function find_slug_by_basename( $plugin_base_name ) {
10587 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10588
10589 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10590 return false;
10591 }
10592
10593 return $file_slug_map[ $plugin_base_name ];
10594 }
10595
10596 /**
10597 * Store the map between the plugin's basename to the slug.
10598 *
10599 * @author Vova Feldman (@svovaf)
10600 * @since 1.0.9
10601 */
10602 private function store_file_slug_map() {
10603 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10604
10605 if ( ! array( $file_slug_map ) ) {
10606 $file_slug_map = array();
10607 }
10608
10609 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10610 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10611 ) {
10612 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10613 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10614 }
10615 }
10616
10617 /**
10618 * @return array[number]FS_User
10619 */
10620 static function get_all_users() {
10621 $users = self::maybe_get_entities_account_option( 'users', array() );
10622
10623 if ( ! is_array( $users ) ) {
10624 $users = array();
10625 }
10626
10627 return $users;
10628 }
10629
10630 /**
10631 * @param string $module_type
10632 * @param null|int $blog_id Since 2.0.0
10633 *
10634 * @return array[string]FS_Site
10635 */
10636 private static function get_all_sites(
10637 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10638 $blog_id = null,
10639 $is_backup = false
10640 ) {
10641 $sites = self::get_account_option(
10642 ( $is_backup ? 'prev_' : '' ) . 'sites',
10643 $module_type,
10644 $blog_id
10645 );
10646
10647 if ( ! is_array( $sites ) ) {
10648 $sites = array();
10649 }
10650
10651 return $sites;
10652 }
10653
10654 /**
10655 * @author Leo Fajardo (@leorw)
10656 *
10657 * @since 1.2.2
10658 *
10659 * @param string $option_name
10660 * @param string $module_type
10661 * @param null|int $network_level_or_blog_id Since 2.0.0
10662 *
10663 * @return mixed
10664 */
10665 private static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10666 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10667 $option_name = $module_type . '_' . $option_name;
10668 }
10669
10670 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10671 }
10672
10673 /**
10674 * @author Leo Fajardo (@leorw)
10675 *
10676 * @since 1.2.2
10677 *
10678 * @param string $option_name
10679 * @param mixed $option_value
10680 * @param bool $store
10681 * @param null|int $network_level_or_blog_id Since 2.0.0
10682 */
10683 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10684 self::set_account_option_by_module(
10685 $this->_module_type,
10686 $option_name,
10687 $option_value,
10688 $store,
10689 $network_level_or_blog_id
10690 );
10691 }
10692
10693 /**
10694 * @author Vova Feldman (@svovaf)
10695 *
10696 * @since 1.2.2.7
10697 *
10698 * @param string $module_type
10699 * @param string $option_name
10700 * @param mixed $option_value
10701 * @param bool $store
10702 * @param null|int $network_level_or_blog_id Since 2.0.0
10703 */
10704 private static function set_account_option_by_module(
10705 $module_type,
10706 $option_name,
10707 $option_value,
10708 $store,
10709 $network_level_or_blog_id = null
10710 ) {
10711 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10712 $option_name = $module_type . '_' . $option_name;
10713 }
10714
10715 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10716 }
10717
10718 /**
10719 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10720 *
10721 * @author Leo Fajardo (@leorw)
10722 * @since 2.3.1
10723 *
10724 * @param string $option_name
10725 * @param mixed $default
10726 * @param null|bool|int $network_level_or_blog_id When an integer, use the given blog storage. When `true` use the multisite storage (if there's a network). When `false`, use the current context blog storage. When `null`, the decision which storage to use (MS vs. Current S) will be handled internally and determined based on the $option (based on self::$_SITE_LEVEL_PARAMS).
10727 *
10728 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10729 */
10730 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10731 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10732
10733 $class_name = '';
10734
10735 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10736 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10737 }
10738
10739 switch ( $option_name ) {
10740 case 'plugins':
10741 case 'themes':
10742 case 'addons':
10743 $class_name = FS_Plugin::get_class_name();
10744 break;
10745 case 'users':
10746 $class_name = FS_User::get_class_name();
10747 break;
10748 case 'sites':
10749 $class_name = FS_Site::get_class_name();
10750 break;
10751 case 'licenses':
10752 case 'all_licenses':
10753 $class_name = FS_Plugin_License::get_class_name();
10754 break;
10755 case 'plans':
10756 $class_name = FS_Plugin_Plan::get_class_name();
10757 break;
10758 case 'updates':
10759 $class_name = FS_Plugin_Tag::get_class_name();
10760 break;
10761 }
10762
10763 if ( empty( $class_name ) ) {
10764 return $option;
10765 }
10766
10767 return fs_get_entities( $option, $class_name );
10768 }
10769
10770 /**
10771 * @author Vova Feldman (@svovaf)
10772 * @since 1.0.6
10773 *
10774 * @param number|null $module_id
10775 *
10776 * @return FS_Plugin_License[]
10777 */
10778 private static function get_all_licenses( $module_id = null ) {
10779 $licenses = self::get_account_option( 'all_licenses' );
10780
10781 if ( ! is_array( $licenses ) ) {
10782 $licenses = array();
10783 }
10784
10785 if ( is_null( $module_id ) ) {
10786 return $licenses;
10787 }
10788
10789 $licenses = isset( $licenses[ $module_id ] ) ?
10790 $licenses[ $module_id ] :
10791 array();
10792
10793 return $licenses;
10794 }
10795
10796 /**
10797 * @author Leo Fajardo (@leorw)
10798 * @since 2.0.0
10799 *
10800 * @return array
10801 */
10802 private static function get_all_licenses_by_module_type() {
10803 $licenses = self::get_account_option( 'all_licenses' );
10804
10805 $licenses_by_module_type = array(
10806 WP_FS__MODULE_TYPE_PLUGIN => array(),
10807 WP_FS__MODULE_TYPE_THEME => array()
10808 );
10809
10810 if ( ! is_array( $licenses ) ) {
10811 return $licenses_by_module_type;
10812 }
10813
10814 foreach ( $licenses as $module_id => $module_licenses ) {
10815 $fs = self::get_instance_by_id( $module_id );
10816 if ( false === $fs ) {
10817 continue;
10818 }
10819
10820 $licenses_by_module_type[ $fs->_module_type ] = array_merge( $licenses_by_module_type[ $fs->_module_type ], $module_licenses );
10821 }
10822
10823 return $licenses_by_module_type;
10824 }
10825
10826 /**
10827 * @author Leo Fajardo (@leorw)
10828 * @since 2.0.0
10829 *
10830 * @param number $module_id
10831 * @param number|null $user_id
10832 *
10833 * @return array
10834 */
10835 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10836 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10837
10838 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10839 $all_modules_user_id_license_ids_map = array();
10840 }
10841
10842 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10843 $all_modules_user_id_license_ids_map[ $module_id ] :
10844 array();
10845
10846 if ( FS_User::is_valid_id( $user_id ) ) {
10847 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10848 $user_id_license_ids_map[ $user_id ] :
10849 array();
10850 }
10851
10852 return $user_id_license_ids_map;
10853 }
10854
10855 /**
10856 * @author Leo Fajardo (@leorw)
10857 * @since 2.0.0
10858 *
10859 * @param array $new_user_id_license_ids_map
10860 * @param number $module_id
10861 * @param number|null $user_id
10862 */
10863 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10864 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10865 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10866 $all_modules_user_id_license_ids_map = array();
10867 }
10868
10869 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10870 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10871 }
10872
10873 if ( FS_User::is_valid_id( $user_id ) ) {
10874 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10875 } else {
10876 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10877 }
10878
10879 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10880 }
10881
10882 /**
10883 * Get a collection of the user's linked license IDs.
10884 *
10885 * @author Vova Feldman (@svovaf)
10886 * @since 2.0.0
10887 *
10888 * @param number $user_id
10889 *
10890 * @return number[]
10891 */
10892 private function get_user_linked_license_ids( $user_id ) {
10893 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10894 }
10895
10896 /**
10897 * Override the user's linked license IDs with a new IDs collection.
10898 *
10899 * @author Vova Feldman (@svovaf)
10900 * @since 2.0.0
10901 *
10902 * @param number $user_id
10903 * @param number[] $license_ids
10904 */
10905 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10906 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10907 }
10908
10909 /**
10910 * Link a specified license ID to a given user.
10911 *
10912 * @author Vova Feldman (@svovaf)
10913 * @since 2.0.0
10914 *
10915 * @param number $license_id
10916 * @param number $user_id
10917 */
10918 private function link_license_2_user( $license_id, $user_id ) {
10919 $license_ids = $this->get_user_linked_license_ids( $user_id );
10920
10921 if ( in_array( $license_id, $license_ids ) ) {
10922 // License already linked.
10923 return;
10924 }
10925
10926 $license_ids[] = $license_id;
10927
10928 $this->set_user_linked_license_ids( $user_id, $license_ids );
10929 }
10930
10931 /**
10932 * @param string|bool $module_type
10933 *
10934 * @return FS_Plugin_Plan[]
10935 */
10936 private static function get_all_plans( $module_type = false ) {
10937 $plans = self::get_account_option( 'plans', $module_type );
10938
10939 if ( ! is_array( $plans ) ) {
10940 $plans = array();
10941 }
10942
10943 return $plans;
10944 }
10945
10946 /**
10947 * @author Vova Feldman (@svovaf)
10948 * @since 1.0.4
10949 *
10950 * @return FS_Plugin_Tag[]
10951 */
10952 private static function get_all_updates() {
10953 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10954
10955 if ( ! is_array( $updates ) ) {
10956 $updates = array();
10957 }
10958
10959 return $updates;
10960 }
10961
10962 /**
10963 * @author Vova Feldman (@svovaf)
10964 * @since 1.0.6
10965 *
10966 * @return array<number,FS_Plugin[]>|false
10967 */
10968 private static function get_all_addons() {
10969 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10970
10971 if ( ! is_array( $addons ) ) {
10972 $addons = array();
10973 }
10974
10975 return $addons;
10976 }
10977
10978 /**
10979 * @author Vova Feldman (@svovaf)
10980 * @since 1.0.6
10981 *
10982 * @return number[]|false
10983 */
10984 private static function get_all_account_addons() {
10985 $addons = self::$_accounts->get_option( 'account_addons', array() );
10986
10987 if ( ! is_array( $addons ) ) {
10988 $addons = array();
10989 }
10990
10991 return $addons;
10992 }
10993
10994 /**
10995 * Check if user has connected his account (opted-in).
10996 *
10997 * Note:
10998 * If the user opted-in and opted-out on a later stage,
10999 * this will still return true. If you want to check if the
11000 * user is currently opted-in, use:
11001 * `$fs->is_registered() && $fs->is_tracking_allowed()`
11002 *
11003 * @author Vova Feldman (@svovaf)
11004 * @since 1.0.1
11005 *
11006 * @param bool $ignore_anonymous_state Since 2.5.1
11007 *
11008 * @return bool
11009 */
11010 function is_registered( $ignore_anonymous_state = false ) {
11011 return (
11012 is_object( $this->_user ) &&
11013 (
11014 $this->is_premium() ||
11015 $ignore_anonymous_state ||
11016 ! $this->is_anonymous()
11017 )
11018 );
11019 }
11020
11021 /**
11022 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
11023 *
11024 * @author Leo Fajardo (@leorw)
11025 * @since 1.2.1.5
11026 *
11027 * @return bool
11028 */
11029 function is_tracking_allowed( $blog_id = null, $install = null ) {
11030 if ( is_null( $install ) ) {
11031 $install = is_null( $blog_id ) ?
11032 $this->_site :
11033 $this->get_install_by_blog_id( $blog_id );
11034 }
11035
11036 return (
11037 is_object( $install ) &&
11038 FS_Permission_Manager::instance( $this )->is_homepage_url_tracking_allowed( $blog_id )
11039 );
11040 }
11041
11042 /**
11043 * Returns TRUE if the user never opted-in or manually opted-out.
11044 *
11045 * @author Vova Feldman (@svovaf)
11046 * @since 1.2.1.5
11047 *
11048 * @param int|null $blog_id
11049 *
11050 * @return bool
11051 */
11052 function is_tracking_prohibited( $blog_id = null ) {
11053 return (
11054 ! $this->is_registered( true ) ||
11055 ! $this->is_tracking_allowed( $blog_id )
11056 );
11057 }
11058
11059 /**
11060 * @author Leo Fajardo (@leorw)
11061 * @since 2.4.0
11062 *
11063 * @return bool
11064 */
11065 function is_bundle_license_auto_activation_enabled() {
11066 return $this->is_addon() ?
11067 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
11068 $this->_is_bundle_license_auto_activation_enabled;
11069 }
11070
11071 /**
11072 * @author Vova Feldman (@svovaf)
11073 * @since 1.0.4
11074 *
11075 * @return FS_Plugin
11076 */
11077 function get_plugin() {
11078 return $this->_plugin;
11079 }
11080
11081 /**
11082 * @author Vova Feldman (@svovaf)
11083 * @since 1.0.3
11084 *
11085 * @return FS_User
11086 */
11087 function get_user() {
11088 return $this->_user;
11089 }
11090
11091 /**
11092 * @author Vova Feldman (@svovaf)
11093 * @since 1.0.3
11094 *
11095 * @return FS_Site
11096 */
11097 function get_site() {
11098 return $this->_site;
11099 }
11100
11101 /**
11102 * @author Leo Fajardo (@leorw)
11103 * @since 2.5.0
11104 */
11105 function store_site( $site ) {
11106 $this->_site = $site;
11107 $this->_store_site( true );
11108 }
11109
11110 /**
11111 * Deletes the current install with an option to back it up in case restoration will be needed (e.g., if the automatic clone resolution attempt fails).
11112 *
11113 * @author Leo Fajardo (@leorw)
11114 * @since 2.5.0
11115 */
11116 function delete_current_install( $back_up ) {
11117 // Back up and delete the unique ID.
11118 if ( $back_up ) {
11119 self::$_accounts->set_option( 'prev_unique_id', $this->get_anonymous_id() );
11120 }
11121
11122 self::$_accounts->set_option( 'unique_id', null );
11123
11124 if ( $back_up ) {
11125 // Back up the install before deleting it so that it can be restored later on if necessary (e.g., if the automatic clone resolution attempt fails).
11126 $this->back_up_site();
11127 }
11128
11129 $this->_delete_site();
11130 $this->_site = null;
11131 }
11132
11133 /**
11134 * @author Leo Fajardo (@leorw)
11135 * @since 2.5.0
11136 */
11137 function restore_backup_site() {
11138 self::$_accounts->set_option(
11139 'unique_id',
11140 self::$_accounts->get_option( 'prev_unique_id' )
11141 );
11142
11143 $sites = self::get_all_sites( $this->_module_type, null, true );
11144 $this->store_site( clone $sites[ $this->_slug ] );
11145 }
11146
11147 /**
11148 * Get plugin add-ons.
11149 *
11150 * @author Vova Feldman (@svovaf)
11151 * @since 1.0.6
11152 *
11153 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
11154 *
11155 * @param bool $flush
11156 *
11157 * @return FS_Plugin[]|false
11158 */
11159 function get_addons( $flush = false ) {
11160 $this->_logger->entrance();
11161
11162 if ( ! $this->_has_addons ) {
11163 return false;
11164 }
11165
11166 $addons = $this->sync_addons( $flush );
11167
11168 return ( ! is_array( $addons ) || empty( $addons ) ) ?
11169 false :
11170 $addons;
11171 }
11172
11173 /**
11174 * @author Vova Feldman (@svovaf)
11175 * @since 1.0.6
11176 *
11177 * @return number[]|false
11178 */
11179 function get_account_addons() {
11180 $this->_logger->entrance();
11181
11182 $addons = self::get_all_account_addons();
11183
11184 if ( ! is_array( $addons ) ||
11185 ! isset( $addons[ $this->_plugin->id ] ) ||
11186 ! is_array( $addons[ $this->_plugin->id ] ) ||
11187 0 === count( $addons[ $this->_plugin->id ] )
11188 ) {
11189 return false;
11190 }
11191
11192 return $addons[ $this->_plugin->id ];
11193 }
11194
11195 /**
11196 * Check if user has any
11197 *
11198 * @author Vova Feldman (@svovaf)
11199 * @since 1.1.6
11200 *
11201 * @return bool
11202 */
11203 function has_account_addons() {
11204 $addons = $this->get_account_addons();
11205
11206 return is_array( $addons ) && ( 0 < count( $addons ) );
11207 }
11208
11209
11210 /**
11211 * Get add-on by ID (from local data).
11212 *
11213 * @author Vova Feldman (@svovaf)
11214 * @since 1.0.6
11215 *
11216 * @param number $id
11217 *
11218 * @return FS_Plugin|false
11219 */
11220 function get_addon( $id ) {
11221 $this->_logger->entrance();
11222
11223 $addons = $this->get_addons();
11224
11225 if ( is_array( $addons ) ) {
11226 foreach ( $addons as $addon ) {
11227 if ( $id == $addon->id ) {
11228 return $addon;
11229 }
11230 }
11231 }
11232
11233 return false;
11234 }
11235
11236 /**
11237 * Get add-on by slug (from local data).
11238 *
11239 * @author Vova Feldman (@svovaf)
11240 * @since 1.0.6
11241 *
11242 * @param string $slug
11243 *
11244 * @param bool $flush
11245 *
11246 * @return FS_Plugin|false
11247 */
11248 function get_addon_by_slug( $slug, $flush = false ) {
11249 $this->_logger->entrance();
11250
11251 $addons = $this->get_addons( $flush );
11252
11253 if ( is_array( $addons ) ) {
11254 foreach ( $addons as $addon ) {
11255 if ( $slug === $addon->slug ) {
11256 return $addon;
11257 }
11258 }
11259 }
11260
11261 return false;
11262 }
11263
11264 /**
11265 * @var array<number,object[]> {
11266 * @key number Add-on ID.
11267 * @val object[] The add-on's plans and prices object.
11268 * }
11269 */
11270 private $plans_and_pricing_by_addon_id;
11271
11272 /**
11273 * @author Leo Fajardo (@leorw)
11274 * @since 2.3.0
11275 *
11276 * @return array<number,object[]> {
11277 * @key number Add-on ID.
11278 * @val object[] The add-on's plans and prices object.
11279 * }
11280 */
11281 function _get_addons_plans_and_pricing_map_by_id() {
11282 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
11283 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
11284
11285 $plans_and_pricing_by_addon_id = array();
11286 if ( $this->is_api_result_object( $result, 'addons' ) ) {
11287 foreach ( $result->addons as $addon ) {
11288 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
11289 }
11290 }
11291
11292 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
11293 }
11294
11295 return $this->plans_and_pricing_by_addon_id;
11296 }
11297
11298 /**
11299 * @author Leo Fajardo (@leorw)
11300 * @since 2.3.0
11301 *
11302 * @param number $addon_id
11303 * @param bool $is_installed
11304 *
11305 * @return array
11306 */
11307 function _get_addon_info( $addon_id, $is_installed ) {
11308 $addon = $this->get_addon( $addon_id );
11309
11310 if ( ! is_object( $addon ) ) {
11311 // Unexpected call.
11312 return array();
11313 }
11314
11315 $slug = $addon->slug;
11316
11317 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
11318
11319 if ( ! fs_is_network_admin() ) {
11320 // Get blog-level activated installations.
11321 $sites = self::maybe_get_entities_account_option( 'sites', array() );
11322 } else {
11323 $sites = null;
11324
11325 if ( $this->is_addon_activated( $addon_id ) &&
11326 $this->get_addon_instance( $addon_id )->is_network_active()
11327 ) {
11328 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
11329 // Get network-level activated installations.
11330 $sites = self::maybe_get_entities_account_option(
11331 'sites',
11332 array(),
11333 $addon_storage->network_install_blog_id
11334 );
11335 }
11336 }
11337 }
11338
11339 $addon_info = array(
11340 'is_connected' => false,
11341 'slug' => $slug,
11342 'title' => $addon->title,
11343 'is_whitelabeled' => $addon_storage->is_whitelabeled
11344 );
11345
11346 if ( ! $is_installed ) {
11347 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
11348
11349 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
11350 $has_paid_plan = false;
11351 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
11352
11353 if ( is_array( $plans ) && count( $plans ) > 0 ) {
11354 foreach ( $plans as $plan ) {
11355 if ( isset( $plan->pricing ) &&
11356 is_array( $plan->pricing ) &&
11357 count( $plan->pricing ) > 0
11358 ) {
11359 $has_paid_plan = true;
11360 break;
11361 }
11362 }
11363 }
11364
11365 $addon_info['has_paid_plan'] = $has_paid_plan;
11366 }
11367 }
11368
11369 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
11370 return $addon_info;
11371 }
11372
11373 $site = $sites[ $slug ];
11374
11375 $addon_info['is_connected'] = (
11376 ( $addon->parent_plugin_id == $this->get_id() ) &&
11377 is_object( $site ) &&
11378 FS_Site::is_valid_id( $site->id ) &&
11379 FS_User::is_valid_id( $site->user_id ) &&
11380 FS_Plugin_Plan::is_valid_id( $site->plan_id )
11381 );
11382
11383 if ( $addon_info['is_connected'] && $is_installed ) {
11384 return $addon_info;
11385 }
11386
11387 $addon_info['site'] = $site;
11388
11389 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
11390 if ( isset( $plugins_data[ $slug ] ) ) {
11391 $plugin_data = $plugins_data[ $slug ];
11392
11393 $addon_info['version'] = $plugin_data->version;
11394 }
11395
11396 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
11397 if ( isset( $all_plans[ $slug ] ) ) {
11398 $plans = $all_plans[ $slug ];
11399
11400 foreach ( $plans as $plan ) {
11401 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
11402 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
11403 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
11404 break;
11405 }
11406 }
11407 }
11408
11409 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
11410 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
11411 foreach ( $licenses[ $addon_id ] as $license ) {
11412 if ( $license->id == $site->license_id ) {
11413 $addon_info['license'] = $license;
11414 break;
11415 }
11416 }
11417 }
11418
11419 if ( isset( $addon_info['license'] ) ) {
11420 if ( isset( $addon_storage->subscriptions ) &&
11421 ! empty( $addon_storage->subscriptions )
11422 ) {
11423 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11424
11425 foreach ( $addon_subscriptions as $subscription ) {
11426 if ( $subscription->license_id == $site->license_id ) {
11427 $addon_info['subscription'] = $subscription;
11428 break;
11429 }
11430 }
11431 }
11432 }
11433
11434 return $addon_info;
11435 }
11436
11437 /**
11438 * @author Vova Feldman (@svovaf)
11439 * @since 2.0.0
11440 *
11441 * @param number $user_id
11442 *
11443 * @return FS_User
11444 */
11445 static function _get_user_by_id( $user_id ) {
11446 self::$_static_logger->entrance( "user_id = {$user_id}" );
11447
11448 $users = self::get_all_users();
11449
11450 if ( is_array( $users ) ) {
11451 if ( isset( $users[ $user_id ] ) &&
11452 $users[ $user_id ] instanceof FS_User &&
11453 $user_id == $users[ $user_id ]->id
11454 ) {
11455 return $users[ $user_id ];
11456 }
11457
11458 // If user wasn't found by the key, iterate over all the users collection.
11459 foreach ( $users as $user ) {
11460 /**
11461 * @var FS_User $user
11462 */
11463 if ( $user_id == $user->id ) {
11464 return $user;
11465 }
11466 }
11467 }
11468
11469 return null;
11470 }
11471
11472 /**
11473 * Checks if a Freemius user_id is associated with a super-admin.
11474 *
11475 * @author Vova Feldman (@svovaf)
11476 * @since 2.0.0
11477 *
11478 * @param number $user_id
11479 *
11480 * @return bool
11481 */
11482 private static function is_super_admin( $user_id ) {
11483 $is_super_admin = false;
11484
11485 $user = self::_get_user_by_id( $user_id );
11486
11487 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11488 self::require_pluggable_essentials();
11489
11490 $wp_user = get_user_by( 'email', $user->email );
11491
11492 if ( $wp_user instanceof WP_User ) {
11493 $super_admins = get_super_admins();
11494 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11495 }
11496 }
11497
11498 return $is_super_admin;
11499 }
11500
11501 #----------------------------------------------------------------------------------
11502 #region Plans & Licensing
11503 #----------------------------------------------------------------------------------
11504
11505 /**
11506 * Check if running premium plugin code.
11507 *
11508 * @author Vova Feldman (@svovaf)
11509 * @since 1.0.5
11510 *
11511 * @return bool
11512 */
11513 function is_premium() {
11514 /**
11515 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11516 * `register_constructor_hooks` method.
11517 *
11518 * @author Leo Fajardo (@leorw)
11519 * @since 2.2.3
11520 */
11521 return is_object( $this->_plugin ) ?
11522 $this->_plugin->is_premium :
11523 false;
11524 }
11525
11526 /**
11527 * Get site's plan ID.
11528 *
11529 * @author Vova Feldman (@svovaf)
11530 * @since 1.0.2
11531 *
11532 * @return number
11533 */
11534 function get_plan_id() {
11535 return $this->_site->plan_id;
11536 }
11537
11538 /**
11539 * Get site's plan title.
11540 *
11541 * @author Vova Feldman (@svovaf)
11542 * @since 1.0.2
11543 *
11544 * @return string
11545 */
11546 function get_plan_title() {
11547 $plan = $this->get_plan();
11548
11549 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11550 }
11551
11552 /**
11553 * Get site's plan name.
11554 *
11555 * @author Vova Feldman (@svovaf)
11556 * @since 2.0.0
11557 *
11558 * @return string
11559 */
11560 function get_plan_name() {
11561 $plan = $this->get_plan();
11562
11563 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11564 }
11565
11566 /**
11567 * @author Vova Feldman (@svovaf)
11568 * @since 1.0.9
11569 *
11570 * @return FS_Plugin_Plan|false
11571 */
11572 function get_plan() {
11573 if ( ! is_object( $this->_site ) ) {
11574 return false;
11575 }
11576
11577 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11578 $this->_get_plan_by_id( $this->_site->plan_id ) :
11579 false;
11580 }
11581
11582 /**
11583 * @author Vova Feldman (@svovaf)
11584 * @since 1.0.3
11585 *
11586 * @return bool
11587 */
11588 function is_trial() {
11589 $this->_logger->entrance();
11590
11591 if ( ! $this->is_registered( true ) || ! is_object( $this->_site ) ) {
11592 return false;
11593 }
11594
11595 return $this->_site->is_trial();
11596 }
11597
11598 /**
11599 * Check if currently in a trial with payment method (credit card or paypal).
11600 *
11601 * @author Vova Feldman (@svovaf)
11602 * @since 1.1.7
11603 *
11604 * @return bool
11605 */
11606 function is_paid_trial() {
11607 $this->_logger->entrance();
11608
11609 if ( ! $this->is_trial() ) {
11610 return false;
11611 }
11612
11613 if ( ! $this->has_active_valid_license() ) {
11614 return false;
11615 }
11616
11617 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11618 return false;
11619 }
11620
11621 /**
11622 * @var FS_Subscription $subscription
11623 */
11624 $subscription = $this->_get_subscription( $this->_license->id );
11625
11626 return ( is_object( $subscription ) && $subscription->is_active() );
11627 }
11628
11629 /**
11630 * Check if trial already utilized.
11631 *
11632 * @since 1.0.9
11633 *
11634 * @return bool
11635 */
11636 function is_trial_utilized() {
11637 $this->_logger->entrance();
11638
11639 if ( ! $this->is_registered() ) {
11640 return false;
11641 }
11642
11643 return $this->_site->is_trial_utilized();
11644 }
11645
11646 /**
11647 * Get trial plan information (if in trial).
11648 *
11649 * @author Vova Feldman (@svovaf)
11650 * @since 1.0.9
11651 *
11652 * @return bool|FS_Plugin_Plan
11653 */
11654 function get_trial_plan() {
11655 $this->_logger->entrance();
11656
11657 if ( ! $this->is_trial() ) {
11658 return false;
11659 }
11660
11661 // Try to load plan from local cache.
11662 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11663
11664 if ( ! is_object( $trial_plan ) ) {
11665 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11666
11667 /**
11668 * If managed to fetch the plan, add it to the plans collection.
11669 */
11670 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11671 if ( ! is_array( $this->_plans ) ) {
11672 $this->_plans = array();
11673 }
11674
11675 $this->_plans[] = $trial_plan;
11676 $this->_store_plans();
11677 }
11678 }
11679
11680 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11681 return $trial_plan;
11682 }
11683
11684 /**
11685 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11686 */
11687 $trial_plan = new FS_Plugin_Plan();
11688 $trial_plan->id = $this->_site->trial_plan_id;
11689 $trial_plan->name = 'pro';
11690 $trial_plan->title = 'Pro';
11691
11692 return $trial_plan;
11693 }
11694
11695 /**
11696 * Check if the user has an activate, non-expired license on current plugin's install.
11697 *
11698 * @since 1.0.9
11699 *
11700 * @return bool
11701 */
11702 function is_paying() {
11703 $this->_logger->entrance();
11704
11705 if ( ! $this->is_registered( true ) ) {
11706 return false;
11707 }
11708
11709 if ( ! $this->has_paid_plan() ) {
11710 return false;
11711 }
11712
11713 return (
11714 ! $this->is_trial() &&
11715 'free' !== $this->get_plan_name() &&
11716 $this->has_active_valid_license()
11717 );
11718 }
11719
11720 /**
11721 * @author Vova Feldman (@svovaf)
11722 * @since 1.0.4
11723 *
11724 * @return bool
11725 */
11726 function is_free_plan() {
11727 if ( ! $this->is_registered() ) {
11728 return true;
11729 }
11730
11731 if ( ! $this->has_paid_plan() ) {
11732 return true;
11733 }
11734
11735 return (
11736 'free' === $this->get_plan_name() ||
11737 ! $this->has_features_enabled_license()
11738 );
11739 }
11740
11741 /**
11742 * @author Vova Feldman (@svovaf)
11743 * @since 1.0.5
11744 *
11745 * @return bool
11746 */
11747 function _has_premium_license() {
11748 $this->_logger->entrance();
11749
11750 $premium_license = $this->_get_available_premium_license();
11751
11752 return ( false !== $premium_license );
11753 }
11754
11755 /**
11756 * Check if user has any licenses associated with the plugin (including expired or blocking).
11757 *
11758 * @author Vova Feldman (@svovaf)
11759 * @since 1.1.7.3
11760 *
11761 * @param bool $including_foreign
11762 *
11763 * @return bool
11764 */
11765 function has_any_license( $including_foreign = true ) {
11766 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11767 return false;
11768 }
11769
11770 if ( $including_foreign ) {
11771 return true;
11772 }
11773
11774 foreach ( $this->_licenses as $license ) {
11775 if ( $this->_user->id == $license->user_id ) {
11776 return true;
11777 }
11778 }
11779
11780 return false;
11781 }
11782
11783 /**
11784 * @author Vova Feldman (@svovaf)
11785 * @since 1.0.5
11786 *
11787 * @param bool|null $is_localhost
11788 *
11789 * @return FS_Plugin_License|false
11790 */
11791 function _get_available_premium_license( $is_localhost = null ) {
11792 $this->_logger->entrance();
11793
11794 $licenses = $this->get_available_premium_licenses( $is_localhost );
11795 if ( ! empty( $licenses ) ) {
11796 return $licenses[0];
11797 }
11798
11799 return false;
11800 }
11801
11802 /**
11803 * @author Vova Feldman (@svovaf)
11804 * @since 1.0.5
11805 *
11806 * @param bool|null $is_localhost
11807 *
11808 * @return FS_Plugin_License[]
11809 */
11810 function get_available_premium_licenses( $is_localhost = null ) {
11811 $this->_logger->entrance();
11812
11813 $licenses = array();
11814 if ( ! $this->has_paid_plan() ) {
11815 return $licenses;
11816 }
11817
11818 if ( is_array( $this->_licenses ) ) {
11819 foreach ( $this->_licenses as $license ) {
11820 if ( ! $license->can_activate( $is_localhost ) ) {
11821 continue;
11822 }
11823
11824 $licenses[] = $license;
11825 }
11826 }
11827
11828 return $licenses;
11829 }
11830
11831 /**
11832 * Sync local plugin plans with remote server.
11833 *
11834 * IMPORTANT: If for some reason a site is associated with deleted plan, we'll preserve the plan's information and append it as the last plan. This means that if plan is deleted, the is_plan() method will ALWAYS return true for any given argument (it becomes the most inclusive plan).
11835 *
11836 * @author Vova Feldman (@svovaf)
11837 * @since 1.0.5
11838 *
11839 * @return FS_Plugin_Plan[]|object
11840 */
11841 function _sync_plans() {
11842 $plans = $this->_fetch_plugin_plans();
11843
11844 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11845 $plans_map = array();
11846 foreach ( $plans as $plan ) {
11847 $plans_map[ $plan->id ] = true;
11848 }
11849
11850 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11851
11852 foreach ( $plans_ids_to_keep as $plan_id ) {
11853 if ( isset( $plans_map[ $plan_id ] ) ) {
11854 continue;
11855 }
11856
11857 $missing_plan = self::_get_plan_by_id( $plan_id );
11858
11859 if ( is_object( $missing_plan ) ) {
11860 $plans[] = $missing_plan;
11861 }
11862 }
11863
11864 $this->_plans = $plans;
11865 $this->_store_plans();
11866 }
11867
11868 $this->do_action( 'after_plans_sync', $plans );
11869
11870 return $this->_plans;
11871 }
11872
11873 /**
11874 * Check if specified plan exists locally. If not, fetch it and store it.
11875 *
11876 * @author Vova Feldman (@svovaf)
11877 * @since 2.0.0
11878 *
11879 * @param number $plan_id
11880 *
11881 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11882 */
11883 private function sync_plan_if_not_exist( $plan_id ) {
11884 $plan = self::_get_plan_by_id( $plan_id );
11885
11886 if ( is_object( $plan ) ) {
11887 // Plan already exists.
11888 return $plan;
11889 }
11890
11891 $plan = $this->fetch_plan_by_id( $plan_id );
11892
11893 if ( $plan instanceof FS_Plugin_Plan ) {
11894 $this->_plans[] = $plan;
11895 $this->_store_plans();
11896
11897 return $plan;
11898 }
11899
11900 return $plan;
11901 }
11902
11903 /**
11904 * Check if specified license exists locally. If not, fetch it and store it.
11905 *
11906 * @author Vova Feldman (@svovaf)
11907 * @since 2.0.0
11908 *
11909 * @param number $license_id
11910 * @param string $license_key
11911 *
11912 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11913 */
11914 private function sync_license_if_not_exist( $license_id, $license_key ) {
11915 $license = $this->_get_license_by_id( $license_id );
11916
11917 if ( is_object( $license ) ) {
11918 // License already exists.
11919 return $license;
11920 }
11921
11922 $license = $this->fetch_license_by_key( $license_id, $license_key );
11923
11924 if ( $license instanceof FS_Plugin_License ) {
11925 $this->_licenses[] = $license;
11926
11927 $this->set_license( $license );
11928
11929 $this->_store_licenses();
11930
11931 return $license;
11932 }
11933
11934 return $license;
11935 }
11936
11937 /**
11938 * Get a collection of unique plan IDs that are associated with any installs in the network.
11939 *
11940 * @author Leo Fajardo (@leorw)
11941 * @since 2.0.0
11942 *
11943 * @return number[]
11944 */
11945 private function get_plans_ids_associated_with_installs() {
11946 if ( ! is_multisite() ) {
11947 if ( ! is_object( $this->_site ) ||
11948 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11949 ) {
11950 return array();
11951 }
11952
11953 return array( $this->_site->plan_id );
11954 }
11955
11956 $plan_ids = array();
11957 $sites = self::get_sites();
11958 foreach ( $sites as $site ) {
11959 $blog_id = self::get_site_blog_id( $site );
11960 $install = $this->get_install_by_blog_id( $blog_id );
11961
11962 if ( ! is_object( $install ) ||
11963 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11964 ) {
11965 continue;
11966 }
11967
11968 $plan_ids[ $install->plan_id ] = true;
11969 }
11970
11971 return array_keys( $plan_ids );
11972 }
11973
11974 /**
11975 * Get a collection of unique license IDs that are associated with any installs in the network.
11976 *
11977 * @author Leo Fajardo (@leorw)
11978 * @since 2.0.0
11979 *
11980 * @return number[]
11981 */
11982 private function get_license_ids_associated_with_installs() {
11983 if ( ! $this->_is_network_active ) {
11984 if ( ! is_object( $this->_site ) ||
11985 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11986 ) {
11987 return array();
11988 }
11989
11990 return array( $this->_site->license_id );
11991 }
11992
11993 $license_ids = array();
11994 $sites = self::get_sites();
11995 foreach ( $sites as $site ) {
11996 $blog_id = self::get_site_blog_id( $site );
11997 $install = $this->get_install_by_blog_id( $blog_id );
11998
11999 if ( ! is_object( $install ) ||
12000 ! FS_Plugin_License::is_valid_id( $install->license_id )
12001 ) {
12002 continue;
12003 }
12004
12005 $license_ids[ $install->license_id ] = true;
12006 }
12007
12008 return array_keys( $license_ids );
12009 }
12010
12011 /**
12012 * @author Vova Feldman (@svovaf)
12013 * @since 1.0.5
12014 *
12015 * @param number $id
12016 *
12017 * @return FS_Plugin_Plan|false
12018 */
12019 function _get_plan_by_id( $id ) {
12020 $this->_logger->entrance();
12021
12022 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
12023 $this->_sync_plans();
12024 }
12025
12026 foreach ( $this->_plans as $plan ) {
12027 if ( $id == $plan->id ) {
12028 return $plan;
12029 }
12030 }
12031
12032 return false;
12033 }
12034
12035 /**
12036 * @author Vova Feldman (@svovaf)
12037 * @since 1.1.8.1
12038 *
12039 * @param string $name
12040 *
12041 * @return FS_Plugin_Plan|false
12042 */
12043 private function get_plan_by_name( $name ) {
12044 $this->_logger->entrance();
12045
12046 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
12047 $this->_sync_plans();
12048 }
12049
12050 foreach ( $this->_plans as $plan ) {
12051 if ( $name == $plan->name ) {
12052 return $plan;
12053 }
12054 }
12055
12056 return false;
12057 }
12058
12059 /**
12060 * Sync local licenses with remote server.
12061 *
12062 * @author Vova Feldman (@svovaf)
12063 * @since 1.0.6
12064 *
12065 * @param number|bool $site_license_id
12066 * @param number|null $blog_id
12067 *
12068 * @return FS_Plugin_License[]|object
12069 */
12070 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
12071 $this->_logger->entrance();
12072
12073 $is_network_admin = fs_is_network_admin();
12074
12075 if ( $is_network_admin && is_null( $blog_id ) ) {
12076 $all_licenses = self::get_all_licenses( $this->_module_id );
12077 } else {
12078 $all_licenses = $this->get_user_licenses( $this->_user->id );
12079 }
12080
12081 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
12082
12083 $all_licenses_map = array();
12084 foreach ( $all_licenses as $license ) {
12085 $all_licenses_map[ $license->id ] = true;
12086 }
12087
12088 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
12089
12090 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
12091 $licenses_map = array();
12092 foreach ( $licenses as $license ) {
12093 $licenses_map[ $license->id ] = true;
12094 }
12095
12096 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
12097 // foreach ( $license_ids_to_keep as $license_id ) {
12098 // if ( isset( $licenses_map[ $license_id ] ) ) {
12099 // continue;
12100 // }
12101 //
12102 // $missing_license = self::_get_license_by_id( $license_id, false );
12103 // if ( is_object( $missing_license ) ) {
12104 // $licenses[] = $missing_license;
12105 // $licenses_map[ $missing_license->id ] = true;
12106 // }
12107 // }
12108
12109 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
12110
12111 foreach ( $user_license_ids as $key => $license_id ) {
12112 if ( ! isset( $licenses_map[ $license_id ] ) ) {
12113 // Remove access to licenses that no longer exist.
12114 unset( $user_license_ids[ $key ] );
12115 }
12116 }
12117
12118 if ( ! empty( $user_license_ids ) ) {
12119 foreach ( $licenses_map as $license_id => $value ) {
12120 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
12121 // Associate new licenses with the user who triggered the license syncing.
12122 $user_license_ids[] = $license_id;
12123 }
12124 }
12125
12126 $user_license_ids = array_unique( $user_license_ids );
12127 } else {
12128 $user_license_ids = array_keys( $licenses_map );
12129 }
12130
12131 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
12132 $user_licenses = array();
12133 foreach ( $licenses as $license ) {
12134 if ( ! in_array( $license->id, $user_license_ids ) ) {
12135 continue;
12136 }
12137
12138 $user_licenses[] = $license;
12139 }
12140
12141 $this->_licenses = $user_licenses;
12142 } else {
12143 $this->_licenses = $licenses;
12144 }
12145
12146 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
12147
12148 $this->_store_licenses( true, $this->_module_id, $licenses );
12149 }
12150
12151 // Update current license.
12152 if ( is_object( $this->_license ) ) {
12153 $license = $this->_get_license_by_id( $this->_license->id );
12154
12155 if ( is_object( $license ) ) {
12156 /**
12157 * `$license` can be `false` in case a user change action has just been completed and this method
12158 * has synced the `$this->_licenses` collection for the new user. In this case, the
12159 * `$this->_licenses` collection may have only the newly activated license that is associated with
12160 * the new user. `set_license` will eventually be called in the same request by the logic that
12161 * follows outside this method which will detect that the install's license has been updated, and
12162 * then `_update_site_license` will be called which in turn will call `set_license`.
12163 *
12164 * @author Leo Fajardo (@leorw)
12165 * @since 2.3.2
12166 */
12167 $this->set_license( $license );
12168 }
12169 }
12170
12171 return $this->_licenses;
12172 }
12173
12174 /**
12175 * @author Vova Feldman (@svovaf)
12176 * @since 1.0.5
12177 *
12178 * @param number $id
12179 * @param bool $sync_licenses
12180 *
12181 * @return FS_Plugin_License|false
12182 */
12183 function _get_license_by_id( $id, $sync_licenses = true ) {
12184 $this->_logger->entrance();
12185
12186 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
12187 return false;
12188 }
12189
12190 /**
12191 * When running from the network level admin and opted-in from the network,
12192 * check if the license exists in the network user licenses collection.
12193 *
12194 * @author Vova Feldman (@svovaf)
12195 * @since 2.0.0
12196 */
12197 if ( fs_is_network_admin() &&
12198 $this->is_network_registered() &&
12199 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
12200 ) {
12201 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
12202
12203 foreach ( $licenses as $license ) {
12204 if ( $id == $license->id ) {
12205 return $license;
12206 }
12207 }
12208 }
12209
12210 if ( ! $this->has_any_license() && $sync_licenses ) {
12211 $this->_sync_licenses( $id );
12212 }
12213
12214 if ( is_array( $this->_licenses ) ) {
12215 foreach ( $this->_licenses as $license ) {
12216 if ( $id == $license->id ) {
12217 return $license;
12218 }
12219 }
12220 }
12221
12222 return false;
12223 }
12224
12225 /**
12226 * Get license by ID. Unlike _get_license_by_id(), this method only checks the local storage and return any license, whether it's associated with the current context user/install or not.
12227 *
12228 * @author Vova Feldman (@svovaf)
12229 * @since 2.0.0
12230 *
12231 * @param number $id
12232 *
12233 * @return FS_Plugin_License
12234 */
12235 private function get_license_by_id( $id ) {
12236 $licenses = self::get_all_licenses( $this->_module_id );
12237
12238 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
12239 foreach ( $licenses as $license ) {
12240 if ( $id == $license->id ) {
12241 return $license;
12242 }
12243 }
12244 }
12245
12246 return null;
12247 }
12248
12249 /**
12250 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
12251 *
12252 * @author Vova Feldman (@svovaf)
12253 * @since 2.0.0
12254 *
12255 * @return \FS_Plugin_License|mixed
12256 */
12257 private function sync_site_license() {
12258 $api = $this->get_api_user_scope();
12259
12260 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
12261
12262 if ( ! $this->is_api_result_entity( $result ) ) {
12263 return $result;
12264 }
12265
12266 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
12267 $this->_store_licenses();
12268
12269 return $license;
12270 }
12271
12272 /**
12273 * Get all user's available licenses for the current module.
12274 *
12275 * @author Vova Feldman (@svovaf)
12276 * @since 2.0.0
12277 *
12278 * @param number $user_id
12279 *
12280 * @return FS_Plugin_License[]
12281 */
12282 private function get_user_licenses( $user_id ) {
12283 $all_licenses = self::get_all_licenses( $this->_module_id );
12284 if ( empty( $all_licenses ) ) {
12285 return array();
12286 }
12287
12288 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
12289 if ( empty( $user_license_ids ) ) {
12290 return array();
12291 }
12292
12293 $licenses = array();
12294 foreach ( $all_licenses as $license ) {
12295 if ( in_array( $license->id, $user_license_ids ) ) {
12296 $licenses[] = $license;
12297 }
12298 }
12299
12300 return $licenses;
12301 }
12302
12303 /**
12304 * Checks if the context license is network activated except on the given blog ID.
12305 *
12306 * @author Vova Feldman (@svovaf)
12307 * @since 2.0.0
12308 *
12309 * @param int $except_blog_id
12310 *
12311 * @return bool
12312 */
12313 private function is_license_network_active( $except_blog_id = 0 ) {
12314 $this->_logger->entrance();
12315
12316 if ( ! is_object( $this->_license ) ) {
12317 return false;
12318 }
12319
12320 $sites = self::get_sites();
12321
12322 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
12323 // There are more sites than the number of activations, so license cannot be network activated.
12324 return false;
12325 }
12326
12327 foreach ( $sites as $site ) {
12328 $blog_id = self::get_site_blog_id( $site );
12329
12330 if ( $except_blog_id == $blog_id ) {
12331 // Skip excluded blog.
12332 continue;
12333 }
12334
12335 $install = $this->get_install_by_blog_id( $blog_id );
12336
12337 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
12338 return false;
12339 }
12340 }
12341
12342 return true;
12343 }
12344
12345 /**
12346 * Checks if license can be activated on all the network sites (opted-in or skipped) that are not yet associated with a license. If possible, try to make the activation, if not return false.
12347 *
12348 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
12349 *
12350 * @author Vova Feldman (@svovaf)
12351 * @since 2.0.0
12352 *
12353 * @param \FS_User $user
12354 * @param \FS_Plugin_License $license
12355 *
12356 * @return bool
12357 */
12358 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
12359 $this->_logger->entrance();
12360
12361 $result = $this->can_activate_license_on_network( $license );
12362
12363 if ( false === $result ) {
12364 return false;
12365 }
12366
12367 $installs_without_license = $result['installs'];
12368 if ( ! empty( $installs_without_license ) ) {
12369 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
12370 }
12371
12372 $disconnected_site_ids = $result['sites'];
12373 if ( ! empty( $disconnected_site_ids ) ) {
12374 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
12375 }
12376
12377 $this->link_license_2_user( $license->id, $user->id );
12378
12379 // Sync license after activations.
12380 $license->activated += $result['production_count'];
12381 $license->activated_local += $result['localhost_count'];
12382
12383 // $this->_store_licenses()
12384
12385 return true;
12386 }
12387
12388 /**
12389 * Checks if the given license can be activated on the whole network.
12390 *
12391 * @author Vova Feldman (@svovaf)
12392 * @since 2.0.0
12393 *
12394 * @param \FS_Plugin_License $license
12395 *
12396 * @return false|array {
12397 * @type array[int]FS_Site $installs Blog ID to install map.
12398 * @type int[] $sites Non-connected blog IDs.
12399 * @type int $production_count Production sites count.
12400 * @type int $localhost_count Production sites count.
12401 * }
12402 */
12403 private function can_activate_license_on_network( FS_Plugin_License $license ) {
12404 $sites = self::get_sites();
12405
12406 $production_count = 0;
12407 $localhost_count = 0;
12408
12409 $installs_without_license = array();
12410 $disconnected_site_ids = array();
12411
12412 foreach ( $sites as $site ) {
12413 $blog_id = self::get_site_blog_id( $site );
12414 $install = $this->get_install_by_blog_id( $blog_id );
12415
12416 if ( is_object( $install ) ) {
12417 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
12418 // License already activated on the install.
12419 continue;
12420 }
12421
12422 $url = $install->url;
12423
12424 $installs_without_license[ $blog_id ] = $install;
12425 } else {
12426 $url = is_object( $site ) ?
12427 $site->siteurl :
12428 self::get_unfiltered_site_url( $blog_id );
12429
12430 $disconnected_site_ids[] = $blog_id;
12431 }
12432
12433 if ( FS_Site::is_localhost_by_address( $url ) ) {
12434 $localhost_count ++;
12435 } else {
12436 $production_count ++;
12437 }
12438 }
12439
12440 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12441 return false;
12442 }
12443
12444 return array(
12445 'installs' => $installs_without_license,
12446 'sites' => $disconnected_site_ids,
12447 'production_count' => $production_count,
12448 'localhost_count' => $localhost_count,
12449 );
12450 }
12451
12452 /**
12453 * Activate a given license on a collection of installs.
12454 *
12455 * @author Vova Feldman (@svovaf)
12456 * @since 2.0.0
12457 *
12458 * @param \FS_User $user
12459 * @param string $license_key
12460 * @param array $blog_2_install_map {
12461 * @key int Blog ID.
12462 * @value FS_Site Blog's associated install.
12463 * }
12464 *
12465 * @return mixed|true
12466 */
12467 private function activate_license_on_many_installs(
12468 FS_User $user,
12469 $license_key,
12470 array $blog_2_install_map
12471 ) {
12472 $params = array(
12473 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12474 );
12475
12476 $install_2_blog_map = array();
12477 foreach ( $blog_2_install_map as $blog_id => $install ) {
12478 $params[] = array( 'id' => $install->id, 'url' => $install->url );
12479
12480 $install_2_blog_map[ $install->id ] = $blog_id;
12481 }
12482
12483 $result = $this->get_api_user_scope_by_user( $user )->call(
12484 "plugins/{$this->_plugin->id}/installs.json",
12485 'PUT',
12486 $params
12487 );
12488
12489 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12490 return $result;
12491 }
12492
12493 foreach ( $result->installs as $r_install ) {
12494 $install = new FS_Site( $r_install );
12495 $install->is_disconnected = false;
12496
12497 // Update install.
12498 $this->_store_site(
12499 true,
12500 $install_2_blog_map[ $r_install->id ],
12501 $install
12502 );
12503 }
12504
12505 return true;
12506 }
12507
12508 /**
12509 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12510 *
12511 * @author Vova Feldman (@svovaf)
12512 * @since 2.3.1
12513 *
12514 * @param \FS_User $user
12515 * @param string $license_key
12516 *
12517 * @return true|mixed True if successful, otherwise, the API result.
12518 */
12519 private function activate_license_on_site( FS_User $user, $license_key ) {
12520 return $this->activate_license_on_many_sites( $user, $license_key );
12521 }
12522
12523 /**
12524 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12525 *
12526 * @author Vova Feldman (@svovaf)
12527 * @since 2.0.0
12528 *
12529 * @param \FS_User $user
12530 * @param string $license_key
12531 * @param int[] $site_ids
12532 *
12533 * @return true|mixed True if successful, otherwise, the API result.
12534 */
12535 private function activate_license_on_many_sites(
12536 FS_User $user,
12537 $license_key,
12538 array $site_ids = array()
12539 ) {
12540 $sites = array();
12541 foreach ( $site_ids as $site_id ) {
12542 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12543 }
12544
12545 // Install the plugin.
12546 $result = $this->create_installs_with_user(
12547 $user,
12548 $license_key,
12549 false,
12550 $sites,
12551 false,
12552 true
12553 );
12554
12555 if ( ! $this->is_api_result_entity( $result ) &&
12556 ! $this->is_api_result_object( $result, 'installs' )
12557 ) {
12558 return $result;
12559 }
12560
12561 $installs = array();
12562
12563 if ( $this->is_api_result_entity( $result ) ) {
12564 $install = new FS_Site( $result );
12565
12566 $this->_user = $user;
12567
12568 $this->_store_site( true, null, $install );
12569
12570 $this->_site = $install;
12571
12572 $this->reset_anonymous_mode();
12573 } else {
12574 foreach ( $result->installs as $install ) {
12575 $installs[] = new FS_Site( $install );
12576 }
12577
12578 // Map site addresses to their blog IDs.
12579 $address_to_blog_map = $this->get_address_to_blog_map();
12580
12581 $first_blog_id = null;
12582
12583 foreach ( $installs as $install ) {
12584 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12585 $blog_id = $address_to_blog_map[ $address ];
12586
12587 $this->_store_site( true, $blog_id, $install );
12588
12589 $this->reset_anonymous_mode( $blog_id );
12590
12591 if ( is_null( $first_blog_id ) ) {
12592 $first_blog_id = $blog_id;
12593 }
12594 }
12595
12596 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12597 $this->_storage->network_install_blog_id = $first_blog_id;
12598 }
12599 }
12600
12601 return true;
12602 }
12603
12604 /**
12605 * Sync site's license with user licenses.
12606 *
12607 * @author Vova Feldman (@svovaf)
12608 * @since 1.0.6
12609 *
12610 * @param FS_Plugin_License|null $new_license
12611 *
12612 * @return FS_Plugin_License|null
12613 */
12614 function _update_site_license( $new_license ) {
12615 $this->_logger->entrance();
12616
12617 /**
12618 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12619 * accordingly so that it will also handle the case when an ownership change is done via license
12620 * activation.
12621 *
12622 * @author Leo Fajardo (@leorw)
12623 * @since 2.3.2
12624 */
12625 $this->set_license( $new_license );
12626
12627 if ( ! is_object( $new_license ) ) {
12628 $this->_site->license_id = null;
12629 $this->_sync_site_subscription( null );
12630
12631 return $this->_license;
12632 }
12633
12634 $this->_site->license_id = $this->_license->id;
12635
12636 if ( ! is_array( $this->_licenses ) ) {
12637 $this->_licenses = array();
12638 }
12639
12640 $is_license_found = false;
12641 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12642 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12643 $this->_licenses[ $i ] = $new_license;
12644
12645 $is_license_found = true;
12646 break;
12647 }
12648 }
12649
12650 // If new license just append.
12651 if ( ! $is_license_found ) {
12652 $this->_licenses[] = $new_license;
12653 }
12654
12655 $this->_sync_site_subscription( $new_license );
12656
12657 return $this->_license;
12658 }
12659
12660 /**
12661 * @author Vova Feldman (@svovaf)
12662 * @since 2.3.1
12663 *
12664 * @param \FS_Plugin_License $license
12665 */
12666 private function set_license( FS_Plugin_License $license = null ) {
12667 $this->_license = $license;
12668
12669 $this->maybe_update_whitelabel_flag( $license );
12670 }
12671
12672 /**
12673 * @author Leo Fajardo (@leorw)
12674 * @since 2.3.1
12675 *
12676 * @param FS_Plugin_License $license
12677 */
12678 private function maybe_update_whitelabel_flag( $license ) {
12679 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12680 $this->_storage->is_whitelabeled :
12681 false;
12682
12683 if ( is_object( $license ) ) {
12684 $license_user = self::_get_user_by_id( $license->user_id );
12685
12686 if ( ! is_object( $license_user ) ) {
12687 // If foreign license, do not update the `is_whitelabeled` flag.
12688 return;
12689 }
12690
12691 if ( $this->is_addon() ) {
12692 /**
12693 * Store the last license data to the parent's storage since it's needed only when showing the
12694 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12695 * iterate over the add-ons just to get the last license data.
12696 */
12697 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12698 } else {
12699 $this->store_last_activated_license_data( $license );
12700 }
12701
12702 if ( $license->is_whitelabeled ) {
12703 // Activated a developer license, data should be hidden.
12704 $is_whitelabeled = true;
12705 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12706 // The account owner activated a regular license key, no need to hide the data.
12707 $is_whitelabeled = false;
12708 }
12709 }
12710
12711 $this->_storage->is_whitelabeled = $is_whitelabeled;
12712
12713 // Reset the whitelabeled status after update.
12714 $this->is_whitelabeled = null;
12715 if ( $this->is_addon() ) {
12716 $parent_fs = $this->get_parent_instance();
12717
12718 if ( is_object( $parent_fs ) ) {
12719 $parent_fs->is_whitelabeled = null;
12720 }
12721 }
12722 }
12723
12724 /**
12725 * @author Leo Fajardo (@leorw)
12726 * @since 2.3.1
12727 *
12728 * @param FS_Plugin_License $license
12729 * @param FS_User $license_user
12730 */
12731 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12732 if ( ! is_object( $license_user ) ) {
12733 $this->_storage->last_license_key = md5( $license->secret_key );
12734 $this->_storage->last_license_user_id = null;
12735 } else {
12736 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12737 $this->_storage->last_license_user_id = $license_user->id;
12738 }
12739 }
12740
12741 /**
12742 * @author Leo Fajardo (@leorw)
12743 * @since 2.3.1
12744 *
12745 * @param bool $ignore_data_debug_mode
12746 *
12747 * @return bool
12748 */
12749 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12750 if ( true !== $this->_storage->is_whitelabeled ) {
12751 return false;
12752 } else if ( $ignore_data_debug_mode ) {
12753 return true;
12754 }
12755
12756 $fs = $this->is_addon() ?
12757 $this->get_parent_instance() :
12758 $this;
12759
12760 return ! $fs->is_data_debug_mode();
12761 }
12762
12763 /**
12764 * @author Leo Fajardo (@leorw)
12765 * @since 2.3.1
12766 *
12767 * @return number
12768 */
12769 function get_last_license_user_id() {
12770 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12771 $this->_storage->last_license_user_id :
12772 null;
12773 }
12774
12775 /**
12776 * @author Leo Fajardo (@leorw)
12777 * @since 2.3.1
12778 *
12779 * @param int $blog_id
12780 * @param bool $ignore_data_debug_mode
12781 *
12782 * @return bool
12783 */
12784 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12785 if ( ! is_null( $blog_id ) ) {
12786 $this->switch_to_blog( $blog_id );
12787 }
12788
12789 if ( ! is_null( $this->is_whitelabeled ) ) {
12790 $is_whitelabeled = $this->is_whitelabeled;
12791 } else {
12792 $is_whitelabeled = false;
12793
12794 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12795
12796 if ( ! $this->has_addons() ) {
12797 $is_whitelabeled = $is_whitelabeled_flag;
12798 } else if ( $is_whitelabeled_flag ) {
12799 $is_whitelabeled = true;
12800 } else {
12801 if ( $this->is_registered() || $this->is_premium() ) {
12802 $addon_ids = $this->get_updated_account_addons();
12803 } else {
12804 $addons = self::get_all_addons();
12805
12806 $plugin_addons = isset( $addons[ $this->_plugin->id ] ) ?
12807 $addons[ $this->_plugin->id ] :
12808 array();
12809
12810 $addon_ids = array();
12811 foreach ( $plugin_addons as $addon ) {
12812 $addon_ids[] = $addon->id;
12813 }
12814 }
12815
12816 $installed_addons = $this->get_installed_addons();
12817 foreach ( $installed_addons as $fs_addon ) {
12818 $addon_ids[] = $fs_addon->get_id();
12819 }
12820
12821 if ( ! empty( $addon_ids ) ) {
12822 $addon_ids = array_unique( $addon_ids );
12823
12824 $is_network_level = (
12825 fs_is_network_admin() &&
12826 $this->is_network_active()
12827 );
12828
12829 foreach ( $addon_ids as $addon_id ) {
12830 $addon = $this->get_addon( $addon_id );
12831
12832 if ( ! is_object( $addon ) ) {
12833 continue;
12834 }
12835
12836 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12837 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12838 self::get_addon_instance( $addon_id ) :
12839 null;
12840
12841 $was_addon_network_activated = false;
12842
12843 if ( is_object( $fs_addon ) ) {
12844 $was_addon_network_activated = $fs_addon->is_network_active();
12845 } else if ( $is_network_level ) {
12846 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12847 }
12848
12849 $network_delegated_connection = (
12850 $was_addon_network_activated &&
12851 $addon_storage->get( 'is_delegated_connection', false, true )
12852 );
12853
12854 if (
12855 $is_network_level &&
12856 ( ! $was_addon_network_activated || $network_delegated_connection )
12857 ) {
12858 $sites = self::get_sites();
12859
12860 /**
12861 * If in network admin area and the add-on was not network-activated or network-activated
12862 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12863 */
12864 foreach ( $sites as $site ) {
12865 $site_info = $this->get_site_info( $site );
12866
12867 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12868 $is_whitelabeled = true;
12869 break;
12870 }
12871 }
12872
12873 if ( $is_whitelabeled ) {
12874 break;
12875 }
12876 } else {
12877 /**
12878 * This will be executed when any of the following is met:
12879 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12880 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12881 * 3. Add-on was not network-activated and in site admin area.
12882 */
12883 if ( true === $addon_storage->is_whitelabeled ) {
12884 $is_whitelabeled = true;
12885 break;
12886 }
12887 }
12888 }
12889 }
12890 }
12891
12892 $this->is_whitelabeled = $is_whitelabeled;
12893
12894 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12895 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12896 }
12897
12898 if ( ! is_null( $blog_id ) ) {
12899 $this->restore_current_blog();
12900 }
12901 }
12902
12903 return (
12904 $is_whitelabeled &&
12905 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12906 );
12907 }
12908
12909 /**
12910 * Sync site's subscription.
12911 *
12912 * @author Vova Feldman (@svovaf)
12913 * @since 1.0.9
12914 *
12915 * @param FS_Plugin_License|null $license
12916 *
12917 * @return bool|\FS_Subscription
12918 */
12919 private function _sync_site_subscription( $license ) {
12920 if ( ! is_object( $license ) ) {
12921 $this->delete_unused_subscriptions();
12922
12923 return false;
12924 }
12925
12926 // Load subscription details if not lifetime.
12927 $subscription = $license->is_lifetime() ?
12928 false :
12929 $this->_fetch_site_license_subscription();
12930
12931 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12932 $this->store_subscription( $subscription );
12933 } else {
12934 $this->delete_unused_subscriptions();
12935 }
12936
12937 return $subscription;
12938 }
12939
12940 /**
12941 * @author Vova Feldman (@svovaf)
12942 * @since 1.0.6
12943 *
12944 * @return bool|\FS_Plugin_License
12945 */
12946 function _get_license() {
12947 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12948 return $this->_license;
12949 }
12950
12951 return $this->_get_available_premium_license();
12952 }
12953
12954 /**
12955 * @param number $license_id
12956 *
12957 * @return null|\FS_Subscription
12958 */
12959 function _get_subscription( $license_id ) {
12960 if ( ! isset( $this->_storage->subscriptions ) ||
12961 empty( $this->_storage->subscriptions )
12962 ) {
12963 return null;
12964 }
12965
12966 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12967 if ( $subscription->license_id == $license_id ) {
12968 return $subscription;
12969 }
12970 }
12971
12972 return null;
12973 }
12974
12975 /**
12976 * @author Leo Fajardo (@leorw)
12977 * @since 2.0.0
12978 *
12979 * @param FS_Subscription $subscription
12980 */
12981 function store_subscription( FS_Subscription $subscription ) {
12982 if ( ! isset( $this->_storage->subscriptions ) ) {
12983 $this->_storage->subscriptions = array();
12984 }
12985
12986 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12987 $this->_storage->subscriptions = array( $subscription );
12988
12989 return;
12990 }
12991
12992 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
12993
12994 $updated_subscription = false;
12995 foreach ( $subscriptions as $key => $existing_subscription ) {
12996 if ( $existing_subscription->id == $subscription->id ) {
12997 $subscriptions[ $key ] = $subscription;
12998 $updated_subscription = true;
12999 break;
13000 }
13001 }
13002
13003 if ( ! $updated_subscription ) {
13004 $subscriptions[] = $subscription;
13005 }
13006
13007 $this->_storage->subscriptions = $subscriptions;
13008 }
13009
13010 /**
13011 * @author Leo Fajardo (@leorw)
13012 * @since 2.0.0
13013 */
13014 function delete_unused_subscriptions() {
13015 if ( ! isset( $this->_storage->subscriptions ) ||
13016 empty( $this->_storage->subscriptions ) ||
13017 // Clean up only if there are already at least 3 subscriptions.
13018 ( count( $this->_storage->subscriptions ) < 3 )
13019 ) {
13020 return;
13021 }
13022
13023 if ( ! is_multisite() ) {
13024 // If not multisite, there should only be 1 subscription, so just clear the array.
13025 $this->_storage->subscriptions = array();
13026
13027 return;
13028 }
13029
13030 $subscriptions_to_keep_by_license_id_map = array();
13031 $sites = self::get_sites();
13032 foreach ( $sites as $site ) {
13033 $blog_id = self::get_site_blog_id( $site );
13034 $install = $this->get_install_by_blog_id( $blog_id );
13035
13036 if ( ! is_object( $install ) ||
13037 ! FS_Plugin_License::is_valid_id( $install->license_id )
13038 ) {
13039 continue;
13040 }
13041
13042 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
13043 }
13044
13045 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
13046 $this->_storage->subscriptions = array();
13047
13048 return;
13049 }
13050
13051 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
13052 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
13053 unset( $this->_storage->subscriptions[ $key ] );
13054 }
13055 }
13056 }
13057
13058 /**
13059 * @author Vova Feldman (@svovaf)
13060 * @since 1.0.2
13061 *
13062 * @param string $plan Plan name
13063 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
13064 *
13065 * @return bool
13066 */
13067 function is_plan( $plan, $exact = false ) {
13068 $this->_logger->entrance();
13069
13070 if ( ! $this->is_registered() ) {
13071 return false;
13072 }
13073
13074 $plan = strtolower( $plan );
13075
13076 $current_plan_name = $this->get_plan_name();
13077
13078 if ( $current_plan_name === $plan ) {
13079 // Exact plan.
13080 return true;
13081 } else if ( $exact ) {
13082 // Required exact, but plans are different.
13083 return false;
13084 }
13085
13086 $current_plan_order = - 1;
13087 $required_plan_order = PHP_INT_MAX;
13088 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
13089 if ( $plan === $this->_plans[ $i ]->name ) {
13090 $required_plan_order = $i;
13091 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
13092 $current_plan_order = $i;
13093 }
13094 }
13095
13096 return ( $current_plan_order > $required_plan_order );
13097 }
13098
13099 /**
13100 * Check if module has only one plan.
13101 *
13102 * @author Vova Feldman (@svovaf)
13103 * @since 1.2.1.7
13104 *
13105 * @param bool $double_check In some cases developers prefer to release their paid offering as premium-only, even though there is a free version. For those cases, looking at the 'is_premium_only' value isn't enough because the result will return false even when the product has only signle paid plan.
13106 *
13107 * @return bool
13108 */
13109 function is_single_plan( $double_check = false ) {
13110 $this->_logger->entrance();
13111
13112 if ( ! $this->is_registered() ||
13113 ! is_array( $this->_plans ) ||
13114 0 === count( $this->_plans )
13115 ) {
13116 return true;
13117 }
13118
13119 $has_free_plan = $this->has_free_plan();
13120
13121 if ( ! $has_free_plan && $double_check ) {
13122 foreach ( $this->_plans as $plan ) {
13123 if ( $plan->is_free() ) {
13124 $has_free_plan = true;
13125 break;
13126 }
13127 }
13128 }
13129
13130 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
13131 }
13132
13133 /**
13134 * Check if plan based on trial. If not in trial mode, should return false.
13135 *
13136 * @since 1.0.9
13137 *
13138 * @param string $plan Plan name
13139 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
13140 *
13141 * @return bool
13142 */
13143 function is_trial_plan( $plan, $exact = false ) {
13144 $this->_logger->entrance();
13145
13146 if ( ! $this->is_registered() ) {
13147 return false;
13148 }
13149
13150 if ( ! $this->is_trial() ) {
13151 return false;
13152 }
13153
13154 $trial_plan = $this->get_trial_plan();
13155
13156 if ( $trial_plan->name === $plan ) {
13157 // Exact plan.
13158 return true;
13159 } else if ( $exact ) {
13160 // Required exact, but plans are different.
13161 return false;
13162 }
13163
13164 $current_plan_order = - 1;
13165 $required_plan_order = - 1;
13166 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
13167 if ( $plan === $this->_plans[ $i ]->name ) {
13168 $required_plan_order = $i;
13169 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
13170 $current_plan_order = $i;
13171 }
13172 }
13173
13174 return ( $current_plan_order > $required_plan_order );
13175 }
13176
13177 /**
13178 * Check if plugin has any paid plans.
13179 *
13180 * @author Vova Feldman (@svovaf)
13181 * @since 1.0.7
13182 *
13183 * @return bool
13184 */
13185 function has_paid_plan() {
13186 return $this->_has_paid_plans ||
13187 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
13188 }
13189
13190 /**
13191 * Check if plugin has any plan with a trail.
13192 *
13193 * @author Vova Feldman (@svovaf)
13194 * @since 1.0.9
13195 *
13196 * @return bool
13197 */
13198 function has_trial_plan() {
13199 /**
13200 * @author Vova Feldman(@svovaf)
13201 * @since 1.2.1.5
13202 *
13203 * Allow setting a trial from the SDK without calling the API.
13204 * But, if the user did opt-in, continue using the real data from the API.
13205 */
13206 if ( $this->_trial_days >= 0 ) {
13207 return true;
13208 }
13209
13210 return $this->_storage->get( 'has_trial_plan', false );
13211 }
13212
13213 /**
13214 * Check if plugin has any free plan, or is it premium only.
13215 *
13216 * Note: If no plans configured, assume plugin is free.
13217 *
13218 * @author Vova Feldman (@svovaf)
13219 * @since 1.0.7
13220 *
13221 * @return bool
13222 */
13223 function has_free_plan() {
13224 return ! $this->is_only_premium();
13225 }
13226
13227 /**
13228 * Displays a license activation dialog box when the user clicks on the "Activate License"
13229 * or "Change License" link on the plugins
13230 * page.
13231 *
13232 * @author Leo Fajardo (@leorw)
13233 * @since 1.1.9
13234 */
13235 function _add_license_activation_dialog_box() {
13236 $vars = array(
13237 'id' => $this->_module_id,
13238 );
13239
13240 fs_require_template( 'forms/license-activation.php', $vars );
13241 fs_require_template( 'forms/resend-key.php', $vars );
13242 }
13243
13244 /**
13245 * Displays an email address update dialog box when the user clicks on the email address "Edit" button on the "Account" page.
13246 *
13247 * @author Leo Fajardo (@leorw)
13248 * @since 2.5.0
13249 */
13250 function _add_email_address_update_dialog_box() {
13251 $vars = array( 'id' => $this->_module_id );
13252
13253 fs_require_template( 'forms/email-address-update.php', $vars );
13254 }
13255
13256 /**
13257 * @author Leo Fajardo (@leorw)
13258 * @since 2.5.0
13259 */
13260 function _add_email_address_update_option() {
13261 if ( ! $this->should_handle_user_change() ) {
13262 return;
13263 }
13264
13265 // Add email address update AJAX handler.
13266 $this->add_ajax_action( 'update_email_address', array( &$this, '_email_address_update_ajax_handler' ) );
13267 }
13268
13269 /**
13270 * @author Leo Fajardo (@leorw)
13271 * @since 2.5.0
13272 */
13273 function _email_address_update_ajax_handler() {
13274 $this->check_ajax_referer( 'update_email_address' );
13275
13276 $new_email_address = fs_request_get( 'email_address' );
13277 $transfer_type = fs_request_get( 'transfer_type' );
13278
13279 $result = $this->update_email( $new_email_address );
13280
13281 if ( ! FS_Api::is_api_error( $result ) ) {
13282 self::shoot_ajax_success();
13283 }
13284
13285 $error = '';
13286
13287 if ( FS_Api::is_api_error_object( $result ) ) {
13288 switch ( $result->error->code ) {
13289 case 'user_exist':
13290 case 'account_verification_required':
13291 $error = array(
13292 'code' => 'change_ownership',
13293 'url' => $this->get_account_url( 'change_owner', array(
13294 'state' => 'init',
13295 'candidate_email' => $new_email_address,
13296 'transfer_type' => $transfer_type,
13297 ) ),
13298 );
13299
13300 break;
13301 }
13302 }
13303
13304 if ( empty( $error ) ) {
13305 $error = is_object( $result ) ?
13306 var_export( $result->error, true ) :
13307 $result;
13308 }
13309
13310 self::shoot_ajax_failure( $error );
13311 }
13312
13313 /**
13314 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
13315 *
13316 * @author Leo Fajardo (@leorw)
13317 * @since 2.3.2
13318 *
13319 * @return number[]
13320 */
13321 function get_installs_ids_with_foreign_licenses() {
13322 $installs = array();
13323
13324 if (
13325 is_object( $this->_license ) &&
13326 $this->_site->user_id != $this->_license->user_id
13327 ) {
13328 $installs[] = $this->_site->id;
13329 }
13330
13331 /**
13332 * Also try to get foreign licenses for the context product's add-ons.
13333 */
13334 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
13335
13336 foreach ( $installs_by_slug_map as $slug => $install_info ) {
13337 if ( $slug == $this->get_slug() ) {
13338 continue;
13339 }
13340
13341 $install = $install_info['install'];
13342 $license = $install_info['license'];
13343
13344 if (
13345 is_object( $license ) &&
13346 $install->user_id != $license->user_id
13347 ) {
13348 $installs[] = $install->id;
13349 }
13350 }
13351
13352 return $installs;
13353 }
13354
13355 /**
13356 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
13357 *
13358 * @author Leo Fajardo (@leorw)
13359 * @since 2.3.2
13360 *
13361 * @param number[] $install_ids
13362 */
13363 function _add_user_change_dialog_box( $install_ids ) {
13364 $vars = array(
13365 'id' => $this->_module_id,
13366 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
13367 );
13368
13369 fs_require_template( 'forms/user-change.php', $vars );
13370 }
13371
13372 /**
13373 * @author Leo Fajardo (@leorw)
13374 * @since 2.3.1
13375 */
13376 function _add_data_debug_mode_dialog_box() {
13377 $vars = array(
13378 'id' => $this->_module_id,
13379 );
13380
13381 fs_require_template( 'forms/data-debug-mode.php', $vars );
13382 }
13383
13384 /**
13385 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
13386 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
13387 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
13388 * is only activated on a single production site.
13389 *
13390 * @author Leo Fajardo (@leorw)
13391 * @since 2.2.1
13392 *
13393 * @param bool $is_license_deactivation
13394 *
13395 * @return array
13396 */
13397 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
13398 if ( fs_is_network_admin() ) {
13399 // Subscription cancellation dialog box is currently not supported for multisite networks.
13400 return array();
13401 }
13402
13403 if ( $this->is_whitelabeled() ) {
13404 return array();
13405 }
13406
13407 $license = $this->_get_license();
13408
13409 /**
13410 * If the installation is associated with a non-lifetime license, which is either a single-site or only activated on a single production site (or zero), and connected to an active subscription, suggest the customer to cancel the subscription upon deactivation.
13411 *
13412 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
13413 * @since 2.2.1
13414 */
13415 if ( ! is_object( $license ) ||
13416 $license->is_lifetime() ||
13417 ( ! $license->is_single_site() && $license->activated > 1 )
13418 ) {
13419 return array();
13420 }
13421
13422 /**
13423 * @var FS_Subscription $subscription
13424 */
13425 $subscription = $this->_get_subscription( $license->id );
13426 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
13427 return array();
13428 }
13429
13430 return array(
13431 'id' => $this->_module_id,
13432 'license' => $license,
13433 'has_trial' => $this->is_paid_trial(),
13434 'is_license_deactivation' => $is_license_deactivation,
13435 );
13436 }
13437
13438 /**
13439 * @author Leo Fajardo (@leorw)
13440 * @since 2.0.2
13441 */
13442 function _add_premium_version_upgrade_selection_dialog_box() {
13443 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
13444 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
13445 return;
13446 }
13447
13448 $vars = array(
13449 'id' => $this->_module_id,
13450 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
13451 $modules_update->response[ $this->_plugin_basename ]->new_version :
13452 $modules_update->response[ $this->_plugin_basename ]['new_version']
13453 );
13454
13455 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13456 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13457 }
13458
13459 /**
13460 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13461 * page.
13462 *
13463 * @author Leo Fajardo (@leorw)
13464 * @since 1.2.1.5
13465 */
13466 function _add_optout_dialog() {
13467 if ( $this->is_theme() ) {
13468 $vars = null;
13469 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13470 }
13471
13472 $vars = array( 'id' => $this->_module_id );
13473 fs_require_template( 'forms/optout.php', $vars );
13474 }
13475
13476 /**
13477 * Prepare page to include all required UI and logic for the license activation dialog.
13478 *
13479 * @author Vova Feldman (@svovaf)
13480 * @since 1.2.0
13481 */
13482 function _add_license_activation() {
13483 if ( $this->is_migration() ) {
13484 return;
13485 }
13486
13487 if ( ! $this->is_user_admin() ) {
13488 // Only admins can activate a license.
13489 return;
13490 }
13491
13492 if ( ! $this->has_paid_plan() ) {
13493 // Module doesn't have any paid plans.
13494 return;
13495 }
13496
13497 if (
13498 $this->has_premium_version() &&
13499 ! $this->is_premium() &&
13500 /**
13501 * Also handle the case when an upgrade was made using the free version.
13502 *
13503 * @author Leo Fajardo (@leorw)
13504 * @since 2.3.2
13505 */
13506 ! is_object( $this->_get_license() )
13507 ) {
13508 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13509 return;
13510 }
13511
13512 // Add license activation link and AJAX request handler.
13513 if ( self::is_plugins_page() ) {
13514 $is_network_admin = fs_is_network_admin();
13515
13516 if (
13517 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13518 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13519 ) {
13520 if (
13521 $this->is_premium() ||
13522 ( $this->has_paid_plan() && ! $this->has_premium_version() )
13523 ) {
13524 /**
13525 * @since 1.2.0 Add license action link only on plugins page.
13526 */
13527 $this->_add_license_action_link();
13528 }
13529 }
13530 }
13531
13532 // Add license activation AJAX callback.
13533 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13534
13535 // Add resend license AJAX callback.
13536 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13537 }
13538
13539 /**
13540 * Prepares page to include all required UI and logic for the "Change User" dialog.
13541 *
13542 * @author Leo Fajardo (@leorw)
13543 * @since 2.3.2
13544 */
13545 function _add_user_change_option() {
13546 if ( ! $this->should_handle_user_change() ) {
13547 return;
13548 }
13549
13550 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13551
13552 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13553 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13554 return;
13555 }
13556
13557 // Add user change AJAX handler.
13558 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13559 }
13560
13561 /**
13562 * @author Leo Fajardo (@leorw)
13563 * @since 2.3.2
13564 */
13565 function should_handle_user_change() {
13566 if ( ! $this->is_user_admin() ) {
13567 // Only admins can change user.
13568 return false;
13569 }
13570
13571 if ( $this->is_addon() ) {
13572 return false;
13573 }
13574
13575 if ( ! $this->is_registered() ) {
13576 return false;
13577 }
13578
13579 if (
13580 $this->is_network_active() &&
13581 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13582 ) {
13583 // Handle only on site-level "Account" section for now.
13584 return false;
13585 }
13586
13587 return true;
13588 }
13589
13590 /**
13591 * @author Leo Fajardo (@leorw)
13592 * @since 2.0.2
13593 */
13594 function _add_premium_version_upgrade_selection() {
13595 if ( ! $this->is_user_admin() ) {
13596 return;
13597 }
13598
13599 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13600 // This is relevant only to the free versions and premium versions without an active license.
13601 return;
13602 }
13603
13604 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13605 $this->_add_premium_version_upgrade_selection_action();
13606 }
13607 }
13608
13609 /**
13610 * @author Edgar Melkonyan
13611 * @since 2.4.1
13612 *
13613 * @throws Freemius_Exception
13614 */
13615 function _toggle_whitelabel_mode_ajax_handler() {
13616 $this->_logger->entrance();
13617
13618 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13619
13620 if ( ! $this->is_user_admin() ) {
13621 // Only for admins.
13622 self::shoot_ajax_failure();
13623 }
13624
13625 $license = $this->get_api_user_scope()->call(
13626 "/licenses/{$this->_site->license_id}.json",
13627 'put',
13628 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13629 );
13630
13631 if ( ! $this->is_api_result_entity( $license ) ) {
13632 self::shoot_ajax_failure(
13633 FS_Api::is_api_error_object( $license ) ?
13634 $license->error->message :
13635 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13636 );
13637 }
13638
13639 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13640 $this->_store_licenses();
13641
13642 $this->_sync_license();
13643
13644 if ( ! $license->is_whitelabeled ) {
13645 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13646 } else {
13647 $this->_admin_notices->add_sticky(
13648 sprintf(
13649 $this->get_text_inline(
13650 'Your %s license was flagged as white-labeled to hide sensitive information from the WP Admin (e.g. your email, license key, prices, billing address & invoices). If you ever wish to revert it back, you can easily do it through your %s. If this was a mistake you can also %s.',
13651 'license_whitelabeled'
13652 ),
13653 "<strong>{$this->get_plugin_title()}</strong>",
13654 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13655 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13656 ),
13657 'license_whitelabeled'
13658 );
13659 }
13660
13661 self::shoot_ajax_response( array( 'success' => true ) );
13662 }
13663
13664 /**
13665 * @author Leo Fajardo (@leorw)
13666 * @since 2.3.0
13667 */
13668 function _add_beta_mode_update_handler() {
13669 if ( ! $this->is_user_admin() ) {
13670 return;
13671 }
13672
13673 if ( ! $this->is_premium() ) {
13674 return;
13675 }
13676
13677 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13678 }
13679
13680 /**
13681 * @author Leo Fajardo (@leorw)
13682 * @since 2.3.0
13683 */
13684 function _set_beta_mode_ajax_handler() {
13685 $this->_logger->entrance();
13686
13687 $this->check_ajax_referer( 'set_beta_mode' );
13688
13689 if ( ! $this->is_user_admin() ) {
13690 // Only for admins.
13691 self::shoot_ajax_failure();
13692 }
13693
13694 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13695
13696 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13697 self::shoot_ajax_failure();
13698 }
13699
13700 $site = $this->api_site_call(
13701 '',
13702 'put',
13703 array(
13704 'is_beta' => ( 'true' == $is_beta ),
13705 'fields' => 'is_beta'
13706 )
13707 );
13708
13709 if ( ! $this->is_api_result_entity( $site ) ) {
13710 self::shoot_ajax_failure(
13711 FS_Api::is_api_error_object( $site ) ?
13712 $site->error->message :
13713 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13714 );
13715 }
13716
13717 $this->_site->is_beta = $site->is_beta;
13718 $this->_store_site();
13719
13720 self::shoot_ajax_response( array( 'success' => true ) );
13721 }
13722
13723 /**
13724 * License activation WP AJAX handler.
13725 *
13726 * @author Leo Fajardo (@leorw)
13727 * @since 1.1.9
13728 *
13729 * @uses Freemius::activate_license()
13730 */
13731 function _activate_license_ajax_action() {
13732 $this->_logger->entrance();
13733
13734 $this->check_ajax_referer( 'activate_license' );
13735
13736 $license_key = trim( fs_request_get_raw( 'license_key' ) );
13737
13738 if ( empty( $license_key ) ) {
13739 $license_id = trim( fs_request_get_raw( 'license_id' ) );
13740
13741 if ( FS_Plugin_License::is_valid_id( $license_id ) ) {
13742 $license = $this->_get_license_by_id( $license_id, false );
13743
13744 if ( is_object( $license ) ) {
13745 $license_key = $license->secret_key;
13746 }
13747 }
13748 }
13749
13750 if ( empty( $license_key ) ) {
13751 exit;
13752 }
13753
13754 $sites = fs_is_network_admin() ?
13755 fs_request_get( 'sites', array(), 'post' ) :
13756 array();
13757
13758 $result = $this->activate_license(
13759 $license_key,
13760 $sites,
13761 fs_request_get_bool( 'is_marketing_allowed', null ),
13762 fs_request_get( 'blog_id', null ),
13763 fs_request_get( 'module_id', null, 'post' ),
13764 fs_request_get( 'user_id', null ),
13765 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
13766 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null )
13767 );
13768
13769 if (
13770 $result['success'] &&
13771 $this->is_bundle_license_auto_activation_enabled()
13772 ) {
13773 $license = new FS_Plugin_License();
13774 $license->secret_key = $license_key;
13775
13776 $this->maybe_activate_bundle_license( $license, $sites );
13777 }
13778
13779 echo json_encode( $result );
13780
13781 exit;
13782 }
13783
13784 /**
13785 * User change WP AJAX handler.
13786 *
13787 * @author Leo Fajardo (@leorw)
13788 * @since 2.3.2
13789 */
13790 function _user_change_ajax_action() {
13791 $this->_logger->entrance();
13792
13793 $this->check_ajax_referer( 'change_user' );
13794
13795 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13796 $new_user_id = fs_request_get( 'user_id' );
13797
13798 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13799 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13800 }
13801
13802 $params = array();
13803
13804 if ( ! empty( $new_email_address ) ) {
13805 $params['user_email'] = $new_email_address;
13806 } else {
13807 $params['user_id'] = $new_user_id;
13808 }
13809
13810 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13811 $install_ids = array();
13812
13813 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13814 $install_ids[ $slug ] = $install_info['install']->id;
13815 }
13816
13817 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13818
13819 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13820
13821 if ( FS_Api::is_api_error( $install ) ) {
13822 $error = '';
13823
13824 if ( is_object( $install ) ) {
13825 switch ( $install->error->code ) {
13826 case 'user_exist':
13827 $error = (
13828 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13829 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
13830 sprintf( $this->get_text_inline( 'If you would like to give up the ownership of the %s\'s account to %s click the Change Ownership button.', 'user-exist-message_ownership' ), $this->_module_type, '<b>' . $new_email_address . '</b>' ) .
13831 sprintf(
13832 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13833 $this->get_account_url( 'change_owner', array(
13834 'state' => 'init',
13835 'candidate_email' => $new_email_address
13836 ) ),
13837 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
13838 )
13839 );
13840 break;
13841 }
13842 }
13843
13844 if ( empty( $error ) ) {
13845 $error = FS_Api::is_api_error_object( $install ) ?
13846 $install->error->message :
13847 var_export( $install->error, true );
13848 }
13849
13850 self::shoot_ajax_failure( $error );
13851 } else {
13852 if (
13853 // If successful ownership change.
13854 $this->get_user()->id != $install->user_id ||
13855 ! empty( $new_email_address )
13856 ) {
13857 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
13858 }
13859 }
13860
13861 self::shoot_ajax_success();
13862 }
13863
13864 /**
13865 * @author Leo Fajardo (@leorw)
13866 * @since 2.3.2.14
13867 */
13868 function starting_migration() {
13869 if ( ! empty( $this->_storage->license_migration ) ) {
13870 // Do not overwrite the data if already set.
13871 return;
13872 }
13873
13874 $this->_storage->license_migration = array(
13875 'is_migrating' => true,
13876 'start_timestamp' => time()
13877 );
13878 }
13879
13880 /**
13881 * @author Leo Fajardo (@leorw)
13882 * @since 2.3.2.14
13883 */
13884 function is_migration() {
13885 if ( $this->is_addon() ) {
13886 return $this->get_parent_instance()->is_migration();
13887 }
13888
13889 if ( empty( $this->_storage->license_migration ) ) {
13890 return false;
13891 }
13892
13893 if ( ! $this->_storage->license_migration['is_migrating'] ) {
13894 return false;
13895 }
13896
13897 return (
13898 // Return `true` if the migration is within 5 minutes from the starting time.
13899 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
13900 );
13901 }
13902
13903 /**
13904 *
13905 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
13906 *
13907 * @author Vova Feldman (@svovaf)
13908 * @since 2.3.0
13909 *
13910 * @param string $license_key
13911 * @param null|bool $is_marketing_allowed
13912 * @param null|number $plugin_id
13913 * @param array $sites
13914 * @param int $blog_id
13915 *
13916 * @return array {
13917 * @var bool $success
13918 * @var string $error
13919 * @var string $next_page
13920 * }
13921 *
13922 * @uses Freemius::activate_license()
13923 */
13924 function activate_migrated_license(
13925 $license_key,
13926 $is_marketing_allowed = null,
13927 $plugin_id = null,
13928 $sites = array(),
13929 $blog_id = null
13930 ) {
13931 $this->_logger->entrance();
13932
13933 $result = $this->activate_license(
13934 $license_key,
13935 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
13936 $this->get_sites_for_network_level_optin() :
13937 $sites,
13938 $is_marketing_allowed,
13939 $blog_id,
13940 $plugin_id
13941 );
13942
13943 // No need to show the sticky after license activation notice after migrating a license.
13944 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13945
13946 return $result;
13947 }
13948
13949 /**
13950 * @author Leo Fajardo (@leorw)
13951 * @since 2.3.1
13952 *
13953 * @return string
13954 */
13955 function get_pricing_js_path() {
13956 if ( ! isset( $this->_pricing_js_path ) ) {
13957 $pricing_js_path = $this->apply_filters( 'freemius_pricing_js_path', '' );
13958
13959 if ( empty( $pricing_js_path ) ) {
13960 global $fs_active_plugins;
13961
13962 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
13963 if ( $data->plugin_path == $this->get_plugin_basename() ) {
13964 $plugin_or_theme_root_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
13965
13966 $pricing_js_path = $plugin_or_theme_root_dir
13967 . '/'
13968 // The basename will be `plugins`, `themes`, or the basename of a custom plugins or themes directory.
13969 . str_replace( '../' . basename( $plugin_or_theme_root_dir ) . '/', '', $sdk_path )
13970 . '/includes/freemius-pricing/freemius-pricing.js';
13971
13972 break;
13973 }
13974 }
13975 }
13976
13977 $this->_pricing_js_path = $pricing_js_path;
13978 }
13979
13980 return $this->_pricing_js_path;
13981 }
13982
13983 /**
13984 * @author Leo Fajardo (@leorw)
13985 * @since 2.3.1
13986 *
13987 * @return bool
13988 */
13989 function should_use_external_pricing() {
13990 if ( is_null( $this->_use_external_pricing ) ) {
13991 $pricing_js_path = $this->get_pricing_js_path();
13992
13993 $this->_use_external_pricing = ( empty( $pricing_js_path ) || ! file_exists( $pricing_js_path ) );
13994 }
13995
13996 return $this->_use_external_pricing;
13997 }
13998
13999 /**
14000 * The implementation of this method was previously in `_activate_license_ajax_action()`.
14001 *
14002 * @author Vova Feldman (@svovaf)
14003 * @since 2.2.4
14004 * @since 2.0.0 When a super-admin that hasn't connected before is network activating a license and excluding some of the sites for the license activation, go over the unselected sites in the network and if a site is not connected, skipped, nor delegated, if it's a freemium product then just skip the connection for the site, if it's a premium only product, delegate the connection and license activation to the site admin (Vova Feldman @svovaf).
14005 * @param string $license_key
14006 * @param array $sites
14007 * @param null|bool $is_marketing_allowed
14008 * @param null|int $blog_id
14009 * @param null|number $plugin_id
14010 * @param null|number $license_owner_id
14011 * @param bool|null $is_extensions_tracking_allowed
14012 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint.
14013 *
14014 *
14015 * @return array {
14016 * @var bool $success
14017 * @var string $error
14018 * @var string $next_page
14019 * }
14020 */
14021 private function activate_license(
14022 $license_key,
14023 $sites = array(),
14024 $is_marketing_allowed = null,
14025 $blog_id = null,
14026 $plugin_id = null,
14027 $license_owner_id = null,
14028 $is_extensions_tracking_allowed = null,
14029 $is_diagnostic_tracking_allowed = null
14030 ) {
14031 $this->_logger->entrance();
14032
14033 $license_key = trim( $license_key );
14034
14035 $is_network_activation_or_migration = (
14036 fs_is_network_admin() ||
14037 ( ! empty( $sites ) && $this->is_migration() )
14038 );
14039
14040 if ( ! $is_network_activation_or_migration ) {
14041 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
14042 $sites = array();
14043 }
14044
14045 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
14046 $this :
14047 $this->get_addon_instance( $plugin_id );
14048
14049 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
14050 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
14051 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
14052 ) );
14053
14054 $error = false;
14055 $next_page = false;
14056
14057 $has_valid_blog_id = is_numeric( $blog_id );
14058
14059 $user = null;
14060
14061 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
14062 /**
14063 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
14064 *
14065 * @author Vova Feldman (@svovaf)
14066 */
14067 $user = $fs->get_parent_instance()->get_current_or_network_user();
14068 } else if ( $fs->is_registered() ) {
14069 $user = $fs->get_current_or_network_user();
14070 }
14071
14072 if ( $has_valid_blog_id ) {
14073 /**
14074 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
14075 *
14076 * @author Leo Fajardo (@leorw)
14077 */
14078 $fs->switch_to_blog( $blog_id );
14079 }
14080
14081 if ( is_object( $user ) ) {
14082 $result = true;
14083
14084 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
14085 // If no specific blog ID was provided, activate the license for all sites in the network.
14086 $blog_2_install_map = array();
14087 $site_ids = array();
14088
14089 foreach ( $sites as $site ) {
14090 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
14091 continue;
14092 }
14093
14094 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
14095
14096 if ( is_object( $install ) ) {
14097 $blog_2_install_map[ $site['blog_id'] ] = $install;
14098 } else {
14099 $site_ids[] = $site['blog_id'];
14100 }
14101 }
14102
14103 if ( ! empty( $blog_2_install_map ) ) {
14104 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
14105 }
14106
14107 if ( true === $result && ! empty( $site_ids ) ) {
14108 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
14109 }
14110 } else {
14111 if ( $fs->is_registered() ) {
14112 $params = array(
14113 'license_key' => $fs->apply_filters( 'license_key', $license_key )
14114 );
14115
14116 $install_ids = array();
14117
14118 $change_owner = FS_User::is_valid_id( $license_owner_id );
14119
14120 if ( $change_owner ) {
14121 $params['user_id'] = $license_owner_id;
14122
14123 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
14124
14125 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
14126 $install_ids[ $slug ] = $install_info['install']->id;
14127 }
14128
14129 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
14130 }
14131
14132 $api = $fs->get_api_site_scope();
14133
14134 $result = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
14135
14136 if ( ! FS_Api::is_api_error( $result ) ) {
14137 $install = $result;
14138
14139 $fs->reconnect_locally( $has_valid_blog_id );
14140
14141 if (
14142 $change_owner &&
14143 // If successful ownership change.
14144 $fs->get_user()->id != $install->user_id
14145 ) {
14146 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
14147 }
14148 }
14149 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
14150 $result = $fs->activate_license_on_site( $user, $license_key );
14151 }
14152 }
14153
14154 $is_connected = null;
14155
14156 if ( true !== $result && ! FS_Api::is_api_result_entity( $result ) ) {
14157 if ( FS_Api::is_blocked( $result ) ) {
14158 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
14159
14160 $is_connected = false;
14161 }
14162
14163 $error = FS_Api::is_api_error_object( $result ) ?
14164 $result->error->message :
14165 var_export( $result, true );
14166 } else {
14167 $is_connected = true;
14168
14169 $fs->network_upgrade_mode_completed();
14170
14171 $fs->_user = $user;
14172
14173 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
14174 $fs->_site = $fs->get_network_install();
14175 }
14176
14177 $fs->_sync_license( true, $has_valid_blog_id );
14178
14179 $this->maybe_sync_install_user();
14180
14181 $next_page = $fs->is_addon() ?
14182 $fs->get_parent_instance()->get_account_url() :
14183 $fs->get_after_activation_url( 'after_connect_url' );
14184 }
14185
14186 $fs->update_connectivity_info( $is_connected );
14187 } else {
14188 $next_page = $fs->opt_in(
14189 false,
14190 false,
14191 false,
14192 $license_key,
14193 false,
14194 false,
14195 false,
14196 $is_marketing_allowed,
14197 $sites
14198 );
14199
14200 if ( isset( $next_page->error ) ) {
14201 $error = $next_page->error;
14202 } else {
14203 if ( $is_network_activation_or_migration ) {
14204 /**
14205 * Get the list of sites that were just opted-in (and license activated).
14206 * This is an optimization for the next part below saving some DB queries.
14207 */
14208 $connected_sites = array();
14209 foreach ( $sites as $site ) {
14210 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
14211 $connected_sites[ $site['blog_id'] ] = true;
14212 }
14213 }
14214
14215 $all_sites = self::get_sites();
14216 $pending_blog_ids = array();
14217
14218 /**
14219 * Check if there are any sites that are not connected, skipped, nor delegated. For every site that falls into that category, if the product is freemium, skip the connection. If the product is premium only, delegate the connection to the site administrator.
14220 *
14221 * @author Vova Feldman (@svovaf)
14222 */
14223 foreach ( $all_sites as $site ) {
14224 $blog_id = self::get_site_blog_id( $site );
14225
14226 if ( isset( $connected_sites[ $blog_id ] ) ) {
14227 // Site was just connected.
14228 continue;
14229 }
14230
14231 if ( $fs->is_installed_on_site( $blog_id ) ) {
14232 // Site was already connected before.
14233 continue;
14234 }
14235
14236 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
14237 // Site's connection was delegated.
14238 continue;
14239 }
14240
14241 if ( $fs->is_anonymous_site( $blog_id ) ) {
14242 // Site connection was already skipped.
14243 continue;
14244 }
14245
14246 $pending_blog_ids[] = $blog_id;
14247 }
14248
14249 if ( ! empty( $pending_blog_ids ) ) {
14250 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
14251 $fs->skip_connection( $pending_blog_ids );
14252 } else {
14253 $fs->delegate_connection( $pending_blog_ids );
14254 }
14255 }
14256 }
14257 }
14258 }
14259
14260 if ( false === $error && true === $fs->_storage->require_license_activation ) {
14261 $fs->_storage->require_license_activation = false;
14262 }
14263
14264 $result = array(
14265 'success' => ( false === $error )
14266 );
14267
14268 if ( false !== $error ) {
14269 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
14270 } else {
14271 if ( $fs->is_addon() || $fs->has_addons() ) {
14272 /**
14273 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
14274 * an updated valid user licenses collection will be fetched from the server which is used to also
14275 * update the account add-ons (add-ons the user has licenses for).
14276 *
14277 * @author Leo Fajardo (@leorw)
14278 * @since 2.2.4
14279 */
14280 $fs->purge_valid_user_licenses_cache();
14281 }
14282
14283 $result['next_page'] = $next_page;
14284 }
14285
14286 return $result;
14287 }
14288
14289 /**
14290 * @author Leo Fajardo (@leorw)
14291 * @since 2.3.2
14292 *
14293 * @return array {
14294 * @key string Product slug.
14295 * @value array {
14296 * @property FS_Site $site
14297 * @property FS_Plugin_License $license
14298 * }
14299 * }
14300 */
14301 private function get_parent_and_addons_installs_info() {
14302 $fs = $this->is_addon() ?
14303 $this->get_parent_instance() :
14304 $this;
14305
14306 $installed_addons_ids = array();
14307
14308 $installed_addons_instances = $fs->get_installed_addons();
14309 foreach ( $installed_addons_instances as $instance ) {
14310 $installed_addons_ids[] = $instance->get_id();
14311 }
14312
14313 $addons_ids = array_unique( array_merge(
14314 $installed_addons_ids,
14315 $fs->get_updated_account_addons()
14316 ) );
14317
14318 // Add parent product info.
14319 $installs_info_by_slug_map = array(
14320 $fs->get_slug() => array(
14321 'install' => $fs->get_site(),
14322 'license' => $fs->_get_license()
14323 )
14324 );
14325
14326 foreach ( $addons_ids as $addon_id ) {
14327 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
14328
14329 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
14330
14331 if ( ! isset( $addon_info['is_connected'] ) || ! $addon_info['is_connected'] ) {
14332 // Add-on is not associated with an install entity.
14333 continue;
14334 }
14335
14336 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
14337 'install' => $addon_info['site'],
14338 'license' => isset( $addon_info['license'] ) ?
14339 $addon_info['license'] :
14340 null
14341 );
14342 }
14343
14344 return $installs_info_by_slug_map;
14345 }
14346
14347 /**
14348 * @author Leo Fajardo (@leorw)
14349 * @since 1.2.3.1
14350 */
14351 function _network_activate_ajax_action() {
14352 $this->_logger->entrance();
14353
14354 $this->check_ajax_referer( 'network_activate' );
14355
14356 $plugin_id = fs_request_get( 'module_id', '', 'post' );
14357 $fs = ( $plugin_id == $this->_module_id ) ?
14358 $this :
14359 $this->get_addon_instance( $plugin_id );
14360
14361 $error = false;
14362
14363 $sites = fs_request_get( 'sites', array(), 'post' );
14364 if ( is_array( $sites ) && ! empty( $sites ) ) {
14365 $sites_by_action = array(
14366 'allow' => array(),
14367 'delegate' => array(),
14368 'skip' => array()
14369 );
14370
14371 foreach ( $sites as $site ) {
14372 $sites_by_action[ $site['action'] ][] = $site;
14373 }
14374
14375 $total_sites = count( $sites );
14376 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
14377
14378 $next_page = '';
14379
14380 $has_any_install = fs_request_get_bool( 'has_any_install' );
14381
14382 if ( $total_sites === $total_sites_to_delegate &&
14383 ! $this->is_network_upgrade_mode() &&
14384 ! $has_any_install
14385 ) {
14386 $this->delegate_connection();
14387 } else {
14388 if ( ! empty( $sites_by_action['delegate'] ) ) {
14389 $this->delegate_connection( self::get_sites_blog_ids( $sites_by_action['delegate'] ) );
14390 }
14391
14392 if ( ! empty( $sites_by_action['skip'] ) ) {
14393 $this->skip_connection( self::get_sites_blog_ids( $sites_by_action['skip'] ) );
14394 }
14395
14396 if ( empty( $sites_by_action['allow'] ) ) {
14397 if ( $has_any_install ) {
14398 $first_install = $fs->find_first_install();
14399
14400 if ( ! is_null( $first_install ) ) {
14401 $fs->_site = $first_install['install'];
14402 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
14403
14404 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
14405 $fs->_storage->network_user_id = $fs->_user->id;
14406 }
14407 }
14408 } else {
14409 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
14410 $next_page = $fs->opt_in(
14411 false,
14412 false,
14413 false,
14414 false,
14415 false,
14416 false,
14417 false,
14418 fs_request_get_bool( 'is_marketing_allowed', null ),
14419 $sites_by_action['allow']
14420 );
14421 } else {
14422 $next_page = $fs->install_with_user(
14423 $this->get_network_user(),
14424 false,
14425 false,
14426 false,
14427 true,
14428 $sites_by_action['allow']
14429 );
14430 }
14431
14432 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
14433 $error = $next_page->error;
14434 }
14435 }
14436 }
14437
14438 if ( empty( $next_page ) ) {
14439 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
14440 }
14441 } else {
14442 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
14443 }
14444
14445 $result = array(
14446 'success' => ( false === $error )
14447 );
14448
14449 if ( false !== $error ) {
14450 $result['error'] = $error;
14451 } else {
14452 $result['next_page'] = $next_page;
14453 }
14454
14455 echo json_encode( $result );
14456
14457 exit;
14458 }
14459
14460 /**
14461 * Billing update AJAX callback.
14462 *
14463 * @author Vova Feldman (@svovaf)
14464 * @since 1.2.1.5
14465 */
14466 function _update_billing_ajax_action() {
14467 $this->_logger->entrance();
14468
14469 $this->check_ajax_referer( 'update_billing' );
14470
14471 if ( ! $this->is_user_admin() ) {
14472 // Only for admins.
14473 self::shoot_ajax_failure();
14474 }
14475
14476 $billing = fs_request_get( 'billing' );
14477
14478 $api = $this->get_api_user_scope();
14479 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
14480 'plugin_id' => $this->get_parent_id(),
14481 ) ) );
14482
14483 if ( ! $this->is_api_result_entity( $result ) ) {
14484 self::shoot_ajax_failure();
14485 }
14486
14487 // Purge cached billing.
14488 $this->get_api_user_scope()->purge_cache( 'billing.json' );
14489
14490 self::shoot_ajax_success();
14491 }
14492
14493 /**
14494 * Trial start for anonymous users (AJAX callback).
14495 *
14496 * @author Vova Feldman (@svovaf)
14497 * @since 1.2.1.5
14498 */
14499 function _start_trial_ajax_action() {
14500 $this->_logger->entrance();
14501
14502 $this->check_ajax_referer( 'start_trial' );
14503
14504 if ( ! $this->is_user_admin() ) {
14505 // Only for admins.
14506 self::shoot_ajax_failure();
14507 }
14508
14509 $trial_data = fs_request_get( 'trial' );
14510
14511 $next_page = $this->opt_in(
14512 false,
14513 false,
14514 false,
14515 false,
14516 false,
14517 $trial_data['plan_id']
14518 );
14519
14520 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14521 self::shoot_ajax_failure(
14522 isset( $next_page->error ) ?
14523 $next_page->error->message :
14524 var_export( $next_page, true )
14525 );
14526 }
14527
14528 $this->shoot_ajax_success( array(
14529 'next_page' => $next_page,
14530 ) );
14531 }
14532
14533 /**
14534 * @author Leo Fajardo (@leorw)
14535 * @since 1.2.0
14536 */
14537 function _resend_license_key_ajax_action() {
14538 $this->_logger->entrance();
14539
14540 $this->check_ajax_referer( 'resend_license_key' );
14541
14542 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14543
14544 if ( empty( $email_address ) ) {
14545 exit;
14546 }
14547
14548 $error = false;
14549
14550 $api = $this->get_api_plugin_scope();
14551 $result = $api->call( '/licenses/resend.json', 'post',
14552 array(
14553 'email' => $email_address,
14554 'url' => home_url(),
14555 )
14556 );
14557
14558 if ( is_object( $result ) && isset( $result->error ) ) {
14559 $error = $result->error;
14560
14561 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14562 $error = $this->get_text_inline( "We couldn't find your email address in the system, are you sure it's the right address?", 'email-not-found' );
14563 } else if ( 'no_license' === $error->code ) {
14564 $error = $this->get_text_inline( "We can't see any active licenses associated with that email address, are you sure it's the right address?", 'no-active-licenses' );
14565 } else {
14566 $error = $error->message;
14567 }
14568 }
14569
14570 $licenses = array(
14571 'success' => ( false === $error )
14572 );
14573
14574 if ( false !== $error ) {
14575 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14576 }
14577
14578 echo json_encode( $licenses );
14579
14580 exit;
14581 }
14582
14583 /**
14584 * @author Vova Feldman (@svovaf)
14585 * @since 1.2.1.8
14586 *
14587 * @var string
14588 */
14589 private static $_pagenow;
14590
14591 /**
14592 * Get current page or the referer if executing a WP AJAX request.
14593 *
14594 * @author Vova Feldman (@svovaf)
14595 * @since 1.2.1.8
14596 *
14597 * @return string
14598 */
14599 static function get_current_page() {
14600 if ( ! isset( self::$_pagenow ) ) {
14601 global $pagenow;
14602 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14603 /**
14604 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14605 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14606 *
14607 * @author Leo Fajardo (@leorw)
14608 * @since 2.2.3
14609 */
14610 if ( is_network_admin() ) {
14611 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14612 } else if ( is_user_admin() ) {
14613 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14614 } else {
14615 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14616 }
14617
14618 $pagenow = $self_matches[1];
14619 $pagenow = trim( $pagenow, '/' );
14620 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14621 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14622 $pagenow = 'index.php';
14623 } else {
14624 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14625 $pagenow = strtolower( $self_matches[1] );
14626 if ( '.php' !== substr($pagenow, -4, 4) )
14627 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14628 }
14629 }
14630
14631 self::$_pagenow = $pagenow;
14632
14633 if ( self::is_ajax() &&
14634 'admin-ajax.php' === $pagenow
14635 ) {
14636 $referer = fs_get_raw_referer();
14637
14638 if ( is_string( $referer ) ) {
14639 $parts = explode( '?', $referer );
14640
14641 self::$_pagenow = basename( $parts[0] );
14642 }
14643 }
14644 }
14645
14646 return self::$_pagenow;
14647 }
14648
14649 /**
14650 * Helper method to check if user in the plugins page.
14651 *
14652 * @author Vova Feldman (@svovaf)
14653 * @since 1.2.1.5
14654 *
14655 * @return bool
14656 */
14657 static function is_plugins_page() {
14658 return ( 'plugins.php' === self::get_current_page() );
14659 }
14660
14661 /**
14662 * @author Leo Fajardo (@leorw)
14663 * @since 2.2.3
14664 *
14665 * @return bool
14666 */
14667 static function is_plugin_install_page() {
14668 return ( 'plugin-install.php' === self::get_current_page() );
14669 }
14670
14671 /**
14672 * @author Leo Fajardo (@leorw)
14673 * @since 2.0.2
14674 *
14675 * @return bool
14676 */
14677 static function is_updates_page() {
14678 return ( 'update-core.php' === self::get_current_page() );
14679 }
14680
14681 /**
14682 * Helper method to check if user in the themes page.
14683 *
14684 * @author Vova Feldman (@svovaf)
14685 * @since 1.2.2.6
14686 *
14687 * @return bool
14688 */
14689 static function is_themes_page() {
14690 return ( 'themes.php' === self::get_current_page() );
14691 }
14692
14693 #----------------------------------------------------------------------------------
14694 #region Affiliation
14695 #----------------------------------------------------------------------------------
14696
14697 /**
14698 * @author Leo Fajardo (@leorw)
14699 * @since 1.2.3
14700 *
14701 * @return bool
14702 */
14703 function has_affiliate_program() {
14704 if ( ! is_object( $this->_plugin ) ) {
14705 return false;
14706 }
14707
14708 return $this->_plugin->has_affiliate_program();
14709 }
14710
14711 /**
14712 * Get Plugin ID under which we will track affiliate application.
14713 *
14714 * This could either be the Bundle ID or the main plugin ID.
14715 *
14716 * @return number Bundle ID if developer has provided one, else the main plugin ID.
14717 */
14718 private function get_plugin_id_for_affiliate_terms() {
14719 return $this->has_bundle_context() ?
14720 $this->get_bundle_id() :
14721 $this->_plugin->id;
14722 }
14723
14724 /**
14725 * @author Leo Fajardo (@leorw)
14726 * @since 1.2.4
14727 */
14728 private function fetch_affiliate_terms() {
14729 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14730 /**
14731 * In case we have a bundle set in SDK configuration, we would like to use that for affiliates, not the main plugin.
14732 */
14733 $plugins_api = $this->has_bundle_context() ?
14734 $this->get_api_bundle_scope() :
14735 $this->get_api_plugin_scope();
14736
14737 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14738
14739 /**
14740 * At this point, we intentionally don't fallback to the main plugin, because the developer has chosen to use bundle. So it makes sense the affiliate program should be in context to the bundle too.
14741 */
14742 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14743 return;
14744 }
14745
14746 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14747 }
14748 }
14749
14750 /**
14751 * @author Leo Fajardo (@leorw)
14752 * @since 1.2.4
14753 */
14754 private function fetch_affiliate_and_custom_terms() {
14755 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14756 $application_data = $this->_storage->affiliate_application_data;
14757 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14758
14759 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14760
14761 $users_api = $this->get_api_user_scope();
14762 $result = $users_api->get( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14763 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14764 if ( ! empty( $result->affiliates ) ) {
14765 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14766
14767 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14768 $application_data['status'] = $affiliate->status;
14769 $this->_storage->affiliate_application_data = $application_data;
14770 }
14771
14772 if ( $affiliate->is_using_custom_terms ) {
14773 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14774 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14775 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14776 }
14777 }
14778
14779 $this->affiliate = $affiliate;
14780 }
14781 }
14782 }
14783 }
14784
14785 /**
14786 * @author Leo Fajardo (@leorw)
14787 * @since 1.2.3
14788 */
14789 private function fetch_affiliate_and_terms() {
14790 $this->_logger->entrance();
14791
14792 $this->fetch_affiliate_terms();
14793 $this->fetch_affiliate_and_custom_terms();
14794 }
14795
14796 /**
14797 * @author Leo Fajardo (@leorw)
14798 * @since 1.2.3
14799 *
14800 * @return FS_Affiliate
14801 */
14802 function get_affiliate() {
14803 return $this->affiliate;
14804 }
14805
14806
14807 /**
14808 * @author Leo Fajardo (@leorw)
14809 * @since 1.2.3
14810 *
14811 * @return FS_AffiliateTerms
14812 */
14813 function get_affiliate_terms() {
14814 return is_object( $this->custom_affiliate_terms ) ?
14815 $this->custom_affiliate_terms :
14816 $this->plugin_affiliate_terms;
14817 }
14818
14819 /**
14820 * @author Leo Fajardo (@leorw)
14821 * @since 1.2.3
14822 */
14823 function _submit_affiliate_application() {
14824 $this->_logger->entrance();
14825
14826 $this->check_ajax_referer( 'submit_affiliate_application' );
14827
14828 if ( ! $this->is_user_admin() ) {
14829 // Only for admins.
14830 self::shoot_ajax_failure();
14831 }
14832
14833 $affiliate = fs_request_get( 'affiliate' );
14834
14835 if ( empty( $affiliate['promotion_methods'] ) ) {
14836 unset( $affiliate['promotion_methods'] );
14837 }
14838
14839 if ( ! empty( $affiliate['additional_domains'] ) ) {
14840 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
14841 }
14842
14843 if ( ! $this->is_registered() ) {
14844 $email_address = isset( $affiliate['email'] ) ? $affiliate['email'] : '';
14845
14846 if ( ! is_email( $email_address ) ) {
14847 self::shoot_ajax_failure('Invalid email address.');
14848 }
14849
14850 // Opt in but don't track usage.
14851 $next_page = $this->opt_in(
14852 $email_address,
14853 false,
14854 false,
14855 false,
14856 false,
14857 false,
14858 true
14859 );
14860
14861 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14862 self::shoot_ajax_failure(
14863 isset( $next_page->error ) ?
14864 $next_page->error->message :
14865 var_export( $next_page, true )
14866 );
14867 } else if ( $this->is_pending_activation() ) {
14868 self::shoot_ajax_failure( $this->get_text_inline( 'Account is pending activation. Please check your email and click the link to activate your account and then submit the affiliate form again.', 'account-is-pending-activation' ) );
14869 }
14870 }
14871
14872 $this->fetch_affiliate_terms();
14873
14874 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14875
14876 $api = $this->get_api_user_scope();
14877 $result = $api->call(
14878 ( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
14879 'post',
14880 $affiliate
14881 );
14882
14883 if ( $this->is_api_error( $result ) ) {
14884 self::shoot_ajax_failure(
14885 isset( $result->error ) ?
14886 $result->error->message :
14887 var_export( $result, true )
14888 );
14889 } else {
14890 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
14891 $this->_admin_notices->remove_sticky( 'affiliate_program' );
14892 }
14893
14894 $affiliate_application_data = array(
14895 'status' => 'pending',
14896 'stats_description' => $affiliate['stats_description'],
14897 'promotion_method_description' => $affiliate['promotion_method_description'],
14898 );
14899
14900 if ( ! empty( $affiliate['promotion_methods'] ) ) {
14901 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
14902 }
14903
14904 if ( ! empty( $affiliate['domain'] ) ) {
14905 $affiliate_application_data['domain'] = $affiliate['domain'];
14906 }
14907
14908 if ( ! empty( $affiliate['additional_domains'] ) ) {
14909 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
14910 }
14911
14912 $this->_storage->affiliate_application_data = $affiliate_application_data;
14913 }
14914
14915 // Purge cached affiliate.
14916 $api->purge_cache( 'affiliate.json' );
14917
14918 self::shoot_ajax_success( $result );
14919 }
14920
14921 /**
14922 * @author Leo Fajardo (@leorw)
14923 * @since 1.2.3
14924 *
14925 * @return array|null
14926 */
14927 function get_affiliate_application_data() {
14928 if ( empty( $this->_storage->affiliate_application_data ) ) {
14929 return null;
14930 }
14931
14932 return $this->_storage->affiliate_application_data;
14933 }
14934
14935 #endregion Affiliation ------------------------------------------------------------
14936
14937 #----------------------------------------------------------------------------------
14938 #region URL Generators
14939 #----------------------------------------------------------------------------------
14940
14941 /**
14942 * Alias to pricing_url().
14943 *
14944 * @author Vova Feldman (@svovaf)
14945 * @since 1.0.2
14946 *
14947 * @uses pricing_url()
14948 *
14949 * @param string $period Billing cycle
14950 * @param bool $is_trial
14951 *
14952 * @return string
14953 */
14954 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14955 return $this->pricing_url( $period, $is_trial );
14956 }
14957
14958 /**
14959 * @author Vova Feldman (@svovaf)
14960 * @since 1.0.9
14961 *
14962 * @uses get_upgrade_url()
14963 *
14964 * @return string
14965 */
14966 function get_trial_url() {
14967 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
14968 }
14969
14970 /**
14971 * @author Leo Fajardo (@leorw)
14972 * @since 2.1.4
14973 *
14974 * @param string $new_version
14975 *
14976 * @return string
14977 */
14978 function version_upgrade_checkout_link( $new_version ) {
14979 if ( ! is_object( $this->_license ) ) {
14980 $url = $this->pricing_url();
14981
14982 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
14983 } else {
14984 $subscription = $this->_get_subscription( $this->_license->id );
14985
14986 $url = $this->checkout_url(
14987 is_object( $subscription ) ?
14988 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
14989 WP_FS__PERIOD_LIFETIME,
14990 false,
14991 array( 'licenses' => $this->_license->quota )
14992 );
14993
14994 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
14995 }
14996
14997 return sprintf(
14998 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
14999 sprintf(
15000 '<a href="%s">%s</a>',
15001 $this->apply_filters( 'update_notice_checkout_url', $url ),
15002 $purchase_license_text
15003 ),
15004 $new_version
15005 );
15006 }
15007
15008 /**
15009 * Plugin's pricing URL.
15010 *
15011 * @author Vova Feldman (@svovaf)
15012 * @since 1.0.4
15013 *
15014 * @param string $billing_cycle Billing cycle
15015 *
15016 * @param bool $is_trial
15017 *
15018 * @return string
15019 */
15020 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
15021 $this->_logger->entrance();
15022
15023 $params = array(
15024 'billing_cycle' => $billing_cycle
15025 );
15026
15027 if ( $is_trial ) {
15028 $params['trial'] = 'true';
15029 }
15030
15031 $url = $this->is_addon() ?
15032 $this->_parent->addon_url( $this->_slug ) :
15033 $this->_get_admin_page_url( 'pricing', $params );
15034
15035 return $this->apply_filters( 'pricing_url', $url );
15036 }
15037
15038 /**
15039 * Checkout page URL.
15040 *
15041 * @author Vova Feldman (@svovaf)
15042 * @since 1.0.6
15043 *
15044 * @param string $billing_cycle Billing cycle
15045 * @param bool $is_trial
15046 * @param array $extra (optional) Extra parameters, override other query params.
15047 * @param bool|null $network
15048 *
15049 * @return string
15050 */
15051 function checkout_url(
15052 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
15053 $is_trial = false,
15054 $extra = array(),
15055 $network = null
15056 ) {
15057 $this->_logger->entrance();
15058
15059 $params = array(
15060 'checkout' => 'true',
15061 'billing_cycle' => $billing_cycle,
15062 );
15063
15064 if ( $is_trial ) {
15065 $params['trial'] = 'true';
15066 }
15067
15068 /**
15069 * Params in extra override other params.
15070 */
15071 $params = array_merge( $params, $extra );
15072
15073 return $this->apply_filters( 'checkout_url', $this->_get_admin_page_url( 'pricing', $params, $network ) );
15074 }
15075
15076 /**
15077 * Add-on checkout URL.
15078 *
15079 * @author Vova Feldman (@svovaf)
15080 * @since 1.1.7
15081 *
15082 * @param number $addon_id
15083 * @param number $pricing_id
15084 * @param string $billing_cycle
15085 * @param bool $is_trial
15086 * @param bool|null $network
15087 *
15088 * @return string
15089 */
15090 function addon_checkout_url(
15091 $addon_id,
15092 $pricing_id,
15093 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
15094 $is_trial = false,
15095 $network = null
15096 ) {
15097 return $this->checkout_url( $billing_cycle, $is_trial, array(
15098 'plugin_id' => $addon_id,
15099 'pricing_id' => $pricing_id,
15100 ), $network );
15101 }
15102
15103 #endregion
15104
15105 #endregion ------------------------------------------------------------------
15106
15107 /**
15108 * Check if plugin has any add-ons.
15109 *
15110 * @author Vova Feldman (@svovaf)
15111 * @since 1.0.5
15112 *
15113 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
15114 *
15115 * @return bool
15116 */
15117 function has_addons() {
15118 $this->_logger->entrance();
15119
15120 return $this->_has_addons;
15121 }
15122
15123 /**
15124 * Check if plugin can work in anonymous mode.
15125 *
15126 * @author Vova Feldman (@svovaf)
15127 * @since 1.0.9
15128 *
15129 * @return bool
15130 *
15131 * @deprecated Please use is_enable_anonymous() instead.
15132 */
15133 function enable_anonymous() {
15134 return $this->_enable_anonymous;
15135 }
15136
15137 /**
15138 * Check if plugin can work in anonymous mode.
15139 *
15140 * @author Vova Feldman (@svovaf)
15141 * @since 1.1.9
15142 *
15143 * @return bool
15144 */
15145 function is_enable_anonymous() {
15146 return $this->_enable_anonymous;
15147 }
15148
15149 /**
15150 * Check if plugin is premium only (no free plans).
15151 *
15152 * @author Vova Feldman (@svovaf)
15153 * @since 1.1.9
15154 *
15155 * @return bool
15156 */
15157 function is_only_premium() {
15158 return $this->_is_premium_only;
15159 }
15160
15161 /**
15162 * Checks if the plugin's type is "plugin". The other type is "theme".
15163 *
15164 * @author Leo Fajardo (@leorw)
15165 * @since 1.2.2
15166 *
15167 * @return bool
15168 */
15169 function is_plugin() {
15170 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
15171 }
15172
15173 /**
15174 * @author Leo Fajardo (@leorw)
15175 * @since 1.2.2
15176 *
15177 * @return string
15178 */
15179 function get_module_type() {
15180 if ( ! isset( $this->_module_type ) ) {
15181 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
15182 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
15183 }
15184
15185 return $this->_module_type;
15186 }
15187
15188 /**
15189 * @author Leo Fajardo (@leorw)
15190 * @since 1.2.2
15191 *
15192 * @return string
15193 */
15194 function get_plugin_main_file_path() {
15195 return $this->_plugin_main_file_path;
15196 }
15197
15198 /**
15199 * Check if module has a premium code version.
15200 *
15201 * Serviceware module might be freemium without any
15202 * premium code version, where the paid features
15203 * are all part of the service.
15204 *
15205 * @author Vova Feldman (@svovaf)
15206 * @since 1.2.1.6
15207 *
15208 * @return bool
15209 */
15210 function has_premium_version() {
15211 return $this->_has_premium_version;
15212 }
15213
15214 /**
15215 * Check if feature supported with current site's plan.
15216 *
15217 * @author Vova Feldman (@svovaf)
15218 * @since 1.0.1
15219 *
15220 * @todo IMPLEMENT
15221 *
15222 * @param number $feature_id
15223 *
15224 * @throws Exception
15225 */
15226 function is_feature_supported( $feature_id ) {
15227 throw new Exception( 'not implemented' );
15228 }
15229
15230 /**
15231 * @author Vova Feldman (@svovaf)
15232 * @since 1.0.1
15233 *
15234 * @return bool Is running in SSL/HTTPS
15235 */
15236 function is_ssl() {
15237 return WP_FS__IS_HTTPS;
15238 }
15239
15240 /**
15241 * @author Vova Feldman (@svovaf)
15242 * @since 1.0.9
15243 *
15244 * @return bool Is running in AJAX call.
15245 *
15246 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
15247 */
15248 static function is_ajax() {
15249 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
15250 }
15251
15252 /**
15253 * Check if it's an AJAX call targeted for the current module.
15254 *
15255 * @author Vova Feldman (@svovaf)
15256 * @since 1.2.0
15257 *
15258 * @param array|string $actions Collection of AJAX actions.
15259 *
15260 * @return bool
15261 */
15262 function is_ajax_action( $actions ) {
15263 // Verify it's an ajax call.
15264 if ( ! self::is_ajax() ) {
15265 return false;
15266 }
15267
15268 // Verify the call is relevant for the plugin.
15269 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
15270 return false;
15271 }
15272
15273 // Verify it's one of the specified actions.
15274 if ( is_string( $actions ) ) {
15275 $actions = explode( ',', $actions );
15276 }
15277
15278 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15279 $ajax_action = fs_request_get( 'action' );
15280
15281 foreach ( $actions as $action ) {
15282 if ( $ajax_action === $this->get_action_tag( $action ) ) {
15283 return true;
15284 }
15285 }
15286 }
15287
15288 return false;
15289 }
15290
15291 /**
15292 * Check if it's an AJAX call targeted for current request.
15293 *
15294 * @author Vova Feldman (@svovaf)
15295 * @since 1.2.0
15296 *
15297 * @param array|string $actions Collection of AJAX actions.
15298 * @param number|null $module_id
15299 *
15300 * @return bool
15301 */
15302 static function is_ajax_action_static( $actions, $module_id = null ) {
15303 // Verify it's an ajax call.
15304 if ( ! self::is_ajax() ) {
15305 return false;
15306 }
15307
15308
15309 if ( ! empty( $module_id ) ) {
15310 // Verify the call is relevant for the plugin.
15311 if ( $module_id != fs_request_get( 'module_id' ) ) {
15312 return false;
15313 }
15314 }
15315
15316 // Verify it's one of the specified actions.
15317 if ( is_string( $actions ) ) {
15318 $actions = explode( ',', $actions );
15319 }
15320
15321 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15322 $ajax_action = fs_request_get( 'action' );
15323
15324 foreach ( $actions as $action ) {
15325 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
15326 return true;
15327 }
15328 }
15329 }
15330
15331 return false;
15332 }
15333
15334 /**
15335 * @author Vova Feldman (@svovaf)
15336 * @since 1.1.7
15337 *
15338 * @return bool
15339 */
15340 static function is_cron() {
15341 return ( defined( 'DOING_CRON' ) && DOING_CRON );
15342 }
15343
15344 /**
15345 * @author Leo Fajardo (@leorw)
15346 * @since 2.5.0
15347 *
15348 * @return bool
15349 */
15350 static function is_admin_post() {
15351 return ( 'admin-post.php' === self::get_current_page() );
15352 }
15353
15354 /**
15355 * Check if a real user is visiting the admin dashboard.
15356 *
15357 * @author Vova Feldman (@svovaf)
15358 * @since 1.1.7
15359 *
15360 * @return bool
15361 */
15362 function is_user_in_admin() {
15363 return (
15364 is_admin() &&
15365 ! self::is_ajax() &&
15366 ! self::is_cron() &&
15367 ! self::is_admin_post()
15368 );
15369 }
15370
15371 /**
15372 * Check if a real user is in the customizer view.
15373 *
15374 * @author Vova Feldman (@svovaf)
15375 * @since 1.2.2.7
15376 *
15377 * @return bool
15378 */
15379 static function is_customizer() {
15380 return is_customize_preview();
15381 }
15382
15383 /**
15384 * Check if running in HTTPS and if site's plan matching the specified plan.
15385 *
15386 * @param string $plan
15387 * @param bool $exact
15388 *
15389 * @return bool
15390 */
15391 function is_ssl_and_plan( $plan, $exact = false ) {
15392 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
15393 }
15394
15395 /**
15396 * Construct plugin's settings page URL.
15397 *
15398 * @author Vova Feldman (@svovaf)
15399 * @since 1.0.4
15400 *
15401 * @param string $page
15402 * @param array $params
15403 * @param bool|null $network
15404 *
15405 * @return string
15406 */
15407 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
15408 if ( is_null( $network ) ) {
15409 $network = (
15410 $this->_is_network_active &&
15411 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
15412 );
15413 }
15414
15415 if ( 0 < count( $params ) ) {
15416 foreach ( $params as $k => $v ) {
15417 $params[ $k ] = urlencode( $v );
15418 }
15419 }
15420
15421 $page_param = $this->_menu->get_slug( $page );
15422
15423 if ( empty( $page ) &&
15424 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
15425 $this->show_opt_in_on_themes_page()
15426 ) {
15427 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
15428
15429 return add_query_arg(
15430 $params,
15431 $this->admin_url( 'themes.php', 'admin', $network )
15432 );
15433 }
15434
15435 if ( ! $this->has_settings_menu() ) {
15436 if ( ! empty( $page ) ) {
15437 // Module doesn't have a setting page, but since the request is for
15438 // a specific Freemius page, use the admin.php path.
15439 return add_query_arg( array_merge( $params, array(
15440 'page' => $page_param,
15441 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15442 } else {
15443 if ( $this->is_activation_mode() ) {
15444 /**
15445 * @author Vova Feldman
15446 * @since 1.2.1.6
15447 *
15448 * If plugin doesn't have a settings page, create one for the opt-in screen.
15449 */
15450 return add_query_arg( array_merge( $params, array(
15451 'page' => $this->_slug,
15452 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15453 } else {
15454 // Plugin without a settings page.
15455 return add_query_arg(
15456 $params,
15457 $this->admin_url( 'plugins.php', 'admin', $network )
15458 );
15459 }
15460 }
15461 }
15462
15463 // Module has a submenu settings page.
15464 if ( ! $this->_menu->is_top_level() ) {
15465 $parent_slug = $this->_menu->get_parent_slug();
15466 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
15467 $parent_slug :
15468 'admin.php';
15469
15470 return add_query_arg( array_merge( $params, array(
15471 'page' => $page_param,
15472 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
15473 }
15474
15475 // Module has a top level CPT settings page.
15476 if ( $this->_menu->is_cpt() ) {
15477 if ( empty( $page ) && $this->is_activation_mode() ) {
15478 return add_query_arg( array_merge( $params, array(
15479 'page' => $page_param
15480 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15481 } else {
15482 if ( ! empty( $page ) ) {
15483 $params['page'] = $page_param;
15484 }
15485
15486 return add_query_arg(
15487 $params,
15488 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
15489 );
15490 }
15491 }
15492
15493 // Module has a custom top level settings page.
15494 return add_query_arg( array_merge( $params, array(
15495 'page' => $page_param,
15496 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15497 }
15498
15499 #--------------------------------------------------------------------------------
15500 #region Multisite
15501 #--------------------------------------------------------------------------------
15502
15503 /**
15504 * @author Leo Fajardo (@leorw)
15505 * @since 2.0.0
15506 *
15507 * @return bool
15508 */
15509 function is_network_active() {
15510 return $this->_is_network_active;
15511 }
15512
15513 /**
15514 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
15515 *
15516 * @author Leo Fajardo (@leorw)
15517 * @since 2.0.0
15518 *
15519 * @param bool|int[] $all_or_blog_ids
15520 */
15521 private function delegate_connection( $all_or_blog_ids = true ) {
15522 $this->_logger->entrance();
15523
15524 $this->_admin_notices->remove_sticky( 'connect_account' );
15525
15526 if ( true === $all_or_blog_ids ) {
15527 // All sites delegation.
15528 $this->_storage->store( 'is_delegated_connection', true, true );
15529 } else {
15530 // Specified sites delegation.
15531 foreach ( $all_or_blog_ids as $blog_id ) {
15532 $this->delegate_site_connection( $blog_id );
15533 }
15534 }
15535
15536 $this->network_upgrade_mode_completed();
15537 }
15538
15539 /**
15540 * Delegate specific network site conncetion to the site admin.
15541 *
15542 * @author Vova Feldman (@svovaf)
15543 * @since 2.0.0
15544 *
15545 * @param int $blog_id
15546 */
15547 private function delegate_site_connection( $blog_id ) {
15548 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
15549 }
15550
15551 /**
15552 * Check if super-admin delegated the connection of ALL sites to the site admins.
15553 *
15554 * @author Vova Feldman (@svovaf)
15555 * @since 2.0.0
15556 *
15557 * @return bool
15558 */
15559 function is_network_delegated_connection() {
15560 if ( ! $this->_is_network_active ) {
15561 return false;
15562 }
15563
15564 return $this->_storage->get( 'is_delegated_connection', false, true );
15565 }
15566
15567 /**
15568 * @author Leo Fajardo (@leorw)
15569 * @since 2.0.0
15570 *
15571 * @param int $blog_id
15572 *
15573 * @return bool
15574 */
15575 function is_site_delegated_connection( $blog_id = 0 ) {
15576 if ( ! $this->_is_network_active ) {
15577 return false;
15578 }
15579
15580 if ( 0 == $blog_id ) {
15581 $blog_id = get_current_blog_id();
15582 }
15583
15584 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15585 }
15586
15587 /**
15588 * Check if delegated the connection. When running within the network admin,
15589 * and haven't specified the blog ID, checks if network level delegated. If running
15590 * within a site admin or specified a blog ID, check if delegated the connection for
15591 * the current context site.
15592 *
15593 * If executed outside the the admin, check if delegated the connection
15594 * for the current context site OR the whole network.
15595 *
15596 * @author Vova Feldman (@svovaf)
15597 * @since 2.0.0
15598 *
15599 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15600 *
15601 * @return bool
15602 */
15603 function is_delegated_connection( $blog_id = 0 ) {
15604 if ( ! $this->_is_network_active ) {
15605 return false;
15606 }
15607
15608 if ( fs_is_network_admin() && 0 == $blog_id ) {
15609 return $this->is_network_delegated_connection();
15610 }
15611
15612 return (
15613 $this->is_network_delegated_connection() ||
15614 $this->is_site_delegated_connection( $blog_id )
15615 );
15616 }
15617
15618 /**
15619 * Check if the current module is active for the site.
15620 *
15621 * @author Vova Feldman (@svovaf)
15622 * @since 2.0.0
15623 *
15624 * @param int $blog_id
15625 *
15626 * @return bool
15627 */
15628 function is_active_for_site( $blog_id ) {
15629 if ( ! is_multisite() ) {
15630 // Not a multisite and this code is executed, means that the plugin is active.
15631 return true;
15632 }
15633
15634 if ( $this->is_theme() ) {
15635 // All themes are site level activated.
15636 return true;
15637 }
15638
15639 if ( $this->_is_network_active ) {
15640 // Plugin was network activated so it's active.
15641 return true;
15642 }
15643
15644 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15645 }
15646
15647 /**
15648 * @todo Implement pagination when accessing the subsites collection.
15649 *
15650 * @author Leo Fajardo (@leorw)
15651 * @since 2.0.0
15652 *
15653 * @param int $limit Default to 1,000
15654 * @param int $offset Default to 0
15655 *
15656 * @return array Active & public sites collection.
15657 */
15658 static function get_sites( $limit = 1000, $offset = 0 ) {
15659 if ( ! is_multisite() ) {
15660 return array();
15661 }
15662
15663 /**
15664 * For consistency with get_blog_list() which only return active public sites.
15665 *
15666 * @author Vova Feldman (@svovaf)
15667 */
15668 $args = array(
15669 /**
15670 * Commented out in order to handle the migration of site options whether the site is public or not.
15671 *
15672 * @author Leo Fajardo (@leorw)
15673 * @since 2.2.1
15674 */
15675 // 'public' => 1,
15676 'archived' => 0,
15677 'mature' => 0,
15678 'spam' => 0,
15679 'deleted' => 0,
15680 'number' => $limit,
15681 'offset' => $offset,
15682 );
15683
15684 return get_sites( $args );
15685 }
15686
15687 /**
15688 * Checks if a given blog is active.
15689 *
15690 * @author Vova Feldman (@svovaf)
15691 * @since 2.0.0
15692 *
15693 * @param $blog_id
15694 *
15695 * @return bool
15696 */
15697 private static function is_site_active( $blog_id ) {
15698 global $wpdb;
15699
15700 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15701
15702 if ( ! is_object( $blog_info ) ) {
15703 return false;
15704 }
15705
15706 return (
15707 true == $blog_info->public &&
15708 false == $blog_info->archived &&
15709 false == $blog_info->mature &&
15710 false == $blog_info->spam &&
15711 false == $blog_info->deleted
15712 );
15713 }
15714
15715 /**
15716 * Get a mapping between the site addresses to their blog IDs.
15717 *
15718 * @author Vova Feldman (@svovaf)
15719 * @since 2.0.0
15720 *
15721 * @return array {
15722 * @key string Site address without protocol with a trailing slash.
15723 * @value int Site's blog ID.
15724 * }
15725 */
15726 private function get_address_to_blog_map() {
15727 $sites = self::get_sites();
15728
15729 // Map site addresses to their blog IDs.
15730 $address_to_blog_map = array();
15731 foreach ( $sites as $site ) {
15732 $blog_id = self::get_site_blog_id( $site );
15733 $address = self::get_unfiltered_site_url( $blog_id, true, true );
15734 $address_to_blog_map[ $address ] = $blog_id;
15735 }
15736
15737 return $address_to_blog_map;
15738 }
15739
15740 /**
15741 * Get a mapping between the site addresses to their blog IDs.
15742 *
15743 * @author Vova Feldman (@svovaf)
15744 * @since 2.0.0
15745 *
15746 * @return array {
15747 * @key int Site's blog ID.
15748 * @value FS_Site Associated install.
15749 * }
15750 */
15751 function get_blog_install_map() {
15752 $sites = self::get_sites();
15753
15754 // Map site blog ID to its install.
15755 $install_map = array();
15756
15757 foreach ( $sites as $site ) {
15758 $blog_id = self::get_site_blog_id( $site );
15759 $install = $this->get_install_by_blog_id( $blog_id );
15760
15761 if ( is_object( $install ) ) {
15762 $install_map[ $blog_id ] = $install;
15763 }
15764 }
15765
15766 return $install_map;
15767 }
15768
15769 /**
15770 * @author Vova Feldman (@svovaf)
15771 * @since 2.5.1
15772 *
15773 * @param bool|null $is_delegated When `true`, returns only connection delegated blog IDs. When `false`, only non-delegated blog IDs.
15774 *
15775 * @return int[]
15776 */
15777 private function get_blog_ids( $is_delegated = null ) {
15778 $blog_ids = array();
15779
15780 $sites = self::get_sites();
15781 foreach ( $sites as $site ) {
15782 $blog_id = self::get_site_blog_id( $site );
15783
15784 if (
15785 is_null( $is_delegated ) ||
15786 $is_delegated === $this->is_site_delegated_connection( $blog_id )
15787 ) {
15788 $blog_ids[] = $blog_id;
15789 }
15790 }
15791
15792 return $blog_ids;
15793 }
15794
15795 /**
15796 * @author Vova Feldman (@svovaf)
15797 * @since 2.5.1
15798 *
15799 * @return int[]
15800 */
15801 private function get_non_delegated_blog_ids() {
15802 return $this->get_blog_ids( false );
15803 }
15804
15805 /**
15806 * Gets a map of module IDs that the given user has opted-in to.
15807 *
15808 * @author Leo Fajardo (@leorw)
15809 * @since 2.1.0
15810 *
15811 * @param number $fs_user_id
15812 *
15813 * @return array {
15814 * @key number $plugin_id
15815 * @value bool Always true.
15816 * }
15817 */
15818 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15819 self::$_static_logger->entrance();
15820
15821 if ( ! is_multisite() ) {
15822 $installs = array_merge(
15823 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15824 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15825 );
15826 } else {
15827 $sites = self::get_sites();
15828
15829 $installs = array();
15830 foreach ( $sites as $site ) {
15831 $blog_id = self::get_site_blog_id( $site );
15832
15833 $installs = array_merge(
15834 $installs,
15835 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15836 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15837 );
15838 }
15839 }
15840
15841 $module_ids_map = array();
15842 foreach ( $installs as $install ) {
15843 if ( is_object( $install ) &&
15844 FS_Site::is_valid_id( $install->id ) &&
15845 FS_User::is_valid_id( $install->user_id ) &&
15846 ( $install->user_id == $fs_user_id )
15847 ) {
15848 $module_ids_map[ $install->plugin_id ] = true;
15849 }
15850 }
15851
15852 return $module_ids_map;
15853 }
15854
15855 /**
15856 * @author Leo Fajardo (@leorw)
15857 *
15858 * @return null|array {
15859 * 'install' => FS_Site Module's install,
15860 * 'blog_id' => string The associated blog ID.
15861 * }
15862 */
15863 function find_first_install() {
15864 $sites = self::get_sites();
15865
15866 foreach ( $sites as $site ) {
15867 $blog_id = self::get_site_blog_id( $site );
15868 $install = $this->get_install_by_blog_id( $blog_id );
15869
15870 if ( is_object( $install ) ) {
15871 return array(
15872 'install' => $install,
15873 'blog_id' => $blog_id
15874 );
15875 }
15876 }
15877
15878 return null;
15879 }
15880
15881 /**
15882 * Switches the Freemius site level context to a specified blog.
15883 *
15884 * @author Vova Feldman (@svovaf)
15885 * @since 2.0.0
15886 *
15887 * @param int $blog_id
15888 * @param FS_Site $install
15889 * @param bool $flush
15890 *
15891 * @return bool Since 2.3.1 returns if a switch was made.
15892 */
15893 function switch_to_blog( $blog_id, FS_Site $install = null, $flush = false ) {
15894 if ( ! is_numeric( $blog_id ) ) {
15895 return false;
15896 }
15897
15898 if ( ! $flush && $blog_id == $this->_context_is_network_or_blog_id ) {
15899 return false;
15900 }
15901
15902 switch_to_blog( $blog_id );
15903 $this->_context_is_network_or_blog_id = $blog_id;
15904
15905 self::$_accounts->set_site_blog_context( $blog_id );
15906 $this->_storage->set_site_blog_context( $blog_id );
15907 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
15908
15909 $this->_site = is_object( $install ) ?
15910 $install :
15911 $this->get_install_by_blog_id( $blog_id );
15912
15913 $this->_user = false;
15914 $this->_licenses = false;
15915 $this->_license = null;
15916 $this->is_whitelabeled = null;
15917
15918 if ( is_object( $this->_site ) ) {
15919 // Try to fetch user from install.
15920 $this->_user = self::_get_user_by_id( $this->_site->user_id );
15921
15922 if ( ! is_object( $this->_user ) &&
15923 FS_User::is_valid_id( $this->_storage->prev_user_id )
15924 ) {
15925 // Try to fetch previously saved user.
15926 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
15927
15928 if ( ! is_object( $this->_user ) ) {
15929 // Fallback to network's user.
15930 $this->_user = $this->get_network_user();
15931 }
15932 }
15933
15934 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
15935
15936 if ( ! empty( $all_plugin_licenses ) ) {
15937 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
15938 $this->_license = null;
15939 } else {
15940 $license_found = false;
15941 foreach ( $all_plugin_licenses as $license ) {
15942 if ( $license->id == $this->_site->license_id ) {
15943 // License found.
15944 $this->_license = $license;
15945 $license_found = true;
15946 break;
15947 }
15948 }
15949
15950 if ( $license_found ) {
15951 $this->link_license_2_user( $this->_license->id, $this->_user->id );
15952 }
15953 }
15954
15955 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15956 }
15957 }
15958
15959 unset( $this->_site_api );
15960 unset( $this->_user_api );
15961
15962 return true;
15963 }
15964
15965 /**
15966 * Restore the blog context to the blog that originally loaded the module.
15967 *
15968 * @author Vova Feldman (@svovaf)
15969 * @since 2.0.0
15970 */
15971 function restore_current_blog() {
15972 $this->switch_to_blog( $this->_blog_id );
15973 }
15974
15975 /**
15976 * @author Vova Feldman (@svovaf)
15977 * @since 2.0.0
15978 *
15979 * @param array|WP_Site $site
15980 *
15981 * @return int
15982 */
15983 static function get_site_blog_id( &$site ) {
15984 return ( $site instanceof WP_Site ) ?
15985 $site->blog_id :
15986 ( is_object( $site ) && isset( $site->userblog_id ) ?
15987 $site->userblog_id :
15988 $site['blog_id'] );
15989 }
15990
15991 /**
15992 * @author Vova Feldman (@svovaf)
15993 * @since 2.5.1
15994 *
15995 * @param WP_Site[]|array[] $sites
15996 *
15997 * @return int[]
15998 */
15999 static function get_sites_blog_ids( $sites ) {
16000 $blog_ids = array();
16001 foreach ( $sites as $site ) {
16002 $blog_ids[] = self::get_site_blog_id( $site );
16003 }
16004
16005 return $blog_ids;
16006 }
16007
16008 /**
16009 * @author Leo Fajardo (@leorw)
16010 * @since 2.0.0
16011 *
16012 * @param array|WP_Site|null $site
16013 * @param bool $load_registration Since 2.5.1 When set to `true` the method will attempt to return the subsite's registration date, regardless of the `$site` type and value. In most calls, the registration date will be returned anyway, even when the value is `false`. This param is purely for performance optimization.
16014 *
16015 * @return array
16016 */
16017 function get_site_info( $site = null, $load_registration = false ) {
16018 $this->_logger->entrance();
16019
16020 $switched = false;
16021
16022 $registration_date = null;
16023
16024 if ( is_null( $site ) ) {
16025 $url = self::get_unfiltered_site_url();
16026 $name = get_bloginfo( 'name' );
16027 $blog_id = null;
16028 } else {
16029 $blog_id = self::get_site_blog_id( $site );
16030
16031 if ( get_current_blog_id() != $blog_id ) {
16032 switch_to_blog( $blog_id );
16033 $switched = true;
16034 }
16035
16036 if ( $site instanceof WP_Site ) {
16037 $url = $site->siteurl;
16038 $name = $site->blogname;
16039 $registration_date = $site->registered;
16040 } else {
16041 $url = self::get_unfiltered_site_url( $blog_id );
16042 $name = get_bloginfo( 'name' );
16043 }
16044 }
16045
16046 if ( empty( $registration_date ) && $load_registration ) {
16047 $blog_details = get_blog_details( $blog_id, false );
16048
16049 if ( is_object( $blog_details ) && isset( $blog_details->registered ) ) {
16050 $registration_date = $blog_details->registered;
16051 }
16052 }
16053
16054 $info = array(
16055 'uid' => $this->get_anonymous_id( $blog_id ),
16056 'url' => $url,
16057 );
16058
16059 // Add these diagnostic information only if user allowed to track.
16060 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
16061 $info = array_merge( $info, array(
16062 'title' => $name,
16063 'language' => self::get_sanitized_language(),
16064 ) );
16065 }
16066
16067 if ( is_numeric( $blog_id ) ) {
16068 $info['blog_id'] = $blog_id;
16069 }
16070
16071 if ( ! empty( $registration_date ) ) {
16072 $info[ 'registration_date' ] = $registration_date;
16073 }
16074
16075 if ( $switched ) {
16076 restore_current_blog();
16077 }
16078
16079 return $info;
16080 }
16081
16082 /**
16083 * Load the module's install based on the blog ID.
16084 *
16085 * @author Vova Feldman (@svovaf)
16086 * @since 2.0.0
16087 *
16088 * @param int|null $blog_id
16089 *
16090 * @return FS_Site
16091 */
16092 function get_install_by_blog_id( $blog_id = null ) {
16093 $installs = self::get_all_sites( $this->_module_type, $blog_id );
16094 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
16095
16096 if ( is_object( $install ) &&
16097 is_numeric( $install->id ) &&
16098 is_numeric( $install->user_id ) &&
16099 FS_Plugin_Plan::is_valid_id( $install->plan_id )
16100 ) {
16101 // Load site.
16102 $install = clone $install;
16103 }
16104
16105 return $install;
16106 }
16107
16108 /**
16109 * Check if module is installed on a specified site.
16110 *
16111 * @author Vova Feldman (@svovaf)
16112 * @since 2.0.0
16113 *
16114 * @param int|null $blog_id
16115 *
16116 * @return bool
16117 */
16118 function is_installed_on_site( $blog_id = null ) {
16119 $installs = self::get_all_sites( $this->_module_type, $blog_id );
16120 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
16121
16122 return (
16123 is_object( $install ) &&
16124 is_numeric( $install->id ) &&
16125 is_numeric( $install->user_id ) &&
16126 FS_Plugin_Plan::is_valid_id( $install->plan_id )
16127 );
16128 }
16129
16130 /**
16131 * Check if super-admin connected at least one site via the network opt-in.
16132 *
16133 * @author Vova Feldman (@svovaf)
16134 * @since 2.0.0
16135 *
16136 * @return bool
16137 */
16138 function is_network_registered() {
16139 if ( ! $this->_is_network_active ) {
16140 return false;
16141 }
16142
16143 return FS_User::is_valid_id( $this->_storage->network_user_id );
16144 }
16145
16146 /**
16147 * Returns the main user associated with the network.
16148 *
16149 * @author Vova Feldman (@svovaf)
16150 * @since 2.0.0
16151 *
16152 * @return FS_User
16153 */
16154 function get_network_user() {
16155 if ( ! $this->_is_network_active ) {
16156 return null;
16157 }
16158
16159 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
16160 self::_get_user_by_id( $this->_storage->network_user_id ) :
16161 null;
16162 }
16163
16164 /**
16165 * Returns the current context user or the network's main user.
16166 *
16167 * @author Vova Feldman (@svovaf)
16168 * @since 2.0.0
16169 *
16170 * @return FS_User
16171 */
16172 function get_current_or_network_user() {
16173 return ( $this->_user instanceof FS_User ) ?
16174 $this->_user :
16175 $this->get_network_user();
16176 }
16177
16178 /**
16179 * Returns the main install associated with the network.
16180 *
16181 * @author Vova Feldman (@svovaf)
16182 * @since 2.0.0
16183 *
16184 * @return FS_Site
16185 */
16186 function get_network_install() {
16187 if ( ! $this->_is_network_active ) {
16188 return null;
16189 }
16190
16191 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
16192 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
16193 null;
16194 }
16195
16196 /**
16197 * Returns the blog ID that is associated with the main install.
16198 *
16199 * @author Leo Fajardo (@leorw)
16200 * @since 2.0.0
16201 *
16202 * @return int|null
16203 */
16204 function get_network_install_blog_id() {
16205 if ( ! $this->_is_network_active ) {
16206 return null;
16207 }
16208
16209 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
16210 $this->_storage->network_install_blog_id :
16211 null;
16212 }
16213
16214 /**
16215 * Returns the current context install or the network's main install.
16216 *
16217 * @author Vova Feldman (@svovaf)
16218 * @since 2.0.0
16219 *
16220 * @return FS_Site
16221 */
16222 function get_current_or_network_install() {
16223 return ( $this->_site instanceof FS_Site ) ?
16224 $this->_site :
16225 $this->get_network_install();
16226 }
16227
16228 /**
16229 * Check if executing a site level action from the network level admin.
16230 *
16231 * @author Vova Feldman (@svovaf)
16232 * @since 2.0.0
16233 *
16234 * @return false|int If yes, return the requested blog ID.
16235 */
16236 private function is_network_level_site_specific_action() {
16237 if ( ! $this->_is_network_active ) {
16238 return false;
16239 }
16240
16241 if ( ! fs_is_network_admin() ) {
16242 return false;
16243 }
16244
16245 $blog_id = fs_request_get( 'blog_id', '' );
16246
16247 return is_numeric( $blog_id ) ? $blog_id : false;
16248 }
16249
16250 /**
16251 * Check if executing an action from the network level admin.
16252 *
16253 * @author Vova Feldman (@svovaf)
16254 * @since 2.0.0
16255 *
16256 * @return bool
16257 */
16258 private function is_network_level_action() {
16259 return ( $this->_is_network_active && fs_is_network_admin() );
16260 }
16261
16262 /**
16263 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
16264 * The logic updates the network level user and blog, and reschedule the crons if the cron executing site matching the site that is no longer publicly active.
16265 *
16266 * @author Vova Feldman (@svovaf)
16267 * @since 2.0.0
16268 *
16269 * @param int $context_blog_id
16270 */
16271 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
16272 $this->_logger->entrance();
16273
16274 if ( $this->_is_network_active ) {
16275 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
16276 $installs_map = $this->get_blog_install_map();
16277
16278 foreach ( $installs_map as $blog_id => $install ) {
16279 /**
16280 * @var FS_Site $install
16281 */
16282 if ( $context_blog_id == $blog_id ) {
16283 continue;
16284 }
16285
16286 if ( $install->user_id != $this->_storage->network_user_id ) {
16287 continue;
16288 }
16289
16290 // Switch reference to a blog that is opted-in and belong to the same super-admin.
16291 $this->_storage->network_install_blog_id = $blog_id;
16292 break;
16293 }
16294 }
16295 }
16296
16297 if ( ! $this->is_registered() ) {
16298 return;
16299 }
16300
16301 if ( $this->is_sync_cron_scheduled() &&
16302 $context_blog_id == $this->get_sync_cron_blog_id()
16303 ) {
16304 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
16305 }
16306
16307 if ( $this->is_install_sync_scheduled() &&
16308 $context_blog_id == $this->get_install_sync_cron_blog_id()
16309 ) {
16310 $this->schedule_install_sync( $context_blog_id );
16311 }
16312 }
16313
16314 /**
16315 * Executed after site deactivation, archive, or flag as spam.
16316 *
16317 * @author Vova Feldman (@svovaf)
16318 * @since 2.0.0
16319 *
16320 * @param int $context_blog_id
16321 */
16322 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
16323 $this->_logger->entrance();
16324
16325 $install = $this->get_install_by_blog_id( $context_blog_id );
16326
16327 if ( ! is_object( $install ) ) {
16328 // Site not connected.
16329 return;
16330 }
16331
16332 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16333
16334 if ( ! $this->is_registered() ) {
16335 return;
16336 }
16337
16338 $current_blog_id = get_current_blog_id();
16339
16340 $this->switch_to_blog( $context_blog_id );
16341
16342 // Send deactivation event.
16343 $this->sync_install( array(
16344 'is_active' => false,
16345 ) );
16346
16347 $this->switch_to_blog( $current_blog_id );
16348 }
16349
16350 /**
16351 * Executed after site deletion.
16352 *
16353 * @author Vova Feldman (@svovaf)
16354 * @since 2.0.0
16355 *
16356 * @param int $context_blog_id
16357 * @param bool $drop True if site's database tables should be dropped. Default is false.
16358 */
16359 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
16360 $this->_logger->entrance();
16361
16362 $install = $this->get_install_by_blog_id( $context_blog_id );
16363
16364 if ( ! is_object( $install ) ) {
16365 // Site not connected.
16366 return;
16367 }
16368
16369 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16370
16371 if ( ! $this->is_registered() ) {
16372 return;
16373 }
16374
16375 $current_blog_id = get_current_blog_id();
16376
16377 $this->switch_to_blog( $context_blog_id );
16378
16379 if ( $drop ) {
16380 // Delete install if dropping site DB.
16381 $this->delete_account_event();
16382 } else {
16383 // Send deactivation event.
16384 $this->sync_install( array(
16385 'is_active' => false,
16386 ) );
16387 }
16388
16389 $this->switch_to_blog( $current_blog_id );
16390 }
16391
16392 /**
16393 * Executed after site deletion, called from wp_delete_site
16394 *
16395 * @author Dario Curvino (@dudo)
16396 * @since 2.5.0
16397 *
16398 * @param WP_Site $old_site
16399 */
16400 public function _after_wpsite_deleted_callback( WP_Site $old_site ) {
16401 $this->_logger->entrance();
16402
16403 $this->_after_site_deleted_callback( $old_site->blog_id, true );
16404 }
16405
16406 /**
16407 * Executed after site re-activation.
16408 *
16409 * @author Vova Feldman (@svovaf)
16410 * @since 2.0.0
16411 *
16412 * @param int $context_blog_id
16413 */
16414 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
16415 $this->_logger->entrance();
16416
16417 $install = $this->get_install_by_blog_id( $context_blog_id );
16418
16419 if ( ! is_object( $install ) ) {
16420 // Site not connected.
16421 return;
16422 }
16423
16424 if ( ! self::is_site_active( $context_blog_id ) ) {
16425 // Site not yet active (can be in spam mode, archived, deleted...).
16426 return;
16427 }
16428
16429 $current_blog_id = get_current_blog_id();
16430
16431 $this->switch_to_blog( $context_blog_id );
16432
16433 // Send re-activation event.
16434 $this->sync_install( array(
16435 'is_active' => true,
16436 ) );
16437
16438 $this->switch_to_blog( $current_blog_id );
16439 }
16440
16441 #endregion Multisite
16442
16443 /**
16444 * @author Leo Fajardo (@leorw)
16445 *
16446 * @param string $path
16447 * @param string $scheme
16448 * @param bool $network
16449 *
16450 * @return string
16451 */
16452 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
16453 return ( $this->_is_network_active && $network ) ?
16454 network_admin_url( $path, $scheme ) :
16455 admin_url( $path, $scheme );
16456 }
16457
16458 /**
16459 * Check if currently in a specified admin page.
16460 *
16461 * @author Vova Feldman (@svovaf)
16462 * @since 1.2.2.7
16463 *
16464 * @param string $page
16465 *
16466 * @return bool
16467 */
16468 function is_admin_page( $page ) {
16469 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
16470 }
16471
16472 /**
16473 * Check if currently in the product's main admin page.
16474 *
16475 * @author Vova Feldman (@svovaf)
16476 * @since 2.3.1
16477 *
16478 * @return bool
16479 */
16480 function is_main_admin_page() {
16481 return $this->is_admin_page( '' );
16482 }
16483
16484 /**
16485 * Get module's main admin setting page URL.
16486 *
16487 * @author Vova Feldman (@svovaf)
16488 * @since 1.2.2.7
16489 *
16490 * @return string
16491 */
16492 function main_menu_url() {
16493 return $this->_menu->main_menu_url();
16494 }
16495
16496 /**
16497 * Check if currently on the theme's setting page or
16498 * on any of the Freemius added pages (via tabs).
16499 *
16500 * @author Vova Feldman (@svovaf)
16501 * @since 1.2.2.7
16502 *
16503 * @return bool
16504 *
16505 * @deprecated Please use is_product_settings_page() instead;
16506 */
16507 function is_theme_settings_page() {
16508 return $this->is_product_settings_page();
16509 }
16510
16511 /**
16512 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
16513 *
16514 * @author Vova Feldman (@svovaf)
16515 * @since 1.2.2.7
16516 *
16517 * @return bool
16518 */
16519 function is_product_settings_page() {
16520 $page = fs_request_get( 'page', '', 'get' );
16521 $menu_slug = $this->_menu->get_slug();
16522
16523 if ( $page === $menu_slug ) {
16524 return true;
16525 }
16526
16527 return fs_starts_with(
16528 // e.g., {$menu_slug}-account, {$menu_slug}-affiliation, etc.
16529 $page,
16530 ( $menu_slug . '-' )
16531 );
16532 }
16533
16534 /**
16535 * Plugin's account page + sync license URL.
16536 *
16537 * @author Vova Feldman (@svovaf)
16538 * @since 1.1.9.1
16539 *
16540 * @param bool|number $plugin_id
16541 * @param bool $add_action_nonce
16542 * @param array $params
16543 *
16544 * @return string
16545 */
16546 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
16547 if ( is_numeric( $plugin_id ) ) {
16548 $params['plugin_id'] = $plugin_id;
16549 }
16550
16551 return $this->get_account_url(
16552 $this->get_unique_affix() . '_sync_license',
16553 $params,
16554 $add_action_nonce
16555 );
16556 }
16557
16558 /**
16559 * Plugin's account URL.
16560 *
16561 * @author Vova Feldman (@svovaf)
16562 * @since 1.0.4
16563 *
16564 * @param bool|string $action
16565 * @param array $params
16566 *
16567 * @param bool $add_action_nonce
16568 *
16569 * @return string
16570 */
16571 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
16572 if ( is_string( $action ) ) {
16573 $params['fs_action'] = $action;
16574 }
16575
16576 self::require_pluggable_essentials();
16577
16578 return ( $add_action_nonce && is_string( $action ) ) ?
16579 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
16580 $this->_get_admin_page_url( 'account', $params );
16581 }
16582
16583 /**
16584 * @author Vova Feldman (@svovaf)
16585 * @since 1.2.0
16586 *
16587 * @param string $tab
16588 * @param bool $action
16589 * @param array $params
16590 * @param bool $add_action_nonce
16591 *
16592 * @return string
16593 *
16594 * @uses get_account_url()
16595 */
16596 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
16597 $params['tab'] = $tab;
16598
16599 return $this->get_account_url( $action, $params, $add_action_nonce );
16600 }
16601
16602 /**
16603 * Plugin's account URL.
16604 *
16605 * @author Vova Feldman (@svovaf)
16606 * @since 1.0.4
16607 *
16608 * @param bool|string $topic
16609 * @param bool|string $message
16610 * @param bool|string $summary Since 2.5.1.
16611 *
16612 * @return string
16613 */
16614 function contact_url( $topic = false, $message = false, $summary = false ) {
16615 $params = array();
16616 if ( is_string( $topic ) ) {
16617 $params['topic'] = $topic;
16618 }
16619 if ( is_string( $message ) ) {
16620 $params['message'] = $message;
16621 }
16622
16623 if ( is_string( $summary ) ) {
16624 $params['summary'] = $summary;
16625 }
16626
16627 if ( $this->is_addon() ) {
16628 $params['addon_id'] = $this->get_id();
16629
16630 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
16631 } else {
16632 return $this->_get_admin_page_url( 'contact', $params );
16633 }
16634 }
16635
16636 /**
16637 * Add-on direct info URL.
16638 *
16639 * @author Vova Feldman (@svovaf)
16640 * @since 1.1.0
16641 *
16642 * @param string $slug
16643 *
16644 * @return string
16645 */
16646 function addon_url( $slug ) {
16647 return $this->_get_admin_page_url( 'addons', array(
16648 'slug' => $slug
16649 ) );
16650 }
16651
16652 /**
16653 * Add-ons URL.
16654 *
16655 * @author Vova Feldman (@svovaf)
16656 * @since 2.4.5
16657 *
16658 * @return string
16659 */
16660 function get_addons_url() {
16661 return $this->_get_admin_page_url( 'addons' );
16662 }
16663
16664 /* Logger
16665 ------------------------------------------------------------------------------------------------------------------*/
16666 /**
16667 * @param string $id
16668 * @param bool $prefix_slug
16669 *
16670 * @return FS_Logger
16671 */
16672 function get_logger( $id = '', $prefix_slug = true ) {
16673 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16674 }
16675
16676 /**
16677 * Note: This method is used externally so don't delete it.
16678 *
16679 * @param $id
16680 * @param bool $load_options
16681 * @param bool $prefix_slug
16682 *
16683 * @return FS_Option_Manager
16684 */
16685 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16686 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16687 }
16688
16689 /* Security
16690 ------------------------------------------------------------------------------------------------------------------*/
16691 private static function _encrypt( $str ) {
16692 if ( is_null( $str ) ) {
16693 return null;
16694 }
16695
16696 /**
16697 * The encrypt/decrypt functions are used to protect
16698 * the user from messing up with some of the sensitive
16699 * data stored for the module as a JSON in the database.
16700 *
16701 * I used the same suggested hack by the theme review team.
16702 * For more details, look at the function `Base64UrlDecode()`
16703 * in `./sdk/FreemiusBase.php`.
16704 *
16705 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16706 *
16707 * @author Vova Feldman (@svovaf)
16708 * @since 1.2.2
16709 */
16710 $fn = 'base64' . '_encode';
16711
16712 return $fn( $str );
16713 }
16714
16715 static function _decrypt( $str ) {
16716 if ( is_null( $str ) ) {
16717 return null;
16718 }
16719
16720 /**
16721 * The encrypt/decrypt functions are used to protect
16722 * the user from messing up with some of the sensitive
16723 * data stored for the module as a JSON in the database.
16724 *
16725 * I used the same suggested hack by the theme review team.
16726 * For more details, look at the function `Base64UrlDecode()`
16727 * in `./sdk/FreemiusBase.php`.
16728 *
16729 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16730 *
16731 * @author Vova Feldman (@svovaf)
16732 * @since 1.2.2
16733 */
16734 $fn = 'base64' . '_decode';
16735
16736 return $fn( $str );
16737 }
16738
16739 /**
16740 * @author Vova Feldman (@svovaf)
16741 * @since 1.0.5
16742 *
16743 * @param FS_Entity $entity
16744 *
16745 * @return FS_Entity Return an encrypted clone entity.
16746 */
16747 private static function _encrypt_entity( FS_Entity $entity ) {
16748 $clone = clone $entity;
16749 $props = get_object_vars( $entity );
16750
16751 foreach ( $props as $key => $val ) {
16752 $clone->{$key} = self::_encrypt( $val );
16753 }
16754
16755 return $clone;
16756 }
16757
16758 /**
16759 * @author Vova Feldman (@svovaf)
16760 * @since 1.0.5
16761 *
16762 * @param FS_Entity $entity
16763 *
16764 * @return FS_Entity Return an decrypted clone entity.
16765 */
16766 private static function decrypt_entity( FS_Entity $entity ) {
16767 $clone = clone $entity;
16768 $props = get_object_vars( $entity );
16769
16770 foreach ( $props as $key => $val ) {
16771 $clone->{$key} = self::_decrypt( $val );
16772 }
16773
16774 return $clone;
16775 }
16776
16777 /**
16778 * @author Vova Feldman (@svovaf)
16779 * @since 1.0.7
16780 *
16781 * @param string $email
16782 *
16783 * @return FS_User|false
16784 */
16785 public static function _get_user_by_email( $email ) {
16786 self::$_static_logger->entrance();
16787
16788 $email = trim( strtolower( $email ) );
16789
16790 $users = self::get_all_users();
16791
16792 if ( is_array( $users ) ) {
16793 foreach ( $users as $user ) {
16794 if ( $email === trim( strtolower( $user->email ) ) ) {
16795 return $user;
16796 }
16797 }
16798 }
16799
16800 return false;
16801 }
16802
16803 #----------------------------------------------------------------------------------
16804 #region Account (Loading, Updates & Activation)
16805 #----------------------------------------------------------------------------------
16806
16807 /***
16808 * Load account information (user + site).
16809 *
16810 * @author Vova Feldman (@svovaf)
16811 * @since 1.0.1
16812 */
16813 private function _load_account() {
16814 $this->_logger->entrance();
16815
16816 $this->do_action( 'before_account_load' );
16817
16818 $users = self::get_all_users();
16819 $plans = self::get_all_plans( $this->_module_type );
16820
16821 if ( $this->_logger->is_on() && is_admin() ) {
16822 $this->_logger->log( 'users = ' . var_export( $users, true ) );
16823 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
16824 }
16825
16826 $site = fs_is_network_admin() ?
16827 $this->get_network_install() :
16828 $this->get_install_by_blog_id();
16829
16830 if ( fs_is_network_admin() &&
16831 $this->is_network_active() &&
16832 ! is_object( $site ) &&
16833 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
16834 ) {
16835 $first_install = $this->find_first_install();
16836
16837 if ( is_null( $first_install ) ) {
16838 unset( $this->_storage->network_install_blog_id );
16839 } else {
16840 $site = $first_install['install'];
16841 $this->_storage->network_install_blog_id = $first_install['blog_id'];
16842 }
16843 }
16844
16845 if ( is_object( $site ) &&
16846 is_numeric( $site->id ) &&
16847 is_numeric( $site->user_id ) &&
16848 FS_Plugin_Plan::is_valid_id( $site->plan_id )
16849 ) {
16850 // Load site.
16851 $this->_site = $site;
16852 }
16853
16854 $user = null;
16855 if ( fs_is_network_admin() && $this->_is_network_active ) {
16856 $user = $this->get_network_user();
16857 }
16858
16859 if ( is_object( $user ) ) {
16860 $this->_user = clone $user;
16861 } else if ( $this->_site ) {
16862 $user = self::_get_user_by_id( $this->_site->user_id );
16863
16864 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
16865 /**
16866 * Try to load the previous owner. This recovery is used for the following use-case:
16867 * 1. Opt-in
16868 * 2. Cloning site1 to site2
16869 * 3. Ownership switch in site1 (same applies for site2)
16870 * 4. Install data sync on site2
16871 * 5. Now site2's install is associated with the new owner which does not exists locally.
16872 */
16873 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
16874 }
16875
16876 if ( ! is_object( $user ) ) {
16877 /**
16878 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
16879 */
16880 if (
16881 ! isset( $this->_storage->user_recovery_from_install_last_attempt_timestamp ) ||
16882 time() > ( $this->_storage->user_recovery_from_install_last_attempt_timestamp + FS_Clone_Manager::CLONE_RESOLUTION_MAX_EXECUTION_TIME )
16883 ) {
16884 $user = $this->sync_user_by_current_install();
16885 } else {
16886 return;
16887 }
16888
16889 if ( is_object( $user ) ) {
16890 $this->_storage->user_was_recovered_from_install = true;
16891 } else {
16892 $this->_storage->user_recovery_from_install_attempts = isset( $this->_storage->user_recovery_from_install_attempts ) ?
16893 ( $this->_storage->user_recovery_from_install_attempts + 1 ) :
16894 1;
16895
16896 if ( $this->_storage->user_recovery_from_install_attempts >= 3 ) {
16897 $this->delete_current_install( false );
16898 } else {
16899 $this->_storage->user_recovery_from_install_last_attempt_timestamp = time();
16900
16901 return;
16902 }
16903 }
16904 }
16905
16906 $this->_user = ( $user instanceof FS_User ) ?
16907 clone $user :
16908 null;
16909 }
16910
16911 if ( is_object( $this->_user ) ) {
16912 // Load licenses.
16913 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16914 }
16915
16916 if ( is_object( $this->_site ) ) {
16917 // Load plans.
16918 $this->_plans = isset( $plans[ $this->_slug ] ) ?
16919 $plans[ $this->_slug ] :
16920 array();
16921
16922 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
16923 $this->_sync_plans();
16924 } else {
16925 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16926 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
16927 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
16928 } else {
16929 unset( $this->_plans[ $i ] );
16930 }
16931 }
16932 }
16933
16934 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
16935
16936 if ( $this->_site->version != $this->get_plugin_version() ) {
16937 // If stored install version is different than current installed plugin version,
16938 // then update plugin version event.
16939 $this->update_plugin_version_event();
16940 }
16941 }
16942
16943 if ( true === $this->_storage->require_license_activation &&
16944 ! fs_request_get_bool( 'require_license', true )
16945 ) {
16946 $this->_storage->require_license_activation = false;
16947 }
16948
16949 if ( $this->is_theme() ) {
16950 $this->_register_account_hooks();
16951 }
16952
16953 if ( $this->is_user_in_admin() && $this->is_clone() ) {
16954 if ( empty( FS_Clone_Manager::instance()->get_clone_identification_timestamp() ) ) {
16955 FS_Clone_Manager::instance()->store_clone_identification_timestamp();
16956 }
16957
16958 FS_Clone_Manager::instance()->maybe_update_clone_resolution_support_flag( $this->_storage->sdk_last_version );
16959 $this->send_pending_clone_update_once();
16960 }
16961 }
16962
16963 /**
16964 * Special user recovery mechanism.
16965 *
16966 * @author Vova Feldman (@svovaf)
16967 * @since 2.0.0
16968 *
16969 * @param number|null $site_user_id
16970 *
16971 * @return \FS_User|mixed
16972 */
16973 private function sync_user_by_current_install( $site_user_id = null ) {
16974 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
16975 $site_user_id :
16976 $this->_site->user_id;
16977
16978 $api = $this->get_api_site_scope();
16979
16980 $uid = $this->get_anonymous_id();
16981 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
16982
16983 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
16984
16985 if ( $this->is_api_result_entity( $result ) ) {
16986 $user = new FS_User( $result );
16987 $this->_user = $user;
16988 $this->_store_user();
16989
16990 return $user;
16991 }
16992
16993 $error_code = FS_Api::get_error_code( $result );
16994
16995 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
16996 /**
16997 * Those API errors will continue coming and are not recoverable with the
16998 * current site's data. Therefore, extend the API call's cached result to 7 days.
16999 */
17000 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
17001 }
17002
17003 return $result;
17004 }
17005
17006 /**
17007 * @author Vova Feldman (@svovaf)
17008 * @since 1.0.1
17009 *
17010 * @param FS_User $user
17011 * @param FS_Site $site
17012 * @param bool|array $plans
17013 */
17014 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
17015 $site->user_id = $user->id;
17016
17017 $this->_site = $site;
17018 $this->_user = $user;
17019 if ( false !== $plans ) {
17020 $this->_plans = $plans;
17021 }
17022
17023 $this->send_install_update();
17024
17025 $this->_store_account();
17026
17027 }
17028
17029 /**
17030 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
17031 * Each version is trimmed after the 16th char.
17032 *
17033 * @author Vova Feldman (@svovaf)
17034 * @since 2.2.1
17035 *
17036 * @return array
17037 */
17038 private function get_versions() {
17039 $versions = array();
17040 $versions['sdk_version'] = $this->version;
17041
17042 // Collect these diagnostic information only if it's allowed.
17043 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17044 $versions['platform_version'] = get_bloginfo( 'version' );
17045 $versions['programming_language_version'] = phpversion();
17046 }
17047
17048 foreach ( $versions as $k => $version ) {
17049 $versions[ $k ] = self::get_api_sanitized_property( $k, $version );
17050 }
17051
17052 return $versions;
17053 }
17054
17055 /**
17056 * Get sanitized site language.
17057 *
17058 * @param string $language
17059 * @param int $max_len
17060 *
17061 * @since 2.5.1
17062 * @author Vova Feldman (@svovaf)
17063 *
17064 * @return string
17065 */
17066 private static function get_sanitized_language( $language = '', $max_len = self::LANGUAGE_MAX_CHARS ) {
17067 if ( empty( $language ) ) {
17068 $language = get_bloginfo( 'language' );
17069 }
17070
17071 return substr( $language, 0, $max_len );
17072 }
17073
17074 /**
17075 * Get core version stripped from pre-release and build.
17076 *
17077 * @since 2.5.1
17078 * @author Vova Feldman (@svovaf)
17079 *
17080 * @param string $version
17081 * @param int $parts
17082 * @param int $max_len
17083 * @param bool $include_pre_release
17084 *
17085 * @return string
17086 */
17087 private static function get_core_version(
17088 $version,
17089 $parts = 3,
17090 $max_len = self::VERSION_MAX_CHARS,
17091 $include_pre_release = false
17092 ) {
17093 if ( empty( $version ) ) {
17094 // Version is empty.
17095 return '';
17096 }
17097
17098 if ( is_numeric( $version ) ) {
17099 $is_float_version = is_float( $version );
17100
17101 $version = (string) $version;
17102
17103 /**
17104 * Casting a whole float number to a string cuts the decimal point. This part make sure to add the missing decimal part to the version.
17105 */
17106 if ( $is_float_version && false === strpos( $version, '.' ) ) {
17107 $version .= '.0';
17108 }
17109 }
17110
17111 if ( ! is_string( $version ) ) {
17112 return '';
17113 }
17114
17115 if ( $parts < 1 ) {
17116 return '';
17117 }
17118
17119 $pre_release_regex = $include_pre_release ?
17120 '(\-(alpha|beta|RC)([0-9]+)?)?' :
17121 '';
17122
17123 if ( 0 === preg_match( '/^([0-9]+(\.[0-9]+){0,' . ( $parts - 1 ) . '}' . $pre_release_regex . ')/i', $version, $matches ) ) {
17124 // Version is not starting with a digit.
17125 return '';
17126 }
17127
17128 return substr( $matches[1], 0, $max_len );
17129 }
17130
17131 /**
17132 * @param string $prop
17133 * @param mixed $val
17134 *
17135 * @return mixed
17136 *@author Vova Feldman (@svovaf)
17137 *
17138 * @since 2.5.1
17139 */
17140 private static function get_api_sanitized_property( $prop, $val ) {
17141 if ( ! is_string( $val ) || empty( $val ) ) {
17142 return $val;
17143 }
17144
17145 switch ( $prop ) {
17146 case 'programming_language_version':
17147 // Get core PHP version, which can have up to 3 parts (ignore pre-releases).
17148 return self::get_core_version( $val );
17149 case 'platform_version':
17150 // Get the exact WordPress version, which can have up to 3 parts (including pre-releases).
17151 return self::get_core_version( $val, 3, self::VERSION_MAX_CHARS, true );
17152 case 'sdk_version':
17153 // Get the exact SDK version, which can have up to 4 parts.
17154 return self::get_core_version( $val, 4 );
17155 case 'version':
17156 // Get the entire version but just limited in length.
17157 return substr( $val, 0, self::VERSION_MAX_CHARS );
17158 case 'language':
17159 return self::get_sanitized_language( $val );
17160 default:
17161 return $val;
17162 }
17163 }
17164
17165 /**
17166 * @author Leo Fajardo (@leorw)
17167 * @since 2.3.0
17168 *
17169 * @return bool
17170 */
17171 function has_beta_update() {
17172 return (
17173 ! empty( $this->_storage->beta_data ) &&
17174 ( true === $this->_storage->beta_data['is_beta'] ) &&
17175 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
17176 );
17177 }
17178
17179 /**
17180 * @author Leo Fajardo (@leorw)
17181 * @since 2.3.0
17182 *
17183 * @return bool
17184 */
17185 function is_beta() {
17186 return (
17187 ! empty( $this->_storage->beta_data ) &&
17188 ( true === $this->_storage->beta_data['is_beta'] ) &&
17189 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
17190 );
17191 }
17192
17193 /**
17194 * @author Vova Feldman (@svovaf)
17195 * @since 1.1.7.4
17196 *
17197 * @param array $override_with
17198 * @param bool|int|null $network_level_or_blog_id If true, return params for network level opt-in. If integer, get params for specified blog in the network.
17199 *
17200 * @return array
17201 */
17202 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
17203 $this->_logger->entrance();
17204
17205 $current_user = self::_get_current_wp_user();
17206
17207 $activation_action = $this->get_unique_affix() . '_activate_new';
17208 $return_url = $this->is_anonymous() ?
17209 // If skipped already, then return to the account page.
17210 $this->get_account_url( $activation_action, array(), false ) :
17211 // Return to the module's main page.
17212 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
17213
17214 $versions = $this->get_versions();
17215
17216 $params = array_merge( $versions, array(
17217 'user_firstname' => $current_user->user_firstname,
17218 'user_lastname' => $current_user->user_lastname,
17219 'user_email' => $current_user->user_email,
17220 'plugin_slug' => $this->_slug,
17221 'plugin_id' => $this->get_id(),
17222 'plugin_public_key' => $this->get_public_key(),
17223 'plugin_version' => $this->get_plugin_version(),
17224 'return_url' => fs_nonce_url( $return_url, $activation_action ),
17225 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
17226 'account',
17227 array( 'fs_action' => 'sync_user' )
17228 ), 'sync_user' ),
17229 'is_premium' => $this->is_premium(),
17230 'is_active' => true,
17231 'is_uninstalled' => false,
17232 'is_localhost' => WP_FS__IS_LOCALHOST,
17233 ) );
17234
17235 if ( $this->is_addon() ) {
17236 $parent_fs = $this->get_parent_instance();
17237
17238 $params['parent_plugin_slug'] = $parent_fs->_slug;
17239 $params['parent_plugin_id'] = $parent_fs->get_id();
17240 }
17241
17242 if ( true === $network_level_or_blog_id ) {
17243 if ( ! isset( $override_with['sites'] ) ) {
17244 $params['sites'] = $this->get_sites_for_network_level_optin();
17245 }
17246 } else {
17247 $site = is_numeric( $network_level_or_blog_id ) ?
17248 array( 'blog_id' => $network_level_or_blog_id ) :
17249 null;
17250
17251 $site = $this->get_site_info( $site );
17252
17253 $diagnostic_info = array();
17254 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17255 $diagnostic_info = array(
17256 'site_name' => $site['title'],
17257 'language' => self::get_sanitized_language( $site['language'] ),
17258 );
17259 }
17260
17261 $params = array_merge( $params, $diagnostic_info, array(
17262 'site_uid' => $site['uid'],
17263 'site_url' => $site['url'],
17264 ) );
17265 }
17266
17267 if ( $this->is_pending_activation() &&
17268 ! empty( $this->_storage->pending_license_key )
17269 ) {
17270 $params['license_key'] = $this->_storage->pending_license_key;
17271 }
17272
17273 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
17274 // Even though rand() is known for its security issues,
17275 // the timestamp adds another layer of protection.
17276 // It would be very hard for an attacker to get the secret key form here.
17277 // Plus, this should never run in production since the secret should never
17278 // be included in the production version.
17279 $params['ts'] = WP_FS__SCRIPT_START_TIME;
17280 $params['salt'] = md5( uniqid( rand() ) );
17281 $params['secure'] = md5(
17282 $params['ts'] .
17283 $params['salt'] .
17284 $this->get_secret_key()
17285 );
17286 }
17287
17288 if ( is_multisite() && function_exists( 'get_network' ) ) {
17289 $params['network_uid'] = $this->get_anonymous_network_id();
17290 }
17291
17292 return array_merge( $params, $override_with );
17293 }
17294
17295 /**
17296 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
17297 * 2. If there was an API error, return the API result.
17298 *
17299 * @author Vova Feldman (@svovaf)
17300 * @since 1.1.7.4
17301 *
17302 * @param string|bool $email
17303 * @param string|bool $first
17304 * @param string|bool $last
17305 * @param string|bool $license_key
17306 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
17307 * In this case, the user and site info will be sent to the server but no
17308 * data will be saved to the WP installation's database.
17309 * @param number|bool $trial_plan_id
17310 * @param bool $is_disconnected Whether to opt in without tracking.
17311 * @param null|bool $is_marketing_allowed
17312 * @param array $sites If network-level opt-in, an array of containing details of sites.
17313 * @param bool $redirect
17314 *
17315 * @return string|object
17316 * @use WP_Error
17317 */
17318 function opt_in(
17319 $email = false,
17320 $first = false,
17321 $last = false,
17322 $license_key = false,
17323 $is_uninstall = false,
17324 $trial_plan_id = false,
17325 $is_disconnected = false,
17326 $is_marketing_allowed = null,
17327 $sites = array(),
17328 $redirect = true
17329 ) {
17330 $this->_logger->entrance();
17331
17332 if ( false === $email ) {
17333 $current_user = self::_get_current_wp_user();
17334 $email = $current_user->user_email;
17335 }
17336
17337 /**
17338 * @since 1.2.1 If activating with license key, ignore the context-user
17339 * since the user will be automatically loaded from the license.
17340 */
17341 if ( empty( $license_key ) ) {
17342 // Clean up pending license if opt-ing in again.
17343 $this->_storage->remove( 'pending_license_key' );
17344
17345 if ( ! $is_uninstall ) {
17346 $fs_user = Freemius::_get_user_by_email( $email );
17347 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
17348 return $this->install_with_user(
17349 $fs_user,
17350 false,
17351 $trial_plan_id,
17352 $redirect,
17353 true,
17354 $sites
17355 );
17356 }
17357 }
17358 }
17359
17360 $user_info = array();
17361 if ( ! empty( $email ) ) {
17362 $user_info['user_email'] = $email;
17363 }
17364 if ( ! empty( $first ) ) {
17365 $user_info['user_firstname'] = $first;
17366 }
17367 if ( ! empty( $last ) ) {
17368 $user_info['user_lastname'] = $last;
17369 }
17370
17371 if ( ! empty( $sites ) ) {
17372 $is_network = true;
17373
17374 $user_info['sites'] = $sites;
17375 } else {
17376 $is_network = false;
17377 }
17378
17379 $params = $this->get_opt_in_params( $user_info, $is_network );
17380
17381 $filtered_license_key = false;
17382 if ( is_string( $license_key ) ) {
17383 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
17384 $params['license_key'] = $filtered_license_key;
17385 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17386 $params['trial_plan_id'] = $trial_plan_id;
17387 }
17388
17389 if ( $is_uninstall ) {
17390 $params['uninstall_params'] = array(
17391 'reason_id' => $this->_storage->uninstall_reason->id,
17392 'reason_info' => $this->_storage->uninstall_reason->info
17393 );
17394 }
17395
17396 if ( isset( $params['license_key'] ) ) {
17397 $fs_user = Freemius::_get_user_by_email( $email );
17398
17399 if ( is_object( $fs_user ) ) {
17400 /**
17401 * If opting in with a context license and the context WP Admin user already opted in
17402 * before from the current site, add the user context security params to avoid the
17403 * unnecessary email activation when the context license is owned by the same context user.
17404 *
17405 * @author Leo Fajardo (@leorw)
17406 * @since 1.2.3
17407 */
17408 $params = array_merge( $params, FS_Security::instance()->get_context_params(
17409 $fs_user,
17410 false,
17411 'install_with_existing_user'
17412 ) );
17413 }
17414 }
17415
17416 if ( is_bool( $is_marketing_allowed ) ) {
17417 $params['is_marketing_allowed'] = $is_marketing_allowed;
17418 }
17419
17420 $params['is_disconnected'] = $is_disconnected;
17421 $params['format'] = 'json';
17422 $params['is_extensions_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed();
17423 $params['is_diagnostic_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed();
17424
17425 $request = array(
17426 'method' => 'POST',
17427 'body' => $params,
17428 'timeout' => 60,
17429 );
17430
17431 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
17432 $response = self::safe_remote_post( $url, $request );
17433
17434 if ( is_wp_error( $response ) ) {
17435 /**
17436 * @var WP_Error $response
17437 */
17438 $result = new stdClass();
17439
17440 $error_code = $response->get_error_code();
17441 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
17442
17443 $result->error = (object) array(
17444 'type' => $error_type,
17445 'message' => $response->get_error_message(),
17446 'code' => $error_code,
17447 'http' => 402
17448 );
17449
17450 $this->maybe_modify_api_curl_error_message( $result );
17451
17452 if ( FS_Api::is_blocked( $result ) ) {
17453 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
17454 }
17455
17456 $is_connected = null;
17457
17458 if ( empty( $license_key ) && $this->is_enable_anonymous() ) {
17459 $this->skip_connection( fs_is_network_admin() );
17460
17461 $is_connected = ( ! FS_Api::is_blocked( $result ) );
17462 }
17463
17464 $this->update_connectivity_info( $is_connected );
17465
17466 return $result;
17467 }
17468
17469 $this->update_connectivity_info( true );
17470
17471 // Module is being uninstalled, don't handle the returned data.
17472 if ( $is_uninstall ) {
17473 return true;
17474 }
17475
17476 /**
17477 * When json_decode() executed on PHP 5.2 with an invalid JSON, it will throw a PHP warning. Unfortunately, the new Theme Check doesn't allow PHP silencing and the theme review team isn't open to change that, therefore, instead of using `@json_decode()` we had to use the method without the `@` directive.
17478 *
17479 * @author Vova Feldman (@svovaf)
17480 * @since 1.2.3
17481 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
17482 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
17483 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
17484 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
17485 */
17486 $decoded = is_string( $response['body'] ) ?
17487 json_decode( $response['body'] ) :
17488 null;
17489
17490 if ( empty( $decoded ) ) {
17491 return false;
17492 }
17493
17494 if ( ! $this->is_api_result_object( $decoded ) ) {
17495 if ( ! empty( $params['license_key'] ) ) {
17496 // Pass the fully entered license key to the failure handler.
17497 $params['license_key'] = $license_key;
17498 }
17499
17500 return $is_uninstall ?
17501 $decoded :
17502 $this->apply_filters( 'after_install_failure', $decoded, $params );
17503 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
17504 if ( $is_network ) {
17505 $site_ids = array();
17506 foreach ( $sites as $site ) {
17507 $site_ids[] = $site['blog_id'];
17508 }
17509
17510 /**
17511 * Store the sites so that they can be installed once the user has clicked on the activation link
17512 * in the email.
17513 *
17514 * @author Leo Fajardo (@leorw)
17515 */
17516 $this->_storage->pending_sites_info = array(
17517 'blog_ids' => $site_ids,
17518 'license_key' => $license_key,
17519 'trial_plan_id' => $trial_plan_id
17520 );
17521 }
17522
17523 // Pending activation, add message.
17524 return $this->set_pending_confirmation(
17525 ( isset( $decoded->email ) ?
17526 $decoded->email :
17527 true ),
17528 false,
17529 $filtered_license_key,
17530 ! empty( $params['trial_plan_id'] ),
17531 isset( $decoded->is_suspicious_email ) && $decoded->is_suspicious_email
17532 );
17533 } else if ( isset( $decoded->install_secret_key ) ) {
17534 return $this->install_with_new_user(
17535 $decoded->user_id,
17536 $decoded->user_public_key,
17537 $decoded->user_secret_key,
17538 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17539 $decoded->is_marketing_allowed :
17540 null ),
17541 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17542 $decoded->is_extensions_tracking_allowed :
17543 null ),
17544 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17545 $decoded->is_diagnostic_tracking_allowed :
17546 null ),
17547 $decoded->install_id,
17548 $decoded->install_public_key,
17549 $decoded->install_secret_key,
17550 false
17551 );
17552 } else if ( is_array( $decoded->installs ) ) {
17553 return $this->install_many_with_new_user(
17554 $decoded->user_id,
17555 $decoded->user_public_key,
17556 $decoded->user_secret_key,
17557 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17558 $decoded->is_marketing_allowed :
17559 null ),
17560 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17561 $decoded->is_extensions_tracking_allowed :
17562 null ),
17563 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17564 $decoded->is_diagnostic_tracking_allowed :
17565 null ),
17566 $decoded->installs,
17567 false
17568 );
17569 }
17570
17571 return $decoded;
17572 }
17573
17574 /**
17575 * Set user and site identities.
17576 *
17577 * @author Vova Feldman (@svovaf)
17578 * @since 1.0.9
17579 *
17580 * @param FS_User $user
17581 * @param FS_Site $site
17582 * @param bool $redirect
17583 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
17584 * redirect (or return a URL) to the account page with a special parameter to
17585 * trigger the auto installation processes.
17586 *
17587 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17588 */
17589 function setup_account(
17590 FS_User $user,
17591 FS_Site $site,
17592 $redirect = true,
17593 $auto_install = false
17594 ) {
17595 return $this->setup_network_account(
17596 $user,
17597 array( $site ),
17598 $redirect,
17599 $auto_install,
17600 false
17601 );
17602 }
17603
17604 /**
17605 * Set user and site identities.
17606 *
17607 * @author Vova Feldman (@svovaf)
17608 * @since 2.0.0
17609 *
17610 * @param FS_User $user
17611 * @param FS_Site[] $installs
17612 * @param bool $redirect
17613 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will redirect (or return a URL) to the account page with a special parameter to trigger the auto installation processes.
17614 * @param bool $is_network_level_opt_in
17615 *
17616 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17617 */
17618 function setup_network_account(
17619 FS_User $user,
17620 array $installs,
17621 $redirect = true,
17622 $auto_install = false,
17623 $is_network_level_opt_in = true
17624 ) {
17625 $first_install = $installs[0];
17626
17627 $this->_user = $user;
17628 $this->_site = $first_install;
17629
17630 $this->_sync_plans();
17631
17632 if ( $this->_storage->handle_gdpr_admin_notice &&
17633 $this->should_handle_gdpr_admin_notice() &&
17634 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
17635 ) {
17636 /**
17637 * Clear user lock after an opt-in.
17638 */
17639 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
17640 FS_User_Lock::instance()->unlock();
17641 }
17642
17643 if ( 1 < count( $installs ) ) {
17644 // Only network level opt-in can have more than one install.
17645 $is_network_level_opt_in = true;
17646 }
17647
17648 $this->update_connectivity_info( true );
17649
17650 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
17651 // If Freemius was OFF before, turn it on.
17652 $this->turn_on();
17653
17654 $this->handle_account_connection(
17655 $installs,
17656 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
17657 );
17658
17659 if ( is_numeric( $first_install->license_id ) ) {
17660 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
17661 }
17662
17663 $this->_admin_notices->remove_sticky( 'connect_account' );
17664
17665 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
17666 $this->clear_pending_activation_mode();
17667
17668 if ( ! $this->is_paying_or_trial() ) {
17669 $this->_admin_notices->add_sticky(
17670 sprintf( $this->get_text_inline( '%s opt-in was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
17671 'activation_complete'
17672 );
17673 }
17674 }
17675
17676 if ( $this->is_paying_or_trial() ) {
17677 if ( ! $this->is_premium() ||
17678 ! $this->has_premium_version() ||
17679 ! $this->has_settings_menu()
17680 ) {
17681 if ( $this->is_paying() ) {
17682 $this->add_complete_upgrade_instructions_notice(
17683 sprintf(
17684 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
17685 $this->get_plan_title()
17686 ),
17687 'plan_upgraded'
17688 );
17689 } else {
17690 $trial_plan = $this->get_trial_plan();
17691
17692 $this->add_complete_upgrade_instructions_notice(
17693 sprintf(
17694 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
17695 '<i>' . $this->get_plugin_name() . '</i>'
17696 ),
17697 'trial_started',
17698 $trial_plan->title
17699 );
17700 }
17701 }
17702
17703 $this->_admin_notices->remove_sticky( array(
17704 'trial_promotion',
17705 ) );
17706 }
17707
17708 $plugin_id = fs_request_get( 'plugin_id', false );
17709
17710 // Store activation time ONLY for plugins & themes (not add-ons).
17711 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
17712 if ( empty( $this->_storage->activation_timestamp ) ) {
17713 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
17714 }
17715 }
17716
17717 $next_page = '';
17718
17719 $extra = array();
17720 if ( $auto_install ) {
17721 $extra['auto_install'] = 'true';
17722 }
17723
17724 if ( is_numeric( $plugin_id ) ) {
17725 /**
17726 * @author Leo Fajardo (@leorw)
17727 * @since 1.2.1.6
17728 *
17729 * Also sync the license after an anonymous user subscribes.
17730 */
17731 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
17732 // Add-on was installed - sync license right after install.
17733 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
17734 }
17735 } else {
17736 /**
17737 * @author Vova Feldman (@svovaf)
17738 * @since 1.1.9 If site installed with a valid license, sync license.
17739 */
17740 if ( $this->is_paying() ) {
17741 $this->_sync_plugin_license(
17742 true,
17743 // Installs data is already synced in the beginning of this method directly or via _set_account().
17744 false
17745 );
17746 }
17747
17748 // Reload the page with the keys.
17749 $next_page = $this->is_anonymous() ?
17750 // If user previously skipped, redirect to account page.
17751 $this->get_account_url( false, $extra ) :
17752 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
17753 }
17754
17755 if ( ! empty( $next_page ) && $redirect ) {
17756 fs_redirect( $next_page );
17757 }
17758
17759 return $next_page;
17760 }
17761
17762 /**
17763 * Install plugin with new user information after approval.
17764 *
17765 * @author Vova Feldman (@svovaf)
17766 * @since 1.0.7
17767 */
17768 function _install_with_new_user() {
17769 $this->_logger->entrance();
17770
17771 if ( $this->is_registered() ) {
17772 return;
17773 }
17774
17775 $has_pending_activation_confirmation_param = fs_request_has( 'pending_activation' );
17776
17777 $this->update_license_required_permissions_if_anonymous();
17778
17779 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17780 // @todo This logic should be improved because it's executed on every load of a theme.
17781 $this->is_theme()
17782 ) {
17783 // check_admin_referer( $this->_slug . '_activate_new' );
17784
17785 if ( fs_request_has( 'user_secret_key' ) ) {
17786 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17787 $pending_sites_info = $this->_storage->pending_sites_info;
17788
17789 $this->install_many_pending_with_user(
17790 fs_request_get( 'user_id' ),
17791 fs_request_get_raw( 'user_public_key' ),
17792 fs_request_get_raw( 'user_secret_key' ),
17793 fs_request_get_bool( 'is_marketing_allowed', null ),
17794 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17795 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17796 $pending_sites_info['blog_ids'],
17797 $pending_sites_info['license_key'],
17798 $pending_sites_info['trial_plan_id']
17799 );
17800 } else {
17801 $this->install_with_new_user(
17802 fs_request_get( 'user_id' ),
17803 fs_request_get_raw( 'user_public_key' ),
17804 fs_request_get_raw( 'user_secret_key' ),
17805 fs_request_get_bool( 'is_marketing_allowed', null ),
17806 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17807 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17808 fs_request_get( 'install_id' ),
17809 fs_request_get_raw( 'install_public_key' ),
17810 fs_request_get_raw( 'install_secret_key' ),
17811 true,
17812 fs_request_get_bool( 'auto_install' )
17813 );
17814 }
17815 } else if ( $has_pending_activation_confirmation_param ) {
17816 $this->set_pending_confirmation(
17817 fs_request_get( 'user_email' ),
17818 true,
17819 false,
17820 false,
17821 fs_request_get_bool( 'is_suspicious_email' ),
17822 fs_request_get_bool( 'has_upgrade_context' ),
17823 fs_request_get( 'support_email_address' )
17824 );
17825 }
17826 }
17827 }
17828
17829 /**
17830 * @author Vova Feldman (@svovaf)
17831 * @since 2.0.0
17832 *
17833 * @param number $id
17834 * @param string $public_key
17835 * @param string $secret_key
17836 *
17837 * @return \FS_User
17838 */
17839 private function setup_user( $id, $public_key, $secret_key ) {
17840 $user = self::_get_user_by_id( $id );
17841
17842 if ( is_object( $user ) ) {
17843 $this->_user = $user;
17844 } else {
17845 $user = new FS_User();
17846 $user->id = $id;
17847 $user->public_key = $public_key;
17848 $user->secret_key = $secret_key;
17849
17850 $this->_user = $user;
17851 $user_result = $this->get_api_user_scope()->get();
17852 $user = new FS_User( $user_result );
17853
17854 $this->_user = $user;
17855 $this->_store_user();
17856 }
17857
17858 return $user;
17859 }
17860
17861 /**
17862 * Install plugin with new user.
17863 *
17864 * @author Vova Feldman (@svovaf)
17865 * @since 1.1.7.4
17866 *
17867 * @param number $user_id
17868 * @param string $user_public_key
17869 * @param string $user_secret_key
17870 * @param bool|null $is_marketing_allowed
17871 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17872 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17873 * @param number $install_id
17874 * @param string $install_public_key
17875 * @param string $install_secret_key
17876 * @param bool $redirect
17877 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will redirect (or return a URL) to the account page with a special parameter to trigger the auto installation processes.
17878 *
17879 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17880 */
17881 private function install_with_new_user(
17882 $user_id,
17883 $user_public_key,
17884 $user_secret_key,
17885 $is_marketing_allowed,
17886 $is_extensions_tracking_allowed,
17887 $is_diagnostic_tracking_allowed,
17888 $install_id,
17889 $install_public_key,
17890 $install_secret_key,
17891 $redirect = true,
17892 $auto_install = false
17893 ) {
17894 /**
17895 * This method is also executed after opting in with a license key since the
17896 * license can be potentially associated with a different owner.
17897 *
17898 * @since 2.0.0
17899 */
17900 $user = self::_get_user_by_id( $user_id );
17901
17902 if ( ! is_object( $user ) ) {
17903 $user = new FS_User();
17904 $user->id = $user_id;
17905 $user->public_key = $user_public_key;
17906 $user->secret_key = $user_secret_key;
17907
17908 $this->_user = $user;
17909 $user_result = $this->get_api_user_scope()->get();
17910 $user = new FS_User( $user_result );
17911 }
17912
17913 $this->_user = $user;
17914
17915 $site = new FS_Site();
17916 $site->id = $install_id;
17917 $site->public_key = $install_public_key;
17918 $site->secret_key = $install_secret_key;
17919
17920 $this->_site = $site;
17921 $site_result = $this->get_api_site_scope( true )->get();
17922 $site = new FS_Site( $site_result );
17923 $this->_site = $site;
17924
17925 if ( ! is_null( $is_marketing_allowed ) ) {
17926 $this->disable_opt_in_notice_and_lock_user();
17927 }
17928
17929 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17930 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17931 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17932 ) );
17933
17934 return $this->setup_account(
17935 $this->_user,
17936 $this->_site,
17937 $redirect,
17938 $auto_install
17939 );
17940 }
17941
17942 /**
17943 * Install plugin with user.
17944 *
17945 * @author Leo Fajardo (@leorw)
17946 * @since 2.0.0
17947 *
17948 * @param number $user_id
17949 * @param string $user_public_key
17950 * @param string $user_secret_key
17951 * @param bool|null $is_marketing_allowed
17952 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17953 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17954 * @param array $site_ids
17955 * @param bool $license_key
17956 * @param bool $trial_plan_id
17957 * @param bool $redirect
17958 *
17959 * @return void
17960 */
17961 private function install_many_pending_with_user(
17962 $user_id,
17963 $user_public_key,
17964 $user_secret_key,
17965 $is_marketing_allowed,
17966 $is_extensions_tracking_allowed,
17967 $is_diagnostic_tracking_allowed,
17968 $site_ids,
17969 $license_key = false,
17970 $trial_plan_id = false,
17971 $redirect = true
17972 ) {
17973 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17974
17975 if ( ! is_null( $is_marketing_allowed ) ) {
17976 $this->disable_opt_in_notice_and_lock_user();
17977 }
17978
17979 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17980 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17981 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17982 ) );
17983
17984 $sites = array();
17985 foreach ( $site_ids as $site_id ) {
17986 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
17987 }
17988
17989 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17990 }
17991
17992 /**
17993 * Multi-site install with a new user.
17994 *
17995 * @author Vova Feldman (@svovaf)
17996 * @since 2.0.0
17997 *
17998 * @param number $user_id
17999 * @param string $user_public_key
18000 * @param string $user_secret_key
18001 * @param bool|null $is_marketing_allowed
18002 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
18003 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
18004 * @param object[] $installs
18005 * @param bool $redirect
18006 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will redirect (or return a URL) to the account page with a special parameter to trigger the auto installation processes.
18007 *
18008 * @return string If redirect is `false`, returns the next page the user should be redirected to.
18009 */
18010 private function install_many_with_new_user(
18011 $user_id,
18012 $user_public_key,
18013 $user_secret_key,
18014 $is_marketing_allowed,
18015 $is_extensions_tracking_allowed,
18016 $is_diagnostic_tracking_allowed,
18017 array $installs,
18018 $redirect = true,
18019 $auto_install = false
18020 ) {
18021 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
18022
18023 if ( ! is_null( $is_marketing_allowed ) ) {
18024 $this->disable_opt_in_notice_and_lock_user();
18025 }
18026
18027 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
18028 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
18029 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
18030 ) );
18031
18032 $install_ids = array();
18033
18034 foreach ( $installs as $install ) {
18035 $install_ids[] = $install->id;
18036 }
18037
18038 $items_per_request = 25;
18039 $left = count( $install_ids );
18040 $offset = 0;
18041
18042 $installs = array();
18043 while ( $left > 0 ) {
18044 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, $items_per_request ) ) );
18045
18046 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18047 // @todo Handle API error.
18048 }
18049
18050 $installs = array_merge( $installs, $result->installs );
18051
18052 $left -= $items_per_request;
18053 $offset += $items_per_request;
18054 }
18055
18056 foreach ( $installs as &$install ) {
18057 $install = new FS_Site( $install );
18058 }
18059
18060 return $this->setup_network_account(
18061 $this->_user,
18062 $installs,
18063 $redirect,
18064 $auto_install
18065 );
18066 }
18067
18068 /**
18069 * @author Vova Feldman (@svovaf)
18070 * @since 1.1.7.4
18071 *
18072 * @param string|bool $email
18073 * @param bool $redirect
18074 * @param string|bool $license_key Since 1.2.1.5
18075 * @param bool $is_pending_trial Since 1.2.1.5
18076 * @param bool $is_suspicious_email Since 2.5.0
18077 * @param bool $has_upgrade_context Since 2.5.3
18078 * @param bool|string $support_email_address Since 2.5.3
18079 *
18080 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
18081 */
18082 private function set_pending_confirmation(
18083 $email = false,
18084 $redirect = true,
18085 $license_key = false,
18086 $is_pending_trial = false,
18087 $is_suspicious_email = false,
18088 $has_upgrade_context = false,
18089 $support_email_address = false
18090 ) {
18091 $is_network_admin = fs_is_network_admin();
18092
18093 if ( $this->_ignore_pending_mode && ! $has_upgrade_context ) {
18094 /**
18095 * If explicitly asked to ignore pending mode, set to anonymous mode
18096 * if require confirmation before finalizing the opt-in except after completing a purchase (otherwise, in this case, they wouldn't see any notice telling them that they should receive their license key via email).
18097 *
18098 * @author Vova Feldman
18099 * @since 1.2.1.6
18100 */
18101 $this->skip_connection( $is_network_admin );
18102 } else {
18103 // Install must be activated via email since
18104 // user with the same email already exist.
18105 $this->_storage->is_pending_activation = true;
18106 $this->_add_pending_activation_notice(
18107 $email,
18108 $is_pending_trial,
18109 $is_suspicious_email,
18110 $has_upgrade_context,
18111 $support_email_address
18112 );
18113 }
18114
18115 if ( ! empty( $license_key ) ) {
18116 $this->_storage->pending_license_key = $license_key;
18117 }
18118
18119 // Remove the opt-in sticky notice.
18120 $this->_admin_notices->remove_sticky( array(
18121 'connect_account',
18122 'trial_promotion',
18123 ) );
18124
18125 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
18126
18127 if ( $redirect ) {
18128 // Reload the page with a pending activation message.
18129 fs_redirect( $next_page );
18130 }
18131
18132 return $next_page;
18133 }
18134
18135 /**
18136 * Install plugin with current logged WP user info.
18137 *
18138 * @author Vova Feldman (@svovaf)
18139 * @since 1.0.7
18140 */
18141 function _install_with_current_user() {
18142 $this->_logger->entrance();
18143
18144 if ( $this->is_registered() ) {
18145 return;
18146 }
18147
18148 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
18149 check_admin_referer( $this->get_unique_affix() . '_activate_existing' );
18150
18151 /**
18152 * @author Vova Feldman (@svovaf)
18153 * @since 1.1.9 Add license key if given.
18154 */
18155 $license_key = fs_request_get_raw( 'license_secret_key' );
18156
18157 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
18158 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
18159 FS_Permission_Manager::PERMISSION_EXTENSIONS => fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
18160 ) );
18161
18162 $this->install_with_current_user( $license_key );
18163 }
18164 }
18165
18166
18167 /**
18168 * @author Vova Feldman (@svovaf)
18169 * @since 1.1.7.4
18170 *
18171 * @param string|bool $license_key
18172 * @param number|bool $trial_plan_id
18173 * @param array $sites Since 2.0.0
18174 * @param bool $redirect
18175 *
18176 * @return object|string If redirect is `false`, returns the next page the user should be redirected to, or the API error object if failed to install.
18177 */
18178 function install_with_current_user(
18179 $license_key = false,
18180 $trial_plan_id = false,
18181 $sites = array(),
18182 $redirect = true
18183 ) {
18184 // Get current logged WP user.
18185 $current_user = self::_get_current_wp_user();
18186
18187 // Find the relevant FS user by the email.
18188 $user = self::_get_user_by_email( $current_user->user_email );
18189
18190 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
18191 }
18192
18193 /**
18194 * @author Vova Feldman (@svovaf)
18195 * @since 2.0.0
18196 *
18197 * @param \FS_User $user
18198 * @param string|bool $license_key
18199 * @param number|bool $trial_plan_id
18200 * @param bool $redirect
18201 * @param bool $setup_account Since 2.0.0. When set to FALSE, executes a light installation without setting up the account as if it's the first opt-in.
18202 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
18203 *
18204 * @return \FS_Site|object|string If redirect is `false`, returns the next page the user should be redirected to, or the API error object if failed to install. If $setup_account is set to `false`, return the newly created install.
18205 */
18206 function install_with_user(
18207 FS_User $user,
18208 $license_key = false,
18209 $trial_plan_id = false,
18210 $redirect = true,
18211 $setup_account = true,
18212 $sites = array()
18213 ) {
18214 // We have to set the user before getting user scope API handler.
18215 $this->_user = $user;
18216
18217 // Install the plugin.
18218 $result = $this->create_installs_with_user(
18219 $user,
18220 $license_key,
18221 $trial_plan_id,
18222 $sites,
18223 $redirect
18224 );
18225
18226 if ( ! $this->is_api_result_entity( $result ) &&
18227 ! $this->is_api_result_object( $result, 'installs' )
18228 ) {
18229 // @todo Handler potential API error of the $result
18230 }
18231
18232 if ( empty( $sites ) ) {
18233 $site = new FS_Site( $result );
18234 $this->_site = $site;
18235
18236 if ( ! $setup_account ) {
18237 $this->_store_site();
18238
18239 $this->sync_plan_if_not_exist( $site->plan_id );
18240
18241 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
18242 $this->sync_license_if_not_exist( $site->license_id, $license_key );
18243 }
18244
18245 $this->_admin_notices->remove_sticky( 'connect_account', false );
18246
18247 return $site;
18248 }
18249
18250 return $this->setup_account( $this->_user, $this->_site, $redirect );
18251 } else {
18252 $installs = array();
18253 foreach ( $result->installs as $install ) {
18254 $installs[] = new FS_Site( $install );
18255 }
18256
18257 return $this->setup_network_account(
18258 $user,
18259 $installs,
18260 $redirect
18261 );
18262 }
18263 }
18264
18265 /**
18266 * Initiate an API request to create a collection of installs.
18267 *
18268 * @author Vova Feldman (@svovaf)
18269 * @since 2.0.0
18270 *
18271 * @param \FS_User $user
18272 * @param bool $license_key
18273 * @param bool $trial_plan_id
18274 * @param array $sites
18275 * @param bool $redirect
18276 * @param bool $silent
18277 *
18278 * @return object|mixed
18279 */
18280 private function create_installs_with_user(
18281 FS_User $user,
18282 $license_key = false,
18283 $trial_plan_id = false,
18284 $sites = array(),
18285 $redirect = false,
18286 $silent = false
18287 ) {
18288 $extra_install_params = array(
18289 'uid' => $this->get_anonymous_id(),
18290 'is_disconnected' => false,
18291 );
18292
18293 if ( ! empty( $license_key ) ) {
18294 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
18295
18296 if ( $silent ) {
18297 $extra_install_params['ignore_license_owner'] = true;
18298 }
18299 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
18300 $extra_install_params['trial_plan_id'] = $trial_plan_id;
18301 }
18302
18303 if ( ! empty( $sites ) ) {
18304 $extra_install_params['sites'] = $sites;
18305 }
18306
18307 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
18308
18309 // Install the plugin.
18310 $result = $this->get_api_user_scope_by_user( $user )->call(
18311 "/plugins/{$this->get_id()}/installs.json",
18312 'post',
18313 $args
18314 );
18315
18316 if ( ! $this->is_api_result_entity( $result ) &&
18317 ! $this->is_api_result_object( $result, 'installs' )
18318 ) {
18319 if ( ! empty( $args['license_key'] ) ) {
18320 // Pass the fully entered license key to the failure handler.
18321 $args['license_key'] = $license_key;
18322 }
18323
18324 $result = $this->apply_filters( 'after_install_failure', $result, $args );
18325
18326 if ( ! $silent ) {
18327 $this->_admin_notices->add(
18328 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18329 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
18330 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18331 'error'
18332 );
18333 }
18334
18335 if ( $redirect ) {
18336 /**
18337 * We set the user before getting the user scope API handler, so the user became temporarily
18338 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
18339 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
18340 * activation page based on the return value of `is_registered()`. In addition, in case the
18341 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
18342 * misleading plugin activation errors will be shown on the `Plugins` page.
18343 *
18344 * @author Leo Fajardo (@leorw)
18345 */
18346 $this->_user = null;
18347
18348 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
18349 }
18350 }
18351
18352 return $result;
18353 }
18354
18355 /**
18356 * Tries to activate add-on account based on parent plugin info.
18357 *
18358 * @author Vova Feldman (@svovaf)
18359 * @since 1.0.6
18360 *
18361 * @param Freemius $parent_fs
18362 * @param bool|int|null $network_level_or_blog_id True for network level opt-in and integer for opt-in for specified blog in the network.
18363 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
18364 */
18365 private function _activate_addon_account(
18366 Freemius $parent_fs,
18367 $network_level_or_blog_id = null,
18368 FS_Plugin_License $bundle_license = null
18369 ) {
18370 if ( $this->is_registered() ) {
18371 // Already activated.
18372 return;
18373 }
18374
18375 $permission_ids = FS_Permission_Manager::get_all_permission_ids();
18376 $permissions = array();
18377 foreach ( $permission_ids as $permission_id ) {
18378 $permissions[ $permission_id ] = FS_Permission_Manager::instance( $parent_fs )->is_permission( $permission_id, true );
18379 }
18380
18381 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( $permissions );
18382
18383 /**
18384 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
18385 * already returns the data for the current blog.
18386 *
18387 * @author Leo Fajardo (@leorw)
18388 * @since 2.3.0
18389 */
18390 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
18391 array() :
18392 array( 'uid' => $this->get_anonymous_id() );
18393
18394 $params = $this->get_install_data_for_api(
18395 $uid_param_to_override,
18396 false,
18397 false,
18398 /**
18399 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
18400 * already includes the data for it.
18401 *
18402 * @author Leo Fajardo (@leorw)
18403 * @since 2.3.0
18404 */
18405 ( true !== $network_level_or_blog_id )
18406 );
18407
18408 if ( true === $network_level_or_blog_id ) {
18409 $params['sites'] = $this->get_sites_for_network_level_optin();
18410
18411 if ( empty( $params['sites'] ) ) {
18412 return;
18413 }
18414 }
18415
18416 if ( is_object( $bundle_license ) ) {
18417 $params['license_key'] = $bundle_license->secret_key;
18418 }
18419
18420 // Activate add-on with parent plugin credentials.
18421 $result = $parent_fs->get_api_site_scope()->call(
18422 "/addons/{$this->_plugin->id}/installs.json",
18423 'post',
18424 $params
18425 );
18426
18427 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18428 if ( is_object( $bundle_license ) ) {
18429 /**
18430 * When a license object is provided, it's an attempt by the SDK to activate a bundle license and not a user-initiated action, therefore, do not show any admin notice to avoid confusion (e.g.: the notice will show up just above the opt-in link). If the license activation fails, the admin will see an opt-in link instead.
18431 *
18432 * @author Leo Fajardo (@leorw)
18433 * @since 2.4.0
18434 */
18435 } else {
18436 $error_message = FS_Api::is_api_error_object( $result ) ?
18437 $result->error->message :
18438 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
18439
18440 $this->_admin_notices->add(
18441 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18442 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
18443 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18444 'error'
18445 );
18446 }
18447
18448 return;
18449 }
18450
18451 $addon_installs = $result->installs;
18452 foreach ( $addon_installs as $key => $addon_install ) {
18453 $addon_installs[ $key ] = new FS_Site( $addon_install );
18454 }
18455
18456 $first_install = $addon_installs[0];
18457
18458 // Get user information based on parent's plugin.
18459 $user = $parent_fs->get_user();
18460
18461 // First of all, set site and user info - otherwise we won't
18462 // be able to invoke API calls.
18463 $this->_site = $first_install;
18464 $this->_user = $user;
18465
18466 // Sync add-on plans.
18467 $this->_sync_plans();
18468
18469 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
18470
18471 // Get site's current plan.
18472 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
18473
18474 // Sync licenses.
18475 $this->_sync_licenses();
18476
18477 if ( ! fs_is_network_admin() ) {
18478 // Try to activate premium license.
18479 $this->_activate_license( true, $bundle_license );
18480
18481 if ( is_object( $bundle_license ) ) {
18482 $this->maybe_activate_bundle_license( $bundle_license );
18483 }
18484 } else {
18485 if ( is_object( $bundle_license ) ) {
18486 $premium_license = $bundle_license;
18487 } else {
18488 $license_id = fs_request_get( 'license_id' );
18489
18490 if ( is_object( $this->_site ) &&
18491 FS_Plugin_License::is_valid_id( $license_id ) &&
18492 $license_id == $this->_site->license_id
18493 ) {
18494 // License is already activated.
18495 return;
18496 }
18497
18498 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
18499 $this->_get_license_by_id( $license_id ) :
18500 $this->_get_available_premium_license();
18501 }
18502
18503 if ( is_object( $premium_license ) ) {
18504 $this->maybe_network_activate_addon_license( $premium_license );
18505 }
18506 }
18507 }
18508
18509 /**
18510 * @author Leo Fajardo (@leorw)
18511 * @since 2.3.0
18512 *
18513 * @param FS_Site[] $installs
18514 * @param bool $is_site_level
18515 */
18516 private function handle_account_connection( $installs, $is_site_level ) {
18517 $first_install = $installs[0];
18518
18519 if ( $is_site_level ) {
18520 $this->_set_account( $this->_user, $first_install );
18521
18522 $this->do_action( 'after_account_connection', $this->_user, $first_install );
18523 } else {
18524 $this->_store_user();
18525
18526 // Map site addresses to their blog IDs.
18527 $address_to_blog_map = $this->get_address_to_blog_map();
18528
18529 $first_blog_id = null;
18530 $blog_2_install_map = array();
18531 foreach ( $installs as $install ) {
18532 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
18533 $blog_id = $address_to_blog_map[ $address ];
18534
18535 $this->_store_site( true, $blog_id, $install );
18536
18537 if ( is_null( $first_blog_id ) ) {
18538 $first_blog_id = $blog_id;
18539 }
18540
18541 $blog_2_install_map[ $blog_id ] = $install;
18542 }
18543
18544 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
18545 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
18546 ) {
18547 // Store network user.
18548 $this->_storage->network_user_id = $this->_user->id;
18549 }
18550
18551 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
18552 $this->_storage->network_install_blog_id = $first_blog_id;
18553 }
18554
18555 if ( count( $installs ) === count( $address_to_blog_map ) ) {
18556 // Super admin opted in for all sites in the network.
18557 $this->_storage->is_network_connected = true;
18558 }
18559
18560 $this->_store_licenses( false );
18561
18562 self::$_accounts->store();
18563
18564 // Don't sync the installs data on network upgrade
18565 if ( ! $this->network_upgrade_mode_completed() ) {
18566 $this->send_installs_update();
18567 }
18568
18569 $current_blog = get_current_blog_id();
18570
18571 foreach ( $blog_2_install_map as $blog_id => $install ) {
18572 $this->switch_to_blog( $blog_id );
18573
18574 $this->do_action( 'after_account_connection', $this->_user, $install );
18575 }
18576
18577 // Switch install context back to the first install.
18578 $this->switch_to_blog(
18579 $current_blog,
18580 $first_install,
18581 ( $this->_site->id != $first_install->id )
18582 );
18583
18584 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
18585 }
18586 }
18587
18588 /**
18589 * Tries to activate parent account based on add-on's info.
18590 *
18591 * @author Vova Feldman (@svovaf)
18592 * @since 1.2.2.7
18593 *
18594 * @param Freemius $parent_fs
18595 */
18596 private function activate_parent_account( Freemius $parent_fs ) {
18597 if ( ! $this->is_addon() ) {
18598 // This is not an add-on.
18599 return;
18600 }
18601
18602 if ( $parent_fs->is_registered() ) {
18603 // Already activated.
18604 return;
18605 }
18606
18607 // Activate parent with add-on's user credentials.
18608 $parent_install = $this->get_api_user_scope()->call(
18609 "/plugins/{$parent_fs->_plugin->id}/installs.json",
18610 'post',
18611 $parent_fs->get_install_data_for_api( array(
18612 'uid' => $parent_fs->get_anonymous_id(),
18613 ), false, false )
18614 );
18615
18616 if ( isset( $parent_install->error ) ) {
18617 $this->_admin_notices->add(
18618 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18619 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
18620 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18621 'error'
18622 );
18623
18624 return;
18625 }
18626
18627 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
18628
18629 if ( $parent_fs->is_pending_activation() ) {
18630 $parent_fs->clear_pending_activation_mode();
18631 }
18632
18633 // Get user information based on parent's plugin.
18634 $user = $this->get_user();
18635
18636 // First of all, set site info - otherwise we won't
18637 // be able to invoke API calls.
18638 $parent_fs->_site = new FS_Site( $parent_install );
18639 $parent_fs->_user = $user;
18640
18641 // Sync add-on plans.
18642 $parent_fs->_sync_plans();
18643
18644 $parent_fs->update_license_required_permissions_if_anonymous();
18645
18646 $parent_fs->_set_account( $user, $parent_fs->_site );
18647 }
18648
18649 #endregion
18650
18651 #----------------------------------------------------------------------------------
18652 #region Admin Menu Items
18653 #----------------------------------------------------------------------------------
18654
18655 private $_menu_items = array();
18656
18657 /**
18658 * @author Vova Feldman (@svovaf)
18659 * @since 1.2.1.8
18660 *
18661 * @return array
18662 */
18663 function get_menu_items() {
18664 return $this->_menu_items;
18665 }
18666
18667 /**
18668 * @author Vova Feldman (@svovaf)
18669 * @since 1.0.7
18670 *
18671 * @return string
18672 */
18673 function get_menu_slug() {
18674 return $this->_menu->get_slug();
18675 }
18676
18677 /**
18678 * @author Vova Feldman (@svovaf)
18679 * @since 1.0.9
18680 */
18681 function _prepare_admin_menu() {
18682 // if ( ! $this->is_on() ) {
18683 // return;
18684 // }
18685
18686 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
18687 return;
18688 }
18689
18690 /**
18691 * When running from a site admin with a network activated module and the connection
18692 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
18693 * site level settings.
18694 *
18695 * @author Vova Feldman (@svovaf)
18696 * @since 2.0.0
18697 */
18698 $should_hide_site_admin_settings = (
18699 $this->_is_network_active &&
18700 ! fs_is_network_admin() &&
18701 ! $this->is_delegated_connection() &&
18702 ! $this->is_anonymous() &&
18703 ! $this->is_registered()
18704 );
18705
18706 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
18707
18708 if ( ( false === $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
18709 $should_hide_site_admin_settings
18710 ) {
18711 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
18712 } else {
18713 $this->do_action( fs_is_network_admin() ?
18714 'before_network_admin_menu_init' :
18715 'before_admin_menu_init'
18716 );
18717
18718 $this->add_menu_action();
18719
18720 $this->add_network_menu_when_missing();
18721
18722 $this->add_submenu_items();
18723 }
18724 }
18725
18726 /**
18727 * Admin dashboard menu items modifications.
18728 *
18729 * NOTE: admin_menu action executed before admin_init.
18730 *
18731 * @author Vova Feldman (@svovaf)
18732 * @since 1.0.7
18733 *
18734 */
18735 private function add_menu_action() {
18736 if ( $this->is_activation_mode() ) {
18737 if ( $this->show_opt_in_on_setting_page() ) {
18738 $this->override_plugin_menu_with_activation();
18739 } else {
18740 /**
18741 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
18742 */
18743 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18744 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
18745 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
18746 fs_request_get_bool( 'pending_activation' )
18747 ) {
18748 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
18749 }
18750 }
18751 } else {
18752 if ( ! $this->is_registered() ) {
18753 // If not registered try to install user.
18754 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18755 $this->_install_with_new_user();
18756 }
18757 } else if (
18758 fs_request_is_action( 'sync_user' ) &&
18759 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
18760 ) {
18761 $this->_handle_account_user_sync();
18762 }
18763 }
18764 }
18765
18766 /**
18767 * @author Vova Feldman (@svovaf)
18768 * @since 1.0.1
18769 */
18770 function _redirect_on_clicked_menu_link() {
18771 $this->_logger->entrance();
18772
18773 $page = fs_request_get('page');
18774 $page = is_string($page) ? strtolower($page) : '';
18775
18776 $this->_logger->log( 'page = ' . $page );
18777
18778 foreach ( $this->_menu_items as $priority => $items ) {
18779 foreach ( $items as $item ) {
18780 if ( isset( $item['url'] ) ) {
18781 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
18782 $this->_logger->log( 'Redirecting to ' . $item['url'] );
18783
18784 fs_redirect( $item['url'] );
18785 }
18786 }
18787 }
18788 }
18789 }
18790
18791 /**
18792 * Remove plugin's all admin menu items & pages, and replace with activation page.
18793 *
18794 * @author Vova Feldman (@svovaf)
18795 * @since 1.0.1
18796 */
18797 private function override_plugin_menu_with_activation() {
18798 $this->_logger->entrance();
18799
18800 $hook = false;
18801
18802 if ( ! $this->has_settings_menu() ) {
18803 // Add the opt-in page without a menu item.
18804 $hook = FS_Admin_Menu_Manager::add_subpage(
18805 '',
18806 $this->get_plugin_name(),
18807 $this->get_plugin_name(),
18808 'manage_options',
18809 $this->_slug,
18810 array( &$this, '_connect_page_render' )
18811 );
18812 } else if ( $this->_menu->is_top_level() ) {
18813 if ( $this->_menu->is_override_exact() ) {
18814 // Make sure the current page is matching the activation page.
18815 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18816 return;
18817 }
18818 }
18819
18820 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18821
18822 if ( false === $hook ) {
18823 // Create new menu item just for the opt-in.
18824 $hook = FS_Admin_Menu_Manager::add_page(
18825 $this->get_plugin_name(),
18826 $this->get_plugin_name(),
18827 'manage_options',
18828 $this->_menu->get_slug(),
18829 array( &$this, '_connect_page_render' )
18830 );
18831 }
18832 } else {
18833 $menus = array( $this->_menu->get_parent_slug() );
18834
18835 if ( $this->_menu->is_override_exact() ) {
18836 // Make sure the current page is matching the activation page.
18837 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18838 return;
18839 }
18840 }
18841
18842 foreach ( $menus as $parent_slug ) {
18843 $hook = $this->_menu->override_submenu_action(
18844 $parent_slug,
18845 $this->_menu->get_raw_slug(),
18846 array( &$this, '_connect_page_render' )
18847 );
18848
18849 if ( false !== $hook ) {
18850 // Found plugin's submenu item.
18851 break;
18852 }
18853 }
18854 }
18855
18856 if ( $this->is_activation_page() ) {
18857 // Clean admin page from distracting content.
18858 self::_clean_admin_content_section();
18859 }
18860
18861 if ( false !== $hook ) {
18862 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18863 $this->_install_with_current_user();
18864 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18865 $this->_install_with_new_user();
18866 }
18867 }
18868 }
18869
18870 /**
18871 * If a plugin was network activated and connected but don't have a network
18872 * level settings, then add an artificial menu item for the Account and other
18873 * Freemius settings.
18874 *
18875 * @author Vova Feldman (@svovaf)
18876 * @since 2.0.0
18877 */
18878 private function add_network_menu_when_missing() {
18879 $this->_logger->entrance();
18880
18881 if ( ! $this->_is_network_active ) {
18882 // Plugin wasn't activated on the network level.
18883 return;
18884 }
18885
18886 if ( ! fs_is_network_admin() ) {
18887 // The context is not the network admin.
18888 return;
18889 }
18890
18891 if ( $this->_menu->has_network_menu() ) {
18892 // Plugin already has a network level menu.
18893 return;
18894 }
18895
18896 if ( $this->is_network_activation_mode() ) {
18897 /**
18898 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
18899 * screen is being shown.
18900 *
18901 * @author Leo Fajardo (@leorw)
18902 */
18903 return;
18904 }
18905
18906 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
18907 if ( $this->is_network_delegated_connection() ) {
18908 // Super-admin delegated the connection to the site admins.
18909 return;
18910 }
18911 }
18912
18913 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
18914
18915 if ( $this->_menu->has_menu() ||
18916 ! $this->is_addon() ||
18917 $this->is_activation_mode()
18918 ) {
18919 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
18920 $this->get_plugin_name(),
18921 $this->get_plugin_name(),
18922 'manage_options',
18923 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
18924 );
18925 }
18926 } else {
18927 $this->_menu->add_subpage_and_update(
18928 $this->_menu->get_parent_slug(),
18929 $this->get_plugin_name(),
18930 $this->get_plugin_name(),
18931 'manage_options',
18932 $this->_menu->get_slug()
18933 );
18934 }
18935 }
18936
18937 /**
18938 * @author Leo Fajardo (@leorw)
18939 * @since 1.2.1
18940 *
18941 * return string
18942 */
18943 function get_top_level_menu_capability() {
18944 global $menu;
18945
18946 $top_level_menu_slug = $this->get_top_level_menu_slug();
18947
18948 foreach ( $menu as $menu_info ) {
18949 /**
18950 * The second element in the menu info array is the capability/role that has access to the menu and the
18951 * third element is the menu slug.
18952 */
18953 if ( $menu_info[2] === $top_level_menu_slug ) {
18954 return $menu_info[1];
18955 }
18956 }
18957
18958 return 'read';
18959 }
18960
18961 /**
18962 * @author Vova Feldman (@svovaf)
18963 * @since 1.0.0
18964 *
18965 * @return string
18966 */
18967 private function get_top_level_menu_slug() {
18968 return ( $this->is_addon() ?
18969 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
18970 $this->_menu->get_top_level_menu_slug() );
18971 }
18972
18973 /**
18974 * @author Vova Feldman (@svovaf)
18975 * @since 1.2.2.7
18976 *
18977 * @return string
18978 */
18979 function get_pricing_cta_label() {
18980 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
18981
18982 if ( $this->is_in_trial_promotion() &&
18983 ! $this->is_paying_or_trial()
18984 ) {
18985 // If running a trial promotion, modify the pricing to load the trial.
18986 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
18987 } else if ( $this->is_paying() ) {
18988 $label = $this->get_text_inline( 'Pricing', 'pricing' );
18989 }
18990
18991 return $label;
18992 }
18993
18994 /**
18995 * @author Vova Feldman (@svovaf)
18996 * @since 1.2.2.7
18997 *
18998 * @return bool
18999 */
19000 function is_pricing_page_visible() {
19001 return (
19002 // Has at least one paid plan.
19003 $this->has_paid_plan() &&
19004 // Didn't ask to hide the pricing page.
19005 $this->is_page_visible( 'pricing' ) &&
19006 // Don't have a valid active license or has more than one plan.
19007 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
19008 );
19009 }
19010
19011 /**
19012 * @author Leo Fajardo (@leorw)
19013 * @since 2.3.0
19014 *
19015 * @param bool $is_activation_mode
19016 *
19017 * @return bool
19018 */
19019 private function should_add_submenu_or_action_links( $is_activation_mode ) {
19020 if ( $this->is_addon() ) {
19021 // No submenu items or action links for add-ons.
19022 return false;
19023 }
19024
19025 if ( $this->show_opt_in_on_themes_page() ) {
19026 if ( ! fs_is_network_admin() ) {
19027 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
19028 return true;
19029 }
19030 } else if ( $is_activation_mode ) {
19031 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
19032 return false;
19033 }
19034
19035 if ( fs_is_network_admin() ) {
19036 /**
19037 * Add submenu items or action links to network level when plugin was network activated and the super
19038 * admin did NOT delegate the connection of all sites to site admins.
19039 */
19040 return (
19041 $this->_is_network_active &&
19042 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
19043 ! $this->is_network_delegated_connection() )
19044 );
19045 }
19046
19047 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
19048 }
19049
19050 /**
19051 * Add default Freemius menu items.
19052 *
19053 * @author Vova Feldman (@svovaf)
19054 * @since 1.0.0
19055 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
19056 */
19057 private function add_submenu_items() {
19058 $this->_logger->entrance();
19059
19060 $is_activation_mode = $this->is_activation_mode();
19061
19062 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
19063
19064 if ( $add_submenu_items ) {
19065 if ( $this->has_affiliate_program() ) {
19066 // Add affiliation page.
19067 $this->add_submenu_item(
19068 $this->get_text_inline( 'Affiliation', 'affiliation' ),
19069 array( &$this, '_affiliation_page_render' ),
19070 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
19071 'manage_options',
19072 'affiliation',
19073 'Freemius::_clean_admin_content_section',
19074 WP_FS__DEFAULT_PRIORITY,
19075 $this->is_submenu_item_visible( 'affiliation' )
19076 );
19077 }
19078 }
19079
19080 if ( $add_submenu_items ||
19081 ( $is_activation_mode &&
19082 $this->is_only_premium() &&
19083 $this->is_admin_page( 'account' ) &&
19084 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
19085 )
19086 ) {
19087 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
19088 $show_account = (
19089 $this->is_submenu_item_visible( 'account' ) &&
19090 /**
19091 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
19092 */
19093 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
19094 );
19095
19096 // Add user account page.
19097 $this->add_submenu_item(
19098 $this->get_text_inline( 'Account', 'account' ),
19099 array( &$this, '_account_page_render' ),
19100 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
19101 'manage_options',
19102 'account',
19103 array( &$this, '_account_page_load' ),
19104 WP_FS__DEFAULT_PRIORITY,
19105 ( $add_submenu_items && $show_account )
19106 );
19107 }
19108 }
19109
19110 if ( $add_submenu_items ) {
19111 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
19112 // Add contact page.
19113 $this->add_submenu_item(
19114 $this->get_text_inline( 'Contact Us', 'contact-us' ),
19115 array( &$this, '_contact_page_render' ),
19116 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
19117 'manage_options',
19118 'contact',
19119 'Freemius::_clean_admin_content_section',
19120 WP_FS__DEFAULT_PRIORITY,
19121 $this->is_submenu_item_visible( 'contact' )
19122 );
19123 }
19124
19125 if ( $this->has_addons() ) {
19126 $this->add_submenu_item(
19127 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
19128 array( &$this, '_addons_page_render' ),
19129 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
19130 'manage_options',
19131 'addons',
19132 array( &$this, '_addons_page_load' ),
19133 WP_FS__LOWEST_PRIORITY - 1,
19134 $this->is_submenu_item_visible( 'addons' )
19135 );
19136 }
19137 }
19138
19139 if ( $add_submenu_items ||
19140 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
19141 ) {
19142 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
19143 $show_pricing = (
19144 $this->is_submenu_item_visible( 'pricing' ) &&
19145 $this->is_pricing_page_visible()
19146 );
19147
19148 $pricing_cta_text = $this->get_pricing_cta_label();
19149 $pricing_class = 'upgrade-mode';
19150 if ( $show_pricing ) {
19151 if ( $this->is_in_trial_promotion() &&
19152 ! $this->is_paying_or_trial()
19153 ) {
19154 // If running a trial promotion, modify the pricing to load the trial.
19155 $pricing_class = 'trial-mode';
19156 } else if ( $this->is_paying() ) {
19157 $pricing_class = '';
19158 }
19159 }
19160
19161 // Add upgrade/pricing page.
19162 $this->add_submenu_item(
19163 $pricing_cta_text . '&nbsp;&nbsp;' . ( is_rtl() ? $this->get_text_x_inline( '&#x2190;', 'ASCII arrow left icon', 'symbol_arrow-left' ) : $this->get_text_x_inline( '&#x27a4;', 'ASCII arrow right icon', 'symbol_arrow-right' ) ),
19164 array( &$this, '_pricing_page_render' ),
19165 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
19166 'manage_options',
19167 'pricing',
19168 'Freemius::_clean_admin_content_section',
19169 WP_FS__LOWEST_PRIORITY,
19170 ( $add_submenu_items && $show_pricing ),
19171 $pricing_class
19172 );
19173 }
19174 }
19175
19176 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
19177 /**
19178 * Add the other menu items if there are any when not in activation mode or license activation is not
19179 * required (license activation is required for registered or anonymous users after activating the
19180 * premium version when the site is not in trial mode or there's no active valid license).
19181 *
19182 * @author Leo Fajardo (@leorw)
19183 * @since 2.2.1
19184 */
19185 if ( 0 < count( $this->_menu_items ) ) {
19186 if ( ! $this->_menu->is_top_level() ) {
19187 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
19188
19189 // Append submenu items right after the plugin's submenu item.
19190 $this->order_sub_submenu_items();
19191 } else {
19192 // Append submenu items.
19193 $this->embed_submenu_items();
19194 }
19195 }
19196 }
19197 }
19198
19199 /**
19200 * Moved the actual submenu item additions to a separated function,
19201 * in order to support sub-submenu items when the plugin's settings
19202 * only have a submenu and not top-level menu item.
19203 *
19204 * @author Vova Feldman (@svovaf)
19205 * @since 1.1.4
19206 */
19207 private function embed_submenu_items() {
19208 $item_template = $this->_menu->is_top_level() ?
19209 '<span class="fs-submenu-item %s %s %s">%s</span>' :
19210 '<span class="fs-submenu-item fs-sub %s %s %s">%s</span>';
19211
19212 $top_level_menu_capability = $this->get_top_level_menu_capability();
19213
19214 ksort( $this->_menu_items );
19215
19216 $is_first_submenu_item = true;
19217
19218 foreach ( $this->_menu_items as $priority => $items ) {
19219 foreach ( $items as $item ) {
19220 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
19221
19222 $menu_item = sprintf(
19223 $item_template,
19224 $this->get_unique_affix(),
19225 $item['menu_slug'],
19226 ! empty( $item['class'] ) ? $item['class'] : '',
19227 $item['menu_title']
19228 );
19229
19230 $top_level_menu_slug = $this->get_top_level_menu_slug();
19231 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
19232
19233 if ( ! isset( $item['url'] ) ) {
19234 $hook = FS_Admin_Menu_Manager::add_subpage(
19235 $item['show_submenu'] ?
19236 $top_level_menu_slug :
19237 '',
19238 $item['page_title'],
19239 $menu_item,
19240 $capability,
19241 $menu_slug,
19242 $item['render_function']
19243 );
19244
19245 if ( false !== $item['before_render_function'] ) {
19246 add_action( "load-$hook", $item['before_render_function'] );
19247 }
19248 } else {
19249 FS_Admin_Menu_Manager::add_subpage(
19250 $item['show_submenu'] ?
19251 $top_level_menu_slug :
19252 '',
19253 $item['page_title'],
19254 $menu_item,
19255 $capability,
19256 $menu_slug,
19257 array( $this, '' )
19258 );
19259 }
19260
19261 if ( $item['show_submenu'] && $is_first_submenu_item ) {
19262 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
19263 /**
19264 * If the top-level menu has been dynamically created, remove the first submenu item that
19265 * WordPress automatically creates when there's no submenu item whose slug matches the
19266 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
19267 *
19268 * Awesome Plugin
19269 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
19270 *
19271 * @author Leo Fajardo (@leorw)
19272 */
19273 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
19274 }
19275
19276 $is_first_submenu_item = false;
19277 }
19278 }
19279 }
19280 }
19281
19282 /**
19283 * Re-order the submenu items so all Freemius added new submenu items
19284 * are added right after the plugin's settings submenu item.
19285 *
19286 * @author Vova Feldman (@svovaf)
19287 * @since 1.1.4
19288 */
19289 private function order_sub_submenu_items() {
19290 global $submenu;
19291
19292 $menu_slug = $this->_menu->get_top_level_menu_slug();
19293
19294 /**
19295 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
19296 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
19297 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
19298 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
19299 *
19300 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
19301 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
19302 * capability needed to access the parent menu as the capability for the submenus that we will add.
19303 */
19304 if ( empty( $submenu[ $menu_slug ] ) ) {
19305 return;
19306 }
19307
19308 $top_level_menu = &$submenu[ $menu_slug ];
19309
19310 $all_submenu_items_after = array();
19311
19312 $found_submenu_item = false;
19313
19314 foreach ( $top_level_menu as $submenu_id => $meta ) {
19315 if ( $found_submenu_item ) {
19316 // Remove all submenu items after the plugin's submenu item.
19317 $all_submenu_items_after[] = $meta;
19318 unset( $top_level_menu[ $submenu_id ] );
19319 }
19320
19321 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
19322 // Found the submenu item, put all below.
19323 $found_submenu_item = true;
19324 continue;
19325 }
19326 }
19327
19328 // Embed all plugin's new submenu items.
19329 $this->embed_submenu_items();
19330
19331 // Start with specially high number to make sure it's appended.
19332 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
19333 foreach ( $all_submenu_items_after as $meta ) {
19334 $top_level_menu[ $i ] = $meta;
19335 $i ++;
19336 }
19337
19338 // Sort submenu items.
19339 ksort( $top_level_menu );
19340 }
19341
19342 /**
19343 * Helper method to return the module's support forum URL.
19344 *
19345 * @author Vova Feldman (@svovaf)
19346 * @since 1.2.2.7
19347 *
19348 * @return string
19349 */
19350 function get_support_forum_url() {
19351 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
19352 }
19353
19354 /**
19355 * Displays the Support Forum link when enabled.
19356 *
19357 * Can be filtered like so:
19358 *
19359 * function _fs_show_support_menu( $is_visible, $menu_id ) {
19360 * if ( 'support' === $menu_id ) {
19361 * return _fs->is_registered();
19362 * }
19363 * return $is_visible;
19364 * }
19365 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
19366 *
19367 */
19368 function _add_default_submenu_items() {
19369 if ( ! $this->is_on() ) {
19370 return;
19371 }
19372
19373 if ( ! $this->is_activation_mode() &&
19374 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
19375 ( ! $this->_is_network_active && is_admin() ) )
19376 ) {
19377 $this->add_submenu_link_item(
19378 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
19379 $this->get_support_forum_url(),
19380 'wp-support-forum',
19381 null,
19382 50,
19383 $this->is_submenu_item_visible( 'support' )
19384 );
19385 }
19386 }
19387
19388 /**
19389 * @author Vova Feldman (@svovaf)
19390 * @since 1.0.1
19391 *
19392 * @param string $menu_title
19393 * @param callable $render_function
19394 * @param bool|string $page_title
19395 * @param string $capability
19396 * @param bool|string $menu_slug
19397 * @param bool|callable $before_render_function
19398 * @param int $priority
19399 * @param bool $show_submenu
19400 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
19401 */
19402 function add_submenu_item(
19403 $menu_title,
19404 $render_function,
19405 $page_title = false,
19406 $capability = 'manage_options',
19407 $menu_slug = false,
19408 $before_render_function = false,
19409 $priority = WP_FS__DEFAULT_PRIORITY,
19410 $show_submenu = true,
19411 $class = ''
19412 ) {
19413 $this->_logger->entrance( 'Title = ' . $menu_title );
19414
19415 if ( $this->is_addon() ) {
19416 $parent_fs = $this->get_parent_instance();
19417
19418 if ( is_object( $parent_fs ) ) {
19419 $parent_fs->add_submenu_item(
19420 $menu_title,
19421 $render_function,
19422 $page_title,
19423 $capability,
19424 $menu_slug,
19425 $before_render_function,
19426 $priority,
19427 $show_submenu,
19428 $class
19429 );
19430
19431 return;
19432 }
19433 }
19434
19435 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19436 $this->_menu_items[ $priority ] = array();
19437 }
19438
19439 $this->_menu_items[ $priority ][] = array(
19440 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
19441 'menu_title' => $menu_title,
19442 'capability' => $capability,
19443 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19444 'render_function' => $render_function,
19445 'before_render_function' => $before_render_function,
19446 'show_submenu' => $show_submenu,
19447 'class' => $class,
19448 );
19449 }
19450
19451 /**
19452 * @author Vova Feldman (@svovaf)
19453 * @since 1.0.1
19454 *
19455 * @param string $menu_title
19456 * @param string $url
19457 * @param bool $menu_slug
19458 * @param string $capability
19459 * @param int $priority
19460 * @param bool $show_submenu
19461 */
19462 function add_submenu_link_item(
19463 $menu_title,
19464 $url,
19465 $menu_slug = false,
19466 $capability = 'read',
19467 $priority = WP_FS__DEFAULT_PRIORITY,
19468 $show_submenu = true
19469 ) {
19470 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
19471
19472 if ( $this->is_addon() ) {
19473 $parent_fs = $this->get_parent_instance();
19474
19475 if ( is_object( $parent_fs ) ) {
19476 $parent_fs->add_submenu_link_item(
19477 $menu_title,
19478 $url,
19479 $menu_slug,
19480 $capability,
19481 $priority,
19482 $show_submenu
19483 );
19484
19485 return;
19486 }
19487 }
19488
19489 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19490 $this->_menu_items[ $priority ] = array();
19491 }
19492
19493 $this->_menu_items[ $priority ][] = array(
19494 'menu_title' => $menu_title,
19495 'capability' => $capability,
19496 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19497 'url' => $url,
19498 'page_title' => $menu_title,
19499 'render_function' => 'fs_dummy',
19500 'before_render_function' => '',
19501 'show_submenu' => $show_submenu,
19502 );
19503 }
19504
19505 #endregion ------------------------------------------------------------------
19506
19507 #--------------------------------------------------------------------------------
19508 #region Admin Notices
19509 #--------------------------------------------------------------------------------
19510
19511 /**
19512 * @author Vova Feldman (@svovaf)
19513 * @since 2.3.1
19514 *
19515 * @param string|string[] $ids
19516 * @param int|null $network_level_or_blog_id
19517 *
19518 * @uses FS_Admin_Notices::remove_sticky()
19519 */
19520 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
19521 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
19522 }
19523
19524 #endregion
19525
19526 #--------------------------------------------------------------------------------
19527 #region Actions / Hooks / Filters
19528 #--------------------------------------------------------------------------------
19529
19530 /**
19531 * @author Vova Feldman (@svovaf)
19532 * @since 1.1.7
19533 *
19534 * @param string $tag
19535 *
19536 * @return string
19537 */
19538 public function get_action_tag( $tag ) {
19539 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
19540 }
19541
19542 /**
19543 * @author Vova Feldman (@svovaf)
19544 * @since 1.2.1.6
19545 *
19546 * @param string $tag
19547 * @param string $slug
19548 * @param bool $is_plugin
19549 *
19550 * @return string
19551 */
19552 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
19553 $action = "fs_{$tag}";
19554
19555 if ( ! empty( $slug ) ) {
19556 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
19557 }
19558
19559 return $action;
19560 }
19561
19562 /**
19563 * Returns a string that can be used to generate a unique action name,
19564 * option name, HTML element ID, or HTML element class.
19565 *
19566 * @author Leo Fajardo (@leorw)
19567 * @since 1.2.2
19568 *
19569 * @return string
19570 */
19571 public function get_unique_affix() {
19572 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
19573 }
19574
19575 /**
19576 * Returns a string that can be used to generate a unique action name,
19577 * option name, HTML element ID, or HTML element class.
19578 *
19579 * @author Vova Feldman (@svovaf)
19580 * @since 1.2.2.5
19581 *
19582 * @param string $slug
19583 * @param bool $is_plugin
19584 *
19585 * @return string
19586 */
19587 static function get_module_unique_affix( $slug, $is_plugin = true ) {
19588 $affix = $slug;
19589
19590 if ( ! $is_plugin ) {
19591 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
19592 }
19593
19594 return $affix;
19595 }
19596
19597 /**
19598 * @author Vova Feldman (@svovaf)
19599 * @since 1.2.1
19600 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19601 * based on the slug for backward compatibility.
19602 *
19603 * @param string $tag
19604 *
19605 * @return string
19606 */
19607 function get_ajax_action( $tag ) {
19608 return self::get_ajax_action_static( $tag, $this->_module_id );
19609 }
19610
19611 /**
19612 * @author Vova Feldman (@svovaf)
19613 * @since 1.2.1.7
19614 *
19615 * @param string $tag
19616 *
19617 * @return string
19618 */
19619 function get_ajax_security( $tag ) {
19620 return wp_create_nonce( $this->get_ajax_action( $tag ) );
19621 }
19622
19623 /**
19624 * @author Vova Feldman (@svovaf)
19625 * @since 1.2.1.7
19626 *
19627 * @param string $tag
19628 */
19629 function check_ajax_referer( $tag ) {
19630 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
19631 }
19632
19633 /**
19634 * @author Vova Feldman (@svovaf)
19635 * @since 1.2.1.6
19636 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19637 * based on the slug for backward compatibility.
19638 *
19639 * @param string $tag
19640 * @param number|null $module_id
19641 *
19642 * @return string
19643 */
19644 static function get_ajax_action_static( $tag, $module_id = null ) {
19645 $action = "fs_{$tag}";
19646
19647 if ( ! empty( $module_id ) ) {
19648 $action .= "_{$module_id}";
19649 }
19650
19651 return $action;
19652 }
19653
19654 /**
19655 * Do action, specific for the current context plugin.
19656 *
19657 * @author Vova Feldman (@svovaf)
19658 * @since 1.0.1
19659 *
19660 * @param string $tag The name of the action to be executed.
19661 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
19662 * functions hooked to the action. Default empty.
19663 *
19664 * @uses do_action()
19665 */
19666 function do_action( $tag, $arg = '' ) {
19667 $args = func_get_args();
19668
19669 $this->_logger->entrance( $tag );
19670
19671 call_user_func_array( 'do_action', array_merge(
19672 array( $this->get_action_tag( $tag ) ),
19673 array_slice( $args, 1 ) )
19674 );
19675 }
19676
19677 /**
19678 * Add action, specific for the current context plugin.
19679 *
19680 * @author Vova Feldman (@svovaf)
19681 * @since 1.0.1
19682 *
19683 * @param string $tag
19684 * @param callable $function_to_add
19685 * @param int $priority
19686 * @param int $accepted_args
19687 *
19688 * @uses add_action()
19689 */
19690 function add_action(
19691 $tag,
19692 $function_to_add,
19693 $priority = WP_FS__DEFAULT_PRIORITY,
19694 $accepted_args = 1
19695 ) {
19696 $this->_logger->entrance( $tag );
19697
19698 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19699 }
19700
19701 /**
19702 * Add AJAX action, specific for the current context plugin.
19703 *
19704 * @author Vova Feldman (@svovaf)
19705 * @since 1.2.1
19706 *
19707 * @param string $tag
19708 * @param callable $function_to_add
19709 * @param int $priority
19710 *
19711 * @uses add_action()
19712 *
19713 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19714 */
19715 function add_ajax_action(
19716 $tag,
19717 $function_to_add,
19718 $priority = WP_FS__DEFAULT_PRIORITY
19719 ) {
19720 $this->_logger->entrance( $tag );
19721
19722 return self::add_ajax_action_static(
19723 $tag,
19724 $function_to_add,
19725 $priority,
19726 $this->_module_id
19727 );
19728 }
19729
19730 /**
19731 * Add AJAX action.
19732 *
19733 * @author Vova Feldman (@svovaf)
19734 * @since 1.2.1.6
19735 *
19736 * @param string $tag
19737 * @param callable $function_to_add
19738 * @param int $priority
19739 * @param number|null $module_id
19740 *
19741 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19742 * @uses add_action()
19743 *
19744 */
19745 static function add_ajax_action_static(
19746 $tag,
19747 $function_to_add,
19748 $priority = WP_FS__DEFAULT_PRIORITY,
19749 $module_id = null
19750 ) {
19751 self::$_static_logger->entrance( $tag );
19752
19753 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
19754 return false;
19755 }
19756
19757 add_action(
19758 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
19759 $function_to_add,
19760 $priority,
19761 0
19762 );
19763
19764 self::$_static_logger->info( "$tag AJAX callback action added." );
19765
19766 return true;
19767 }
19768
19769 /**
19770 * Send a JSON response back to an Ajax request.
19771 *
19772 * @author Vova Feldman (@svovaf)
19773 * @since 1.2.1.5
19774 *
19775 * @param mixed $response
19776 */
19777 static function shoot_ajax_response( $response ) {
19778 wp_send_json( $response );
19779 }
19780
19781 /**
19782 * Send a JSON response back to an Ajax request, indicating success.
19783 *
19784 * @author Vova Feldman (@svovaf)
19785 * @since 1.2.1.5
19786 *
19787 * @param mixed $data Data to encode as JSON, then print and exit.
19788 */
19789 static function shoot_ajax_success( $data = null ) {
19790 wp_send_json_success( $data );
19791 }
19792
19793 /**
19794 * Send a JSON response back to an Ajax request, indicating failure.
19795 *
19796 * @author Vova Feldman (@svovaf)
19797 * @since 1.2.1.5
19798 *
19799 * @param mixed $error Optional error message.
19800 */
19801 static function shoot_ajax_failure( $error = '' ) {
19802 $result = array( 'success' => false );
19803 if ( ! empty( $error ) ) {
19804 $result['error'] = $error;
19805 }
19806
19807 wp_send_json( $result );
19808 }
19809
19810 /**
19811 * Returns an AJAX URL with a special extra param to indicate whether the request was triggered from the network admin or blog admin.
19812 *
19813 * @author Vova Feldman (@svovaf)
19814 * @since 2.5.1
19815 *
19816 * @param string $wrap_with By default, returns the AJAX URL wrapped with single quotes.
19817 *
19818 * @return string
19819 */
19820 static function ajax_url( $wrap_with = "'") {
19821 if ( fs_is_network_admin() ) {
19822 $param_name = '_fs_network_admin';
19823 } else {
19824 $param_name = '_fs_blog_admin';
19825 }
19826
19827 $url = admin_url( 'admin-ajax.php', 'relative' );
19828 $url .= ( false === strpos( $url, '?' ) ) ? '?' : '&';
19829 $url .= "{$param_name}=true";
19830
19831 return "{$wrap_with}{$url}{$wrap_with}";
19832 }
19833
19834 /**
19835 * Apply filter, specific for the current context plugin.
19836 *
19837 * @author Vova Feldman (@svovaf)
19838 * @since 1.0.9
19839 *
19840 * @param string $tag The name of the filter hook.
19841 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
19842 *
19843 * @return mixed The filtered value after all hooked functions are applied to it.
19844 *
19845 * @uses apply_filters()
19846 */
19847 function apply_filters( $tag, $value ) {
19848 $args = func_get_args();
19849
19850 $this->_logger->entrance( $tag );
19851
19852 array_unshift( $args, $this->get_unique_affix() );
19853
19854 return call_user_func_array( 'fs_apply_filter', $args );
19855 }
19856
19857 /**
19858 * Add filter, specific for the current context plugin.
19859 *
19860 * @author Vova Feldman (@svovaf)
19861 * @since 1.0.9
19862 *
19863 * @param string $tag
19864 * @param callable $function_to_add
19865 * @param int $priority
19866 * @param int $accepted_args
19867 *
19868 * @uses add_filter()
19869 */
19870 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
19871 $this->_logger->entrance( $tag );
19872
19873 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19874 }
19875
19876 /**
19877 * Check if has filter.
19878 *
19879 * @author Vova Feldman (@svovaf)
19880 * @since 1.1.4
19881 *
19882 * @param string $tag
19883 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
19884 *
19885 * @return false|int
19886 *
19887 * @uses has_filter()
19888 */
19889 function has_filter( $tag, $function_to_check = false ) {
19890 $this->_logger->entrance( $tag );
19891
19892 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
19893 }
19894
19895 #endregion
19896
19897 /**
19898 * Override default i18n text phrases.
19899 *
19900 * @author Vova Feldman (@svovaf)
19901 * @since 1.1.6
19902 *
19903 * @param string[] string $key_value
19904 *
19905 * @uses fs_override_i18n()
19906 */
19907 function override_i18n( $key_value ) {
19908 fs_override_i18n( $key_value, $this->_slug );
19909 }
19910
19911 /* Account Page
19912 ------------------------------------------------------------------------------------------------------------------*/
19913 /**
19914 * Update site information.
19915 *
19916 * @author Vova Feldman (@svovaf)
19917 * @since 1.0.1
19918 *
19919 * @param bool $store Flush to Database if true.
19920 * @param null|int $network_level_or_blog_id Since 2.0.0
19921 * @param \FS_Site $site Since 2.0.0
19922 */
19923 private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null, $is_backup = false ) {
19924 $this->_logger->entrance();
19925
19926 if ( is_null( $site ) ) {
19927 $site = $this->_site;
19928 }
19929
19930 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
19931 $this->_logger->error( "Empty install ID, can't store site." );
19932
19933 return;
19934 }
19935
19936 $site_clone = clone $site;
19937
19938 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id, $is_backup );
19939
19940 if (
19941 ! $is_backup &&
19942 is_object( $this->_user ) && $this->_user->id != $site->user_id
19943 ) {
19944 $this->sync_user_by_current_install( $site->user_id );
19945
19946 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
19947
19948 if ( empty( $prev_stored_user_id ) &&
19949 is_object($this->_user) && $this->_user->id != $site->user_id
19950 ) {
19951 /**
19952 * Store the current user ID as the previous user ID so that the previous user can be used
19953 * as the install's owner while the new owner's details are not yet available.
19954 *
19955 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
19956 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
19957 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
19958 * into `replica`.
19959 *
19960 * @author Leo Fajardo (@leorw)
19961 */
19962 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
19963 }
19964 }
19965
19966 $sites[ $this->_slug ] = $site_clone;
19967
19968 $this->set_account_option(
19969 ( $is_backup ? 'prev_' : '' ) . 'sites',
19970 $sites,
19971 $store,
19972 $network_level_or_blog_id
19973 );
19974 }
19975
19976 /**
19977 * Stores the context site in the sites backup storage. This logic is used before deleting the site info so that it can be restored later on if necessary (e.g., if the automatic clone resolution attempt fails).
19978 *
19979 * @author Leo Fajardo (@leorw)
19980 * @since 2.5.0
19981 */
19982 private function back_up_site() {
19983 $this->_logger->entrance();
19984
19985 $site_clone = clone $this->_site;
19986
19987 $this->_store_site( true, null, $site_clone, true );
19988 }
19989
19990 /**
19991 * Update plugin's plans information.
19992 *
19993 * @author Vova Feldman (@svovaf)
19994 * @since 1.0.2
19995 *
19996 * @param bool $store Flush to Database if true.
19997 */
19998 private function _store_plans( $store = true ) {
19999 $this->_logger->entrance();
20000
20001 $plans = self::get_all_plans( $this->_module_type );
20002
20003 // Copy plans.
20004 $encrypted_plans = array();
20005 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
20006 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
20007 }
20008
20009 $plans[ $this->_slug ] = $encrypted_plans;
20010
20011 $this->set_account_option( 'plans', $plans, $store );
20012 }
20013
20014 /**
20015 * Update user's plugin licenses.
20016 *
20017 * @author Vova Feldman (@svovaf)
20018 * @since 1.0.5
20019 *
20020 * @param bool $store
20021 * @param number|bool $module_id
20022 * @param FS_Plugin_License[] $licenses
20023 */
20024 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
20025 $this->_logger->entrance();
20026
20027 $all_licenses = self::get_all_licenses();
20028
20029 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
20030 $module_id = $this->_module_id;
20031
20032 $user_licenses = is_array( $this->_licenses ) ?
20033 $this->_licenses :
20034 array();
20035
20036 if ( empty( $user_licenses ) ) {
20037 // If the context user doesn't have any license, don't update the licenses collection.
20038 return;
20039 }
20040
20041 $new_user_licenses_map = array();
20042 foreach ( $user_licenses as $user_license ) {
20043 $new_user_licenses_map[ $user_license->id ] = $user_license;
20044 }
20045
20046 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
20047
20048 // Update user licenses.
20049 $licenses_to_update_count = count( $new_user_licenses_map );
20050 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
20051 if ( 0 === $licenses_to_update_count ) {
20052 break;
20053 }
20054
20055 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
20056 // Update license.
20057 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
20058 unset( $new_user_licenses_map[ $license->id ] );
20059
20060 $licenses_to_update_count --;
20061 }
20062 }
20063
20064 if ( ! empty( $new_user_licenses_map ) ) {
20065 // Add new licenses.
20066 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
20067 }
20068
20069 $licenses = $all_licenses[ $module_id ];
20070 }
20071
20072 if ( ! isset( $all_licenses[ $module_id ] ) ) {
20073 $all_licenses[ $module_id ] = array();
20074 }
20075
20076 $all_licenses[ $module_id ] = $licenses;
20077
20078 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
20079 }
20080
20081 /**
20082 * Update user information.
20083 *
20084 * @author Vova Feldman (@svovaf)
20085 * @since 1.0.1
20086 *
20087 * @param bool $store Flush to Database if true.
20088 */
20089 private function _store_user( $store = true ) {
20090 $this->_logger->entrance();
20091
20092 if ( empty( $this->_user->id ) ) {
20093 $this->_logger->error( "Empty user ID, can't store user." );
20094
20095 return;
20096 }
20097
20098 $users = self::get_all_users();
20099 $users[ $this->_user->id ] = $this->_user;
20100 self::$_accounts->set_option( 'users', $users, $store );
20101 }
20102
20103 /**
20104 * Update new updates information.
20105 *
20106 * @author Vova Feldman (@svovaf)
20107 * @since 1.0.4
20108 *
20109 * @param FS_Plugin_Tag|null $update
20110 * @param bool $store Flush to Database if true.
20111 * @param bool|number $plugin_id
20112 */
20113 private function _store_update( $update, $store = true, $plugin_id = false ) {
20114 $this->_logger->entrance();
20115
20116 if ( $update instanceof FS_Plugin_Tag ) {
20117 $update->updated = time();
20118 }
20119
20120 if ( ! is_numeric( $plugin_id ) ) {
20121 $plugin_id = $this->_plugin->id;
20122 }
20123
20124 $updates = self::get_all_updates();
20125 $updates[ $plugin_id ] = $update;
20126 self::$_accounts->set_option( 'updates', $updates, $store );
20127 }
20128
20129 /**
20130 * Update new updates information.
20131 *
20132 * @author Vova Feldman (@svovaf)
20133 * @since 1.0.6
20134 *
20135 * @param FS_Plugin[] $plugin_addons
20136 * @param bool $store Flush to Database if true.
20137 */
20138 private function _store_addons( $plugin_addons, $store = true ) {
20139 $this->_logger->entrance();
20140
20141 $addons = self::get_all_addons();
20142 $addons[ $this->_plugin->id ] = $plugin_addons;
20143 self::$_accounts->set_option( 'addons', $addons, $store );
20144 }
20145
20146 /**
20147 * Delete plugin's associated add-ons.
20148 *
20149 * @author Vova Feldman (@svovaf)
20150 * @since 1.0.8
20151 *
20152 * @param bool $store
20153 *
20154 * @return bool
20155 */
20156 private function _delete_account_addons( $store = true ) {
20157 $all_addons = self::get_all_account_addons();
20158
20159 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
20160 return false;
20161 }
20162
20163 unset( $all_addons[ $this->_plugin->id ] );
20164
20165 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20166
20167 return true;
20168 }
20169
20170 /**
20171 * Update account add-ons list.
20172 *
20173 * @author Vova Feldman (@svovaf)
20174 * @since 1.0.6
20175 *
20176 * @param FS_Plugin[] $addons
20177 * @param bool $store Flush to Database if true.
20178 */
20179 private function _store_account_addons( $addons, $store = true ) {
20180 $this->_logger->entrance();
20181
20182 $all_addons = self::get_all_account_addons();
20183 $all_addons[ $this->_plugin->id ] = $addons;
20184 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20185 }
20186
20187 /**
20188 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
20189 * the valid user licenses will be fetched again and the account add-ons may be updated.
20190 *
20191 * @author Leo Fajardo (@leorw)
20192 * @since 2.2.4
20193 */
20194 private function purge_valid_user_licenses_cache() {
20195 if ( ! $this->is_registered() ) {
20196 return;
20197 }
20198
20199 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
20200 }
20201
20202 /**
20203 * @author Leo Fajardo (@leorw)
20204 * @since 2.3.0
20205 *
20206 * @param array $all_licenses
20207 * @param number|null $site_license_id
20208 * @param bool $include_parent_licenses
20209 *
20210 * @return array
20211 */
20212 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
20213 $foreign_licenses = array(
20214 'ids' => array(),
20215 'license_keys' => array()
20216 );
20217
20218 $parent_license_ids_map = array();
20219
20220 foreach ( $all_licenses as $license ) {
20221 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
20222 continue;
20223 }
20224
20225 $foreign_licenses['ids'][] = $license->id;
20226 $foreign_licenses['license_keys'][] = $license->secret_key;
20227
20228 if (
20229 $include_parent_licenses &&
20230 is_object( $this->_license ) &&
20231 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
20232 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
20233 ) {
20234 /**
20235 * Include the parent license's info only if it has not been included before since child licenses
20236 * can have the same parent license.
20237 */
20238 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
20239 $foreign_licenses['license_keys'][] = $license->secret_key;
20240
20241 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
20242 }
20243 }
20244
20245 if ( empty( $foreign_licenses['ids'] ) ) {
20246 $foreign_licenses = array();
20247 }
20248
20249 return $foreign_licenses;
20250 }
20251
20252 /**
20253 * @author Leo Fajardo (@leorw)
20254 * @since 2.3.0
20255 *
20256 * @return string
20257 */
20258 private function get_valid_user_licenses_endpoint() {
20259 $user_licenses_endpoint = '/licenses.json?type=active' .
20260 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
20261
20262 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
20263
20264 if ( ! empty ( $foreign_licenses ) ) {
20265 $foreign_licenses = array(
20266 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20267 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20268 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20269 );
20270
20271 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20272 }
20273
20274 return $user_licenses_endpoint;
20275 }
20276
20277 /**
20278 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
20279 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
20280 * method filters out non–add-on product IDs and stores the add-on IDs.
20281 *
20282 * @author Leo Fajardo (@leorw)
20283 * @since 2.2.4
20284 *
20285 * @return stdClass[] array
20286 */
20287 private function fetch_valid_user_licenses() {
20288 $this->_logger->entrance();
20289
20290 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
20291
20292 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
20293 ! is_array( $result->licenses )
20294 ) {
20295 return array();
20296 }
20297
20298 return $result->licenses;
20299 }
20300
20301 /**
20302 * @author Leo Fajardo (@leorw)
20303 * @since 2.2.4
20304 *
20305 * @return number[] Account add-on IDs.
20306 */
20307 function get_updated_account_addons() {
20308 $addons = $this->get_addons();
20309 if ( empty( $addons ) ) {
20310 return array();
20311 }
20312
20313 $account_addons = $this->get_account_addons();
20314 if ( ! is_array( $account_addons ) ) {
20315 $account_addons = array();
20316 }
20317
20318 $user_licenses = $this->is_registered() ?
20319 $this->fetch_valid_user_licenses() :
20320 array();
20321
20322 if ( empty( $user_licenses ) ) {
20323 return $account_addons;
20324 }
20325
20326 $addon_ids = array();
20327 foreach ( $addons as $addon ) {
20328 $addon_ids[] = $addon->id;
20329 }
20330
20331 $license_product_ids = array();
20332
20333 foreach ( $user_licenses as $license ) {
20334 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
20335 $license_product_ids = array_merge( $license_product_ids, $license->products );
20336 } else {
20337 $license_product_ids[] = $license->plugin_id;
20338 }
20339 }
20340
20341 // Filter out non–add-on IDs.
20342 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
20343 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
20344 $this->_store_account_addons( array_unique( $new_account_addons ) );
20345 }
20346
20347 return $new_account_addons;
20348 }
20349
20350 /**
20351 * Store account params in the Database.
20352 *
20353 * @author Vova Feldman (@svovaf)
20354 * @since 1.0.1
20355 *
20356 * @param null|int $blog_id Since 2.0.0
20357 */
20358 private function _store_account( $blog_id = null ) {
20359 $this->_logger->entrance();
20360
20361 $this->_store_site( false, $blog_id );
20362 $this->_store_user( false );
20363 $this->_store_plans( false );
20364 $this->_store_licenses( false );
20365
20366 self::$_accounts->store( $blog_id );
20367 }
20368
20369 /**
20370 * Sync user's information.
20371 *
20372 * @author Vova Feldman (@svovaf)
20373 * @since 1.0.3
20374 * @uses FS_Api
20375 */
20376 private function _handle_account_user_sync() {
20377 $this->_logger->entrance();
20378
20379 $api = $this->get_api_user_scope();
20380
20381 // Get user's information.
20382 $user = $api->get( '/', true );
20383
20384 if ( isset( $user->id ) ) {
20385 $this->_user->first = $user->first;
20386 $this->_user->last = $user->last;
20387 $this->_user->email = $user->email;
20388
20389 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
20390
20391 if ( $user->is_verified &&
20392 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
20393 ) {
20394 $this->_user->is_verified = true;
20395
20396 $this->do_action( 'account_email_verified', $user->email );
20397
20398 $this->_admin_notices->add(
20399 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
20400 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
20401 'success',
20402 // Make admin sticky if account menu item is invisible,
20403 // since the page will be auto redirected to the plugin's
20404 // main settings page, and the non-sticky message
20405 // will disappear.
20406 ! $is_menu_item_account_visible,
20407 'email_verified'
20408 );
20409 }
20410
20411 // Flush user details to DB.
20412 $this->_store_user();
20413
20414 $this->do_action( 'after_account_user_sync', $user );
20415
20416 /**
20417 * If account menu item is hidden, redirect to plugin's main settings page.
20418 *
20419 * @author Vova Feldman (@svovaf)
20420 * @since 1.1.6
20421 *
20422 * @link https://github.com/Freemius/wordpress-sdk/issues/6
20423 */
20424 if ( ! $is_menu_item_account_visible ) {
20425 fs_redirect( $this->_get_admin_page_url() );
20426 }
20427 }
20428 }
20429
20430 /**
20431 * @author Vova Feldman (@svovaf)
20432 * @since 1.0.9
20433 * @uses FS_Api
20434 *
20435 * @param number|bool $license_id
20436 *
20437 * @return FS_Subscription|object|bool
20438 */
20439 private function _fetch_site_license_subscription( $license_id = false ) {
20440 $this->_logger->entrance();
20441 $api = $this->get_api_site_scope();
20442
20443 if ( ! is_numeric( $license_id ) ) {
20444 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
20445 $this->_license->parent_license_id :
20446 $this->_license->id;
20447 }
20448
20449 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
20450
20451 return ! isset( $result->error ) ?
20452 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
20453 new FS_Subscription( $result->subscriptions[0] ) :
20454 false
20455 ) :
20456 $result;
20457 }
20458
20459 /**
20460 * @author Vova Feldman (@svovaf)
20461 * @since 1.0.4
20462 * @uses FS_Api
20463 *
20464 * @param number|bool $plan_id
20465 *
20466 * @return FS_Plugin_Plan|object
20467 */
20468 private function _fetch_site_plan( $plan_id = false ) {
20469 $this->_logger->entrance();
20470 $api = $this->get_api_site_scope();
20471
20472 if ( ! is_numeric( $plan_id ) ) {
20473 $plan_id = $this->_site->plan_id;
20474 }
20475
20476 $plan = $api->get( "/plans/{$plan_id}.json", true );
20477
20478 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
20479 }
20480
20481 /**
20482 * @author Vova Feldman (@svovaf)
20483 * @since 1.0.5
20484 * @uses FS_Api
20485 *
20486 * @return FS_Plugin_Plan[]|object
20487 */
20488 private function _fetch_plugin_plans() {
20489 $this->_logger->entrance();
20490 $api = $this->get_current_or_network_user_api_scope();
20491
20492 /**
20493 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
20494 */
20495 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
20496
20497 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
20498 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
20499 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
20500 }
20501
20502 $result = $result->plans;
20503 }
20504
20505 return $result;
20506 }
20507
20508 /**
20509 * @author Vova Feldman (@svovaf)
20510 * @since 2.0.0
20511 *
20512 * @param number $plan_id
20513 *
20514 * @return \FS_Plugin_Plan|object
20515 */
20516 private function fetch_plan_by_id( $plan_id ) {
20517 $this->_logger->entrance();
20518 $api = $this->get_current_or_network_user_api_scope();
20519
20520 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
20521
20522 return $this->is_api_result_entity( $result ) ?
20523 new FS_Plugin_Plan( $result ) :
20524 $result;
20525 }
20526
20527 /**
20528 * @author Vova Feldman (@svovaf)
20529 * @since 1.0.5
20530 * @uses FS_Api
20531 *
20532 * @param number|bool $plugin_id
20533 * @param number|bool $site_license_id
20534 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
20535 * @param number|null $blog_id
20536 *
20537 * @return FS_Plugin_License[]|object
20538 */
20539 private function _fetch_licenses(
20540 $plugin_id = false,
20541 $site_license_id = false,
20542 $foreign_licenses = array(),
20543 $blog_id = null
20544 ) {
20545 $this->_logger->entrance();
20546
20547 $api = $this->get_api_user_scope();
20548
20549 if ( ! is_numeric( $plugin_id ) ) {
20550 $plugin_id = $this->_plugin->id;
20551 }
20552
20553 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
20554 if ( ! empty ( $foreign_licenses ) ) {
20555 $foreign_licenses = array(
20556 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20557 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20558 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20559 );
20560
20561 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20562 }
20563
20564 $result = $api->get( $user_licenses_endpoint, true );
20565
20566 $is_site_license_synced = false;
20567
20568 $api_errors = array();
20569
20570 if ( $this->is_api_result_object( $result, 'licenses' ) &&
20571 is_array( $result->licenses )
20572 ) {
20573 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
20574 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
20575
20576 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
20577 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
20578 }
20579 }
20580
20581 $result = $result->licenses;
20582 } else {
20583 $api_errors[] = $result;
20584 $result = array();
20585 }
20586
20587 if ( ! $is_site_license_synced ) {
20588 if ( ! is_null( $blog_id ) ) {
20589 /**
20590 * If blog ID is not null, the request is for syncing of the license of a single site via the
20591 * network-level "Account" page.
20592 *
20593 * @author Leo Fajardo (@leorw)
20594 */
20595 $this->switch_to_blog( $blog_id );
20596 }
20597
20598 $api = $this->get_api_site_scope();
20599
20600 if ( is_numeric( $site_license_id ) ) {
20601 // Try to retrieve a foreign license that is linked to the install.
20602 $api_result = $api->call( '/licenses.json?is_enriched=true' );
20603
20604 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
20605 is_array( $api_result->licenses )
20606 ) {
20607 $licenses = $api_result->licenses;
20608
20609 if ( ! empty( $licenses ) ) {
20610 $result[] = new FS_Plugin_License( $licenses[0] );
20611 }
20612 } else {
20613 $api_errors[] = $api_result;
20614 }
20615 } else if (
20616 is_object( $this->_license ) &&
20617 /**
20618 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
20619 * the FS instance that does the syncing is the parent FS instance.
20620 *
20621 * @author Leo Fajardo (@leorw)
20622 * @since 2.3.0
20623 */
20624 $this->_license->plugin_id == $plugin_id
20625 ) {
20626 $is_license_in_result = false;
20627 if ( ! empty( $result ) ) {
20628 foreach ( $result as $license ) {
20629 if ( $license->id == $this->_license->id ) {
20630 $is_license_in_result = true;
20631 break;
20632 }
20633 }
20634 }
20635
20636 if ( ! $is_license_in_result ) {
20637 // Fetch foreign license by ID and license key.
20638 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
20639 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
20640
20641 if ( $this->is_api_result_entity( $license ) ) {
20642 $result[] = new FS_Plugin_License( $license );
20643 } else {
20644 $api_errors[] = $license;
20645 }
20646 }
20647 }
20648
20649 if ( ! is_null( $blog_id ) ) {
20650 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20651 }
20652 }
20653
20654 if ( is_array( $result ) && 0 < count( $result ) ) {
20655 // If found at least one license, return license collection even if there are errors.
20656 return $result;
20657 }
20658
20659 if ( ! empty( $api_errors ) ) {
20660 // If found any errors and no licenses, return first error.
20661 return $api_errors[0];
20662 }
20663
20664 // Fallback to empty licenses list.
20665 return $result;
20666 }
20667
20668 /**
20669 * @author Vova Feldman (@svovaf)
20670 * @since 2.0.0
20671 *
20672 * @param number $license_id
20673 * @param string $license_key
20674 *
20675 * @return \FS_Plugin_License|object
20676 */
20677 private function fetch_license_by_key( $license_id, $license_key ) {
20678 $this->_logger->entrance();
20679
20680 $api = $this->get_current_or_network_user_api_scope();
20681
20682 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
20683
20684 return $this->is_api_result_entity( $result ) ?
20685 new FS_Plugin_License( $result ) :
20686 $result;
20687 }
20688
20689 /**
20690 * @author Vova Feldman (@svovaf)
20691 * @since 1.2.0
20692 * @uses FS_Api
20693 *
20694 * @param number|bool $plugin_id
20695 * @param bool $flush
20696 *
20697 * @return FS_Payment[]|object
20698 */
20699 function _fetch_payments( $plugin_id = false, $flush = false ) {
20700 $this->_logger->entrance();
20701
20702 $api = $this->get_api_user_scope();
20703
20704 if ( ! is_numeric( $plugin_id ) ) {
20705 $plugin_id = $this->_plugin->id;
20706 }
20707
20708 $include_bundles = (
20709 is_object( $this->_plugin ) &&
20710 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
20711 );
20712
20713 $result = $api->get(
20714 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
20715 $flush
20716 );
20717
20718 if ( ! isset( $result->error ) ) {
20719 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
20720 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
20721 }
20722 $result = $result->payments;
20723 }
20724
20725 return $result;
20726 }
20727
20728 /**
20729 * @author Vova Feldman (@svovaf)
20730 * @since 1.2.1.5
20731 * @uses FS_Api
20732 *
20733 * @param bool $flush
20734 *
20735 * @return \FS_Billing|mixed
20736 */
20737 function _fetch_billing( $flush = false ) {
20738 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
20739
20740 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
20741
20742 if ( $this->is_api_result_entity( $billing ) ) {
20743 $billing = new FS_Billing( $billing );
20744 }
20745
20746 return $billing;
20747 }
20748
20749 /**
20750 * @author Vova Feldman (@svovaf)
20751 * @since 1.0.5
20752 *
20753 * @param FS_Plugin_License[] $licenses
20754 * @param number $module_id
20755 */
20756 private function _update_licenses( $licenses, $module_id ) {
20757 $this->_logger->entrance();
20758
20759 if ( is_array( $licenses ) ) {
20760 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
20761 $licenses[ $i ]->updated = time();
20762 }
20763 }
20764
20765 $this->_store_licenses( true, $module_id, $licenses );
20766 }
20767
20768 /**
20769 * @author Vova Feldman (@svovaf)
20770 * @since 1.0.4
20771 *
20772 * @param bool|number $plugin_id
20773 * @param bool $flush Since 1.1.7.3
20774 * @param int $expiration Since 1.2.2.7
20775 * @param bool|string $newer_than Since 2.2.1
20776 *
20777 * @return object|false New plugin tag info if exist.
20778 */
20779 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
20780 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
20781
20782 if ( ! is_object( $latest_tag ) ) {
20783 return false;
20784 }
20785
20786 $plugin_version = $this->get_plugin_version();
20787
20788 // Check if version is actually newer.
20789 $has_new_version =
20790 // If it's an non-installed add-on then always return latest.
20791 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
20792 // Compare versions.
20793 version_compare( $plugin_version, $latest_tag->version, '<' );
20794
20795 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
20796
20797 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
20798
20799 $this->_storage->beta_data = array(
20800 'is_beta' => $is_latest_version_beta,
20801 'version' => $latest_tag->version
20802 );
20803
20804 return $has_new_version ? $latest_tag : false;
20805 }
20806
20807 /**
20808 * @author Vova Feldman (@svovaf)
20809 * @since 1.0.5
20810 *
20811 * @param bool|number $plugin_id
20812 * @param bool $flush Since 1.1.7.3
20813 * @param int $expiration Since 1.2.2.7
20814 * @param bool|string $newer_than Since 2.2.1
20815 *
20816 * @return bool|FS_Plugin_Tag
20817 */
20818 function get_update( $plugin_id = false, $flush = true, $expiration = FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION, $newer_than = false ) {
20819 $this->_logger->entrance();
20820
20821 if ( ! is_numeric( $plugin_id ) ) {
20822 $plugin_id = $this->_plugin->id;
20823 }
20824
20825 $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than );
20826 $updates = $this->get_all_updates();
20827
20828 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
20829 }
20830
20831 /**
20832 * Check if site assigned with active license.
20833 *
20834 * @author Vova Feldman (@svovaf)
20835 * @since 1.0.6
20836 *
20837 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
20838 */
20839 function has_active_license() {
20840 return (
20841 is_object( $this->_license ) &&
20842 is_numeric( $this->_license->id ) &&
20843 ! $this->_license->is_expired()
20844 );
20845 }
20846
20847 /**
20848 * Check if site assigned with active & valid (not expired) license.
20849 *
20850 * @author Vova Feldman (@svovaf)
20851 * @since 1.2.1
20852 *
20853 * @param bool $check_expiration
20854 */
20855 function has_active_valid_license( $check_expiration = true ) {
20856 return self::is_active_valid_license( $this->_license, $check_expiration );
20857 }
20858
20859 /**
20860 * @author Leo Fajardo (@leorw)
20861 * @since 2.3.1
20862 */
20863 function is_data_debug_mode() {
20864 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
20865 return false;
20866 }
20867
20868 $fs = $this->is_addon() ?
20869 $this->get_parent_instance() :
20870 $this;
20871
20872 if ( $fs->is_network_active() && fs_is_network_admin() ) {
20873 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
20874 } else {
20875 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
20876 }
20877
20878 return ( 'true' === $is_developer_license_debug_mode );
20879 }
20880
20881 /**
20882 * @author Leo Fajardo (@leorw)
20883 * @since 2.3.1
20884 */
20885 function _set_data_debug_mode() {
20886 if ( ! $this->is_whitelabeled( true ) ) {
20887 return;
20888 }
20889
20890 $license_or_user_key = fs_request_get_raw( 'license_or_user_key' );
20891
20892 $transient_value = ( ! empty( $license_or_user_key ) ) ?
20893 'true' :
20894 'false';
20895
20896 if ( 'true' === $transient_value ) {
20897 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
20898 'last_license_key' :
20899 'last_license_user_key'
20900 );
20901
20902 if ( md5( $license_or_user_key ) !== $stored_key ) {
20903 $this->shoot_ajax_failure( sprintf(
20904 '%s... %s',
20905 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
20906 $this->get_text_inline(
20907 'seems like the key you entered doesn\'t match our records.',
20908 'developer-or-license-not-found'
20909 )
20910 ) );
20911 }
20912 }
20913
20914 if ( $this->is_network_active() && fs_is_network_admin() ) {
20915 set_site_transient(
20916 "fs_{$this->get_id()}_data_debug_mode",
20917 $transient_value,
20918 WP_FS__TIME_24_HOURS_IN_SEC / 24
20919 );
20920 } else {
20921 set_transient(
20922 "fs_{$this->get_id()}_data_debug_mode",
20923 $transient_value,
20924 WP_FS__TIME_24_HOURS_IN_SEC / 24
20925 );
20926 }
20927
20928 if ( 'true' === $transient_value ) {
20929 $this->_admin_notices->add_sticky(
20930 $this->get_text_inline(
20931 'Debug mode was successfully enabled and will be automatically disabled in 60 min. You can also disable it earlier by clicking the "Stop Debug" link.',
20932 'data_debug_mode_enabled'
20933 ),
20934 'data_debug_mode_enabled'
20935 );
20936 }
20937
20938 $this->shoot_ajax_success();
20939 }
20940
20941 /**
20942 * Check if a given license is active & valid (not expired).
20943 *
20944 * @author Vova Feldman (@svovaf)
20945 * @since 2.1.3
20946 *
20947 * @param FS_Plugin_License $license
20948 * @param bool $check_expiration
20949 *
20950 * @return bool
20951 */
20952 private static function is_active_valid_license( $license, $check_expiration = true ) {
20953 return (
20954 is_object( $license ) &&
20955 FS_Plugin_License::is_valid_id( $license->id ) &&
20956 $license->is_active() &&
20957 ( ! $check_expiration || $license->is_valid() )
20958 );
20959 }
20960
20961 /**
20962 * Checks if there's any site that is associated with an active & valid license.
20963 * This logic is used to determine if the admin can download the premium code base from a network level admin.
20964 *
20965 * @author Vova Feldman (@svovaf)
20966 * @since 2.1.3
20967 *
20968 * @return bool
20969 */
20970 function has_any_active_valid_license() {
20971 if ( ! fs_is_network_admin() ) {
20972 return $this->has_active_valid_license();
20973 }
20974
20975 $installs = $this->get_blog_install_map();
20976 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
20977
20978 foreach ( $installs as $blog_id => $install ) {
20979 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20980 continue;
20981 }
20982
20983 foreach ( $all_plugin_licenses as $license ) {
20984 if ( $license->id == $install->license_id ) {
20985 if ( self::is_active_valid_license( $license ) ) {
20986 return true;
20987 }
20988 }
20989 }
20990 }
20991
20992 return false;
20993 }
20994
20995 /**
20996 * Check if site assigned with license with enabled features.
20997 *
20998 * @author Vova Feldman (@svovaf)
20999 * @since 1.0.6
21000 *
21001 * @return bool
21002 */
21003 function has_features_enabled_license() {
21004 return (
21005 is_object( $this->_license ) &&
21006 is_numeric( $this->_license->id ) &&
21007 $this->_license->is_features_enabled()
21008 );
21009 }
21010
21011 /**
21012 * Checks if the product is activated with a bundle license.
21013 *
21014 * @author Leo Fajardo (@leorw)
21015 * @since 2.4.0
21016 *
21017 * @return bool
21018 */
21019 function is_activated_with_bundle_license() {
21020 if ( ! $this->has_features_enabled_license() ) {
21021 return false;
21022 }
21023
21024 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
21025 }
21026
21027 /**
21028 * Check if user is a trial or have feature enabled license.
21029 *
21030 * @author Vova Feldman (@svovaf)
21031 * @since 1.1.7
21032 *
21033 * @return bool
21034 */
21035 function can_use_premium_code() {
21036 return $this->is_trial() || $this->has_features_enabled_license();
21037 }
21038
21039 /**
21040 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
21041 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
21042 * the context user is authenticated.
21043 *
21044 * @author Leo Fajardo (@leorw)
21045 * @since 1.2.2
21046 *
21047 * @return bool
21048 */
21049 function is_user_admin() {
21050 /**
21051 * Require a super-admin when network activated, running from the network level OR if
21052 * running from the site level but not delegated the opt-in.
21053 *
21054 * @author Vova Feldman (@svovaf)
21055 * @since 2.0.0
21056 */
21057 if ( $this->_is_network_active &&
21058 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
21059 ) {
21060 return is_super_admin();
21061 }
21062
21063 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
21064 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
21065 }
21066
21067 /**
21068 * Sync site's plan.
21069 *
21070 * @author Vova Feldman (@svovaf)
21071 * @since 1.0.3
21072 *
21073 * @uses FS_Api
21074 *
21075 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
21076 * the admin.
21077 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
21078 * network-level "Account" page.
21079 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
21080 * `_sync_plugin_license` method in order to switch to the previous blog when sending
21081 * updates for a single site in case `execute_cron` has switched to a different blog.
21082 */
21083 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
21084 $this->_logger->entrance();
21085
21086 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
21087
21088 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
21089
21090 if ( $is_addon_sync ) {
21091 $this->_sync_addon_license( $plugin_id, $background );
21092 } else {
21093 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
21094 }
21095
21096 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
21097 }
21098
21099 /**
21100 * Sync plugin's add-on license.
21101 *
21102 * @author Vova Feldman (@svovaf)
21103 * @since 1.0.6
21104 * @uses FS_Api
21105 *
21106 * @param number $addon_id
21107 * @param bool $background
21108 */
21109 private function _sync_addon_license( $addon_id, $background ) {
21110 $this->_logger->entrance();
21111
21112 if ( $this->is_addon_activated( $addon_id ) ) {
21113 // If already installed, use add-on sync.
21114 $fs_addon = self::get_instance_by_id( $addon_id );
21115
21116 if (
21117 // Add-on is network activated and network integrated.
21118 $fs_addon->is_network_active() ||
21119 // Background sync cron.
21120 self::is_cron() ||
21121 // Add-on is not network activated or not network integrated.
21122 ! fs_is_network_admin()
21123 ) {
21124 $fs_addon->_sync_license( $background );
21125
21126 return;
21127 }
21128 }
21129
21130 // Validate add-on exists.
21131 $addon = $this->get_addon( $addon_id );
21132
21133 if ( ! is_object( $addon ) ) {
21134 return;
21135 }
21136
21137 // Add add-on into account add-ons.
21138 $account_addons = $this->get_account_addons();
21139 if ( ! is_array( $account_addons ) ) {
21140 $account_addons = array();
21141 }
21142 $account_addons[] = $addon->id;
21143 $account_addons = array_unique( $account_addons );
21144 $this->_store_account_addons( $account_addons );
21145
21146 // Load add-on licenses.
21147 $licenses = $this->_fetch_licenses( $addon->id );
21148
21149 // Sync add-on licenses.
21150 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
21151 $this->_update_licenses( $licenses, $addon->id );
21152
21153 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
21154 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
21155
21156 if ( ! isset( $plans_result->error ) ) {
21157 $plans = array();
21158 foreach ( $plans_result->plans as $plan ) {
21159 $plans[] = new FS_Plugin_Plan( $plan );
21160 }
21161
21162 $this->_admin_notices->add_sticky(
21163 sprintf(
21164 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
21165 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
21166 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
21167 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
21168 $addon->title
21169 ) . ' ' . $this->get_latest_download_link(
21170 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
21171 $addon_id
21172 ),
21173 'addon_plan_upgraded_' . $addon->slug,
21174 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
21175 );
21176 }
21177 }
21178 }
21179 }
21180
21181 /**
21182 * Sync site's plugin plan.
21183 *
21184 * @author Vova Feldman (@svovaf)
21185 * @since 1.0.6
21186 * @uses FS_Api
21187 *
21188 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
21189 * @param bool $send_installs_update Since 2.0.0
21190 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
21191 * syncing its license from the network-level "Account" page (e.g.: after
21192 * activating a license only for the single install).
21193 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
21194 * can be used here to switch to the previous blog in case `execute_cron`
21195 * has switched to a different blog.
21196 */
21197 private function _sync_plugin_license(
21198 $background = false,
21199 $send_installs_update = true,
21200 $is_context_single_site = false,
21201 $current_blog_id = null
21202 ) {
21203 $this->_logger->entrance();
21204
21205 $plan_change = 'none';
21206
21207 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
21208
21209 if ( ! $send_installs_update ) {
21210 $site = $this->_site;
21211 } else {
21212 /**
21213 * Sync site info.
21214 *
21215 * @todo This line will execute install sync on a daily basis, even if running the free version (for opted-in users). The reason we want to keep it that way is for cases when the user was a paying customer, then there was a failure in subscription payment, and then after some time the payment was successful. This could be heavily optimized. For example, we can skip the $flush if the current install was never associated with a paid version.
21216 */
21217 if ( $is_site_level_sync ) {
21218 /**
21219 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
21220 *
21221 * @author Leo Fajardo (@leorw)
21222 * @since 2.2.3
21223 */
21224 if ( is_numeric( $current_blog_id ) ) {
21225 $this->switch_to_blog( $current_blog_id );
21226 }
21227
21228 $result = $this->send_install_update( array(), true, true );
21229 $is_valid = $this->is_api_result_entity( $result );
21230 } else {
21231 $result = $this->send_installs_update( array(), true, true );
21232 $is_valid = $this->is_api_result_object( $result, 'installs' );
21233 }
21234
21235 if ( ! $is_valid ) {
21236 if ( $is_context_single_site ) {
21237 // Switch back to the main blog so that the following logic will have the right entities.
21238 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21239 }
21240
21241 // Show API message only if not background sync or if paying customer.
21242 if ( ! $background || $this->is_paying() ) {
21243 // Try to ping API to see if not blocked.
21244 if ( FS_Api::is_blocked( $result ) ) {
21245 /**
21246 * @author Vova Feldman (@svovaf)
21247 * @since 1.1.6 Only show message related to one of the Freemius powered plugins. Once it will be resolved it will fix the issue for all plugins anyways. There's no point to scare users with multiple error messages.
21248 */
21249 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
21250 // Add notice immediately if not a background sync.
21251 $add_notice = ( ! $background );
21252
21253 if ( ! $add_notice ) {
21254 $counter = (int) get_transient( '_fs_api_connection_retry_counter' );
21255
21256 // We only want to add the notice after 3 consecutive failures.
21257 $add_notice = ( 3 <= $counter );
21258
21259 if ( ! $add_notice ) {
21260 /**
21261 * Update counter transient only if notice shouldn't be added. If it is added the transient will be reset anyway, because the retries mechanism should only start counting if the admin isn't aware of the connectivity issue.
21262 *
21263 * Also, since the background sync happens once a day, setting the transient expiration for a week should be enough to count 3 failures, if there's an actual connectivity issue.
21264 */
21265 set_transient( '_fs_api_connection_retry_counter', $counter + 1, WP_FS__TIME_WEEK_IN_SEC );
21266 }
21267 }
21268
21269 // Add notice instantly for not-background sync and only after 3 failed attempts for background sync.
21270 if ( $add_notice ) {
21271 self::$_global_admin_notices->add(
21272 $this->generate_api_blocked_notice_message_from_result( $result ),
21273 '',
21274 'error',
21275 $background,
21276 'api_blocked'
21277 );
21278
21279 add_action( 'admin_footer', array( 'Freemius', '_add_api_connectivity_notice_handler_js' ) );
21280
21281 // Notice was just shown, reset connectivity counter.
21282 delete_transient( '_fs_api_connection_retry_counter' );
21283 }
21284 }
21285 } else if ( is_object( $result ) ) {
21286 // Authentication params are broken.
21287 $this->_admin_notices->add(
21288 $this->get_text_inline( 'It seems like one of the authentication parameters is wrong. Update your Public Key, Secret Key & User ID, and try again.', 'wrong-authentication-param-message' ) . '<br> ' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . var_export( $result->error, true ),
21289 '',
21290 'error'
21291 );
21292 }
21293 }
21294
21295 // No reason to continue with license sync while there are API issues.
21296 return;
21297 }
21298
21299 // API is working now. Delete the transient and start afresh.
21300 delete_transient('_fs_api_connection_retry_counter');
21301
21302 if ( $is_site_level_sync ) {
21303 $site = new FS_Site( $result );
21304 } else {
21305 // Map site addresses to their blog IDs.
21306 $address_to_blog_map = $this->get_address_to_blog_map();
21307
21308 // Find the current context install.
21309 $site = null;
21310 foreach ( $result->installs as $install ) {
21311 if ( $install->id == $this->_site->id ) {
21312 $site = new FS_Site( $install );
21313 } else {
21314 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
21315 $blog_id = $address_to_blog_map[ $address ];
21316
21317 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
21318 }
21319 }
21320 }
21321
21322 // Sync plans.
21323 $this->_sync_plans();
21324 }
21325
21326 // Remove sticky API connectivity message.
21327 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
21328
21329 if ( ! $this->has_paid_plan() ) {
21330 $this->_site = $site;
21331 $this->_store_site(
21332 true,
21333 $is_site_level_sync ?
21334 null :
21335 $this->get_network_install_blog_id()
21336 );
21337 } else {
21338 $context_blog_id = 0;
21339
21340 if ( $is_context_single_site ) {
21341 $context_blog_id = get_current_blog_id();
21342
21343 // Switch back to the main blog in order to properly sync the license.
21344 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21345 }
21346
21347 /**
21348 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
21349 * associated with that ID is not included in the user's licenses collection.
21350 * Save previous value to manage remote license renewals.
21351 */
21352 $was_license_expired_before_sync = is_object( $this->_license ) && $this->_license->is_expired();
21353 $this->_sync_licenses(
21354 $site->license_id,
21355 ( $is_context_single_site ?
21356 $context_blog_id :
21357 null
21358 )
21359 );
21360
21361 if ( $is_context_single_site ) {
21362 $this->switch_to_blog( $context_blog_id );
21363 }
21364
21365 // Check if plan / license changed.
21366 if ( $site->plan_id != $this->_site->plan_id ||
21367 // Check if trial started.
21368 $site->trial_plan_id != $this->_site->trial_plan_id ||
21369 $site->trial_ends != $this->_site->trial_ends ||
21370 // Check if license changed.
21371 $site->license_id != $this->_site->license_id
21372 ) {
21373 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
21374 // New trial started.
21375 $this->_site = $site;
21376 $plan_change = 'trial_started';
21377
21378 // For trial with subscription use-case.
21379 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
21380
21381 if ( is_object( $new_license ) && $new_license->is_valid() ) {
21382 $this->_site = $site;
21383 $this->_update_site_license( $new_license );
21384 $this->_store_licenses();
21385
21386 $this->_sync_site_subscription( $this->_license );
21387 }
21388 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
21389 // Was in trial, but now trial expired and no license ID.
21390 // New trial started.
21391 $this->_site = $site;
21392 $plan_change = 'trial_expired';
21393 } else {
21394 $is_free = $this->is_free_plan();
21395
21396 // Make sure license exist and not expired.
21397 $new_license = is_null( $site->license_id ) ?
21398 null :
21399 $this->_get_license_by_id( $site->license_id );
21400
21401 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
21402 // License cancelled.
21403 $this->_site = $site;
21404 $this->_update_site_license( $new_license );
21405 $this->_store_licenses();
21406
21407 $plan_change = 'cancelled';
21408 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
21409 // The license is expired, so ignore upgrade method.
21410 $this->_site = $site;
21411 } else {
21412 // License changed.
21413 $this->_site = $site;
21414
21415 /**
21416 * IMPORTANT:
21417 * The line below should be executed before trying to activate the license on the rest of the network, otherwise, the license' activation counters may be out of sync + there's no need to activate the license on the context site since it's already activated on it.
21418 *
21419 * @author Vova Feldman (@svovaf)
21420 * @since 2.0.0
21421 */
21422 $this->_update_site_license( $new_license );
21423
21424 if ( ! $is_context_single_site &&
21425 fs_is_network_admin() &&
21426 $this->_is_network_active &&
21427 $new_license->quota > 1 &&
21428 get_blog_count() > 1
21429 ) {
21430 // See if license can activated on all sites.
21431 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
21432 if ( ! fs_request_get_bool( 'auto_install' ) ) {
21433 // Open the license activation dialog box on the account page.
21434 add_action( 'admin_footer', array(
21435 &$this,
21436 '_open_license_activation_dialog_box'
21437 ) );
21438 }
21439 }
21440 }
21441
21442 $this->_store_licenses();
21443
21444 $plan_change = $is_free ?
21445 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
21446 ( is_object( $new_license ) ?
21447 'changed' :
21448 'downgraded' );
21449 }
21450 }
21451
21452 // Store updated site info.
21453 $this->_store_site(
21454 true,
21455 $is_site_level_sync ?
21456 null :
21457 $this->get_network_install_blog_id()
21458 );
21459 } else {
21460 if ( ! is_object( $this->_license ) ) {
21461 $this->maybe_update_whitelabel_flag(
21462 FS_Plugin_License::is_valid_id( $site->license_id ) ?
21463 $this->get_license_by_id( $site->license_id ) :
21464 null
21465 );
21466 } else {
21467 $this->maybe_update_whitelabel_flag( $this->_license );
21468
21469 if ( $this->_license->is_expired() ) {
21470 if ( ! $this->has_features_enabled_license() ) {
21471 $this->_deactivate_license();
21472 $plan_change = 'downgraded';
21473 } else {
21474 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
21475
21476 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
21477 /**
21478 * Show the expired license notice every 14 days.
21479 *
21480 * @author Leo Fajardo (@leorw)
21481 * @since 2.3.1
21482 */
21483 $plan_change = 'expired';
21484 }
21485 }
21486 } else if ( $was_license_expired_before_sync ) {
21487 /**
21488 * If license was expired but it is not anymore.
21489 *
21490 *
21491 * @author Daniele Alessandra (@danielealessandra)
21492 */
21493 $plan_change = 'extended';
21494 }
21495 }
21496
21497 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
21498 $this->_sync_site_subscription( $this->_license );
21499 }
21500 }
21501
21502 if ( ! $this->is_addon() &&
21503 $this->_site->is_beta() !== $site->is_beta()
21504 ) {
21505 // Beta flag updated.
21506 $this->_site = $site;
21507
21508 $this->_store_site(
21509 true,
21510 $is_site_level_sync ?
21511 null :
21512 $this->get_network_install_blog_id()
21513 );
21514 }
21515
21516 if ( $this->is_addon() || $this->has_addons() ) {
21517 /**
21518 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21519 * an updated valid user licenses collection will be fetched from the server which is used to also
21520 * update the account add-ons (add-ons the user has licenses for).
21521 *
21522 * @author Leo Fajardo (@leorw)
21523 * @since 2.2.4
21524 */
21525 $this->purge_valid_user_licenses_cache();
21526 }
21527 }
21528
21529 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21530
21531 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
21532 switch ( $plan_change ) {
21533 case 'none':
21534 if ( ! $background && is_admin() ) {
21535 $plan = $this->is_trial() ?
21536 $this->get_trial_plan() :
21537 $this->get_plan();
21538
21539 if ( $plan->is_free() ) {
21540 $this->_admin_notices->add(
21541 sprintf(
21542 $this->get_text_inline( 'It looks like you are still on the %s plan. If you did upgrade or change your plan, it\'s probably an issue on our side - sorry.', 'plan-did-not-change-message' ),
21543 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
21544 ) . ' ' . sprintf(
21545 '<a href="%s">%s</a>',
21546 $this->contact_url(
21547 'bug',
21548 sprintf( $this->get_text_inline( 'I have upgraded my account but when I try to Sync the License, the plan remains %s.', 'plan-did-not-change-email-message' ),
21549 strtoupper( $plan->name )
21550 )
21551 ),
21552 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21553 ),
21554 $hmm_text
21555 );
21556 }
21557 }
21558 break;
21559 case 'upgraded':
21560 case 'activated':
21561 $this->add_after_plan_activation_or_upgrade_instructions_notice( 'upgraded' === $plan_change );
21562
21563 $this->_admin_notices->remove_sticky( array(
21564 'trial_started',
21565 'trial_promotion',
21566 'trial_expired',
21567 'activation_complete',
21568 'license_expired',
21569 ) );
21570 break;
21571 case 'extended':
21572 $this->_admin_notices->remove_sticky( array(
21573 'trial_expired',
21574 'license_expired',
21575 ) );
21576 break;
21577 case 'changed':
21578 $this->_admin_notices->add_sticky(
21579 sprintf(
21580 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
21581 $this->get_plan_title()
21582 ),
21583 'plan_changed'
21584 );
21585
21586 $this->_admin_notices->remove_sticky( array(
21587 'trial_started',
21588 'trial_promotion',
21589 'trial_expired',
21590 'activation_complete',
21591 ) );
21592 break;
21593 case 'downgraded':
21594 $this->_admin_notices->add_sticky(
21595 ($this->has_free_plan() ?
21596 sprintf( $this->get_text_inline( 'Your license has expired. You can still continue using the free %s forever.', 'license-expired-blocking-message' ), $this->_module_type ) :
21597 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21598 sprintf( $this->get_text_inline( 'Your license has expired. %1$sUpgrade now%2$s to continue using the %3$s without interruptions.', 'license-expired-blocking-message_premium-only' ), sprintf('<a href="%s">', $this->pricing_url()), '</a>', $this->get_module_label(true) ) ),
21599 'license_expired',
21600 $hmm_text
21601 );
21602 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21603 break;
21604 case 'cancelled':
21605 $this->_admin_notices->add(
21606 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
21607 sprintf(
21608 '<a href="%s">%s</a>',
21609 $this->contact_url( 'bug' ),
21610 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21611 ),
21612 $hmm_text,
21613 'error'
21614 );
21615 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21616 break;
21617 case 'expired':
21618 $this->_admin_notices->add_sticky(
21619 sprintf( $this->get_text_inline( 'Your license has expired. You can still continue using all the %s features, but you\'ll need to renew your license to continue getting updates and support.', 'license-expired-non-blocking-message' ), $this->get_plan()->title ),
21620 'license_expired',
21621 $hmm_text
21622 );
21623
21624 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
21625
21626 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21627 break;
21628 case 'trial_started':
21629 $this->add_complete_upgrade_instructions_notice(
21630 sprintf(
21631 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
21632 '<i>' . $this->get_plugin_name() . '</i>'
21633 ),
21634 'trial_started',
21635 $this->get_trial_plan()->title
21636 );
21637
21638 $this->_admin_notices->remove_sticky( array(
21639 'trial_promotion',
21640 ) );
21641 break;
21642 case 'trial_expired':
21643 $this->_admin_notices->add_sticky(
21644 ($this->has_free_plan() ?
21645 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
21646 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21647 sprintf( $this->get_text_inline( 'Your free trial has expired. %1$sUpgrade now%2$s to continue using the %3$s without interruptions.', 'trial-expired-message_premium-only' ), sprintf('<a href="%s">', $this->pricing_url()), '</a>', $this->get_module_label(true))),
21648 'trial_expired',
21649 $hmm_text
21650 );
21651 $this->_admin_notices->remove_sticky( array(
21652 'trial_started',
21653 'trial_promotion',
21654 'plan_upgraded',
21655 ) );
21656 break;
21657 }
21658 }
21659
21660 if ( 'none' !== $plan_change ) {
21661 if (
21662 ! is_object( $this->_license ) ||
21663 ! $this->_license->is_whitelabeled
21664 ) {
21665 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
21666 }
21667
21668 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
21669 }
21670 }
21671
21672 /**
21673 * @author Leo Fajardo (@leorw)
21674 * @since 2.5.4
21675 *
21676 * @param mixed $result
21677 *
21678 * @return string
21679 */
21680 private function generate_api_blocked_notice_message_from_result( $result ) {
21681 $api_domains = $this->apply_filters( 'api_domains', array(
21682 'api.freemius.com',
21683 'wp.freemius.com',
21684 ) );
21685
21686 $api_domains_list_items = '';
21687
21688 foreach( $api_domains as $api_domain ) {
21689 $api_domains_list_items .= "<li>{$api_domain}</li>";
21690 }
21691
21692 $error_message = sprintf(
21693 $this->get_text_inline( 'Your server is blocking the access to Freemius\' API, which is crucial for %1$s synchronization. Please contact your host to whitelist the following domains:%2$s', 'server-blocking-access' ),
21694 $this->get_plugin_name(),
21695 "<ol>{$api_domains_list_items}</ol><a href='#' class='fs-api-request-error-show-details-link'>" . $this->get_text_inline( 'Show error details', 'show-error-details' ) . " <span class='dashicons dashicons-arrow-down-alt2'></span></a>"
21696 );
21697
21698 $error_message =
21699 "<div>{$error_message}</div>" .
21700 '<div class="fs-api-request-error-details" style="display: none">' .
21701 '<strong>' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . '</strong><br>' .
21702 $result->error->message .
21703 '</div>';
21704
21705 return $error_message;
21706 }
21707
21708 /**
21709 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
21710 *
21711 * @author Vova Feldman (@svovaf)
21712 * @since 2.0.0
21713 */
21714 public function _open_license_activation_dialog_box() {
21715 $vars = array( 'license_id' => $this->_site->license_id );
21716 fs_require_once_template( 'js/open-license-activation.php', $vars );
21717 }
21718
21719 /**
21720 * @author Vova Feldman (@svovaf)
21721 * @since 1.0.5
21722 *
21723 * @param bool $background
21724 * @param FS_Plugin_License|null $premium_license
21725 */
21726 protected function _activate_license( $background = false, $premium_license = null ) {
21727 $this->_logger->entrance();
21728
21729 if ( is_null( $premium_license ) ) {
21730 $license_id = fs_request_get( 'license_id' );
21731
21732 if ( is_object( $this->_site ) &&
21733 FS_Plugin_License::is_valid_id( $license_id ) &&
21734 $license_id == $this->_site->license_id
21735 ) {
21736 // License is already activated.
21737 return;
21738 }
21739
21740 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
21741 $this->_get_license_by_id( $license_id ) :
21742 $this->_get_available_premium_license();
21743 }
21744
21745 if ( ! is_object( $premium_license ) ) {
21746 return;
21747 }
21748
21749 if ( ! is_object( $this->_site ) ) {
21750 // Not yet opted-in.
21751 $user = $this->get_current_or_network_user();
21752 if ( ! is_object( $user ) ) {
21753 $user = self::_get_user_by_id( $premium_license->user_id );
21754 }
21755
21756 if ( is_object( $user ) ) {
21757 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
21758 } else {
21759 $this->opt_in(
21760 false,
21761 false,
21762 false,
21763 $premium_license->secret_key
21764 );
21765
21766 return;
21767 }
21768 }
21769
21770
21771 /**
21772 * If the premium license is already associated with the install, just
21773 * update the license reference (activation is not required).
21774 *
21775 * @since 1.1.9
21776 */
21777 if ( $premium_license->id == $this->_site->license_id ) {
21778 // License is already activated.
21779 $this->_update_site_license( $premium_license );
21780 $this->_store_account();
21781
21782 return;
21783 }
21784
21785 if ( $this->_site->user_id != $premium_license->user_id ) {
21786 $api_request_params = array( 'license_key' => $premium_license->secret_key );
21787 } else {
21788 $api_request_params = array();
21789 }
21790
21791 $api = $this->get_api_site_scope();
21792 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
21793
21794 if ( ! $this->is_api_result_entity( $license ) ) {
21795 if ( ! $background ) {
21796 $this->_admin_notices->add( sprintf(
21797 '%s %s',
21798 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
21799 ( is_object( $license ) && isset( $license->error ) ?
21800 $license->error->message :
21801 sprintf( '%s<br><code>%s</code>',
21802 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
21803 var_export( $license, true )
21804 )
21805 )
21806 ),
21807 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
21808 'error'
21809 );
21810 }
21811
21812 return;
21813 }
21814
21815 $premium_license = new FS_Plugin_License( $license );
21816
21817 // Updated site plan.
21818 $site = $this->get_api_site_scope()->get( '/', true );
21819 if ( $this->is_api_result_entity( $site ) ) {
21820 $this->_site = new FS_Site( $site );
21821 }
21822 $this->_update_site_license( $premium_license );
21823
21824 $this->_store_account();
21825
21826 if ( $this->is_addon() || $this->has_addons() ) {
21827 /**
21828 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21829 * an updated valid user licenses collection will be fetched from the server which is used to also
21830 * update the account add-ons (add-ons the user has licenses for).
21831 *
21832 * @author Leo Fajardo (@leorw)
21833 * @since 2.2.4
21834 */
21835 $this->purge_valid_user_licenses_cache();
21836 }
21837
21838 if ( ! $background ) {
21839 $this->add_complete_upgrade_instructions_notice(
21840 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ),
21841 'license_activated'
21842 );
21843 }
21844
21845 $this->_admin_notices->remove_sticky( array(
21846 'trial_promotion',
21847 'license_expired',
21848 ) );
21849 }
21850
21851 /**
21852 * @author Vova Feldman (@svovaf)
21853 * @since 1.0.5
21854 *
21855 * @param bool $show_notice
21856 */
21857 protected function _deactivate_license( $show_notice = true ) {
21858 $this->_logger->entrance();
21859
21860 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21861
21862 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
21863 $this->_admin_notices->add(
21864 sprintf( $this->get_text_inline( 'It looks like your site currently doesn\'t have an active license.', 'no-active-license-message' ), $this->get_plan_title() ),
21865 $hmm_text
21866 );
21867
21868 return;
21869 }
21870
21871 $api = $this->get_api_site_scope();
21872 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
21873
21874 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
21875 }
21876
21877 /**
21878 * @author Leo Fajardo (@leorw)
21879 * @since 2.2.1
21880 *
21881 * @param FS_Plugin_License $license
21882 * @param bool|string $hmm_text
21883 * @param bool $show_notice
21884 */
21885 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
21886 if ( isset( $license->error ) ) {
21887 $this->_admin_notices->add(
21888 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
21889 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
21890 $hmm_text,
21891 'error'
21892 );
21893
21894 return;
21895 }
21896
21897 // Update license cache.
21898 if ( is_array( $this->_licenses ) ) {
21899 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
21900 if ( $license->id == $this->_licenses[ $i ]->id ) {
21901 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
21902 }
21903 }
21904 }
21905
21906 // Update site plan to default.
21907 $this->_sync_plans();
21908 $this->_site->plan_id = $this->_plans[0]->id;
21909 // Unlink license from site.
21910 $this->_update_site_license( null );
21911
21912 $this->_store_account();
21913
21914 if ( $show_notice ) {
21915 $this->_admin_notices->add(
21916 sprintf( $this->is_only_premium() ?
21917 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
21918 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
21919 $this->get_plan_title()
21920 ),
21921 $this->get_text_inline( 'O.K', 'ok' )
21922 );
21923 }
21924
21925 $this->_admin_notices->remove_sticky( array(
21926 'plan_upgraded',
21927 'license_activated',
21928 ) );
21929 }
21930
21931 /**
21932 * Site plan downgrade.
21933 *
21934 * @author Vova Feldman (@svovaf)
21935 * @since 1.0.4
21936 *
21937 * @return object
21938 *
21939 * @uses FS_Api
21940 */
21941 private function _downgrade_site() {
21942 $this->_logger->entrance();
21943
21944 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
21945
21946 $api = $this->get_api_site_scope();
21947 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
21948
21949 $plan_downgraded = false;
21950 $plan = false;
21951 if ( $this->is_api_result_entity( $site ) ) {
21952 $prev_plan_id = $this->_site->plan_id;
21953
21954 // Update new site plan id.
21955 $this->_site->plan_id = $site->plan_id;
21956
21957 $plan = $this->get_plan();
21958 $subscription = $this->_sync_site_subscription( $this->_license );
21959
21960 // Plan downgraded if plan was changed or subscription was cancelled.
21961 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
21962 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
21963 } else {
21964 // handle different error cases.
21965 $this->handle_license_deactivation_result(
21966 $site,
21967 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
21968 );
21969 }
21970
21971 if ( ! $plan_downgraded ) {
21972 return (object) array(
21973 'error' => (object) array(
21974 'message' => $this->get_text_inline( 'Seems like we are having some temporary issue with your subscription cancellation. Please try again in few minutes.', 'subscription-cancellation-failure-message' )
21975 )
21976 );
21977 }
21978
21979 // Remove previous sticky message about upgrade (if exist).
21980 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21981
21982 $this->_admin_notices->add(
21983 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
21984 $plan->title,
21985 human_time_diff( time(), strtotime( $this->_license->expiration ) )
21986 )
21987 );
21988
21989 // Store site updates.
21990 $this->_store_site();
21991
21992 if ( $deactivate_license &&
21993 ! FS_Plugin_License::is_valid_id( $site->license_id )
21994 ) {
21995 if ( $this->_site->is_localhost() ) {
21996 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
21997 } else {
21998 $this->_license->activated = max( 0, $this->_license->activated - 1 );
21999 }
22000
22001 // Handle successful license deactivation result.
22002 $this->handle_license_deactivation_result( $this->_license );
22003 }
22004
22005 return $site;
22006 }
22007
22008 /**
22009 * @author Vova Feldman (@svovaf)
22010 * @since 1.1.8.1
22011 *
22012 * @param bool|string $plan_name
22013 *
22014 * @return bool If trial was successfully started.
22015 */
22016 function start_trial( $plan_name = false ) {
22017 $this->_logger->entrance();
22018
22019 // Alias.
22020 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
22021
22022 if ( $this->is_trial() ) {
22023 // Already in trial mode.
22024 $this->_admin_notices->add(
22025 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
22026 $oops_text,
22027 'error'
22028 );
22029
22030 return false;
22031 }
22032
22033 if ( $this->_site->is_trial_utilized() ) {
22034 // Trial was already utilized.
22035 $this->_admin_notices->add(
22036 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
22037 $oops_text,
22038 'error'
22039 );
22040
22041 return false;
22042 }
22043
22044 if ( false !== $plan_name ) {
22045 $plan = $this->get_plan_by_name( $plan_name );
22046
22047 if ( false === $plan ) {
22048 // Plan doesn't exist.
22049 $this->_admin_notices->add(
22050 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
22051 $oops_text,
22052 'error'
22053 );
22054
22055 return false;
22056 }
22057
22058 if ( ! $plan->has_trial() ) {
22059 // Plan doesn't exist.
22060 $this->_admin_notices->add(
22061 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
22062 $oops_text,
22063 'error'
22064 );
22065
22066 return false;
22067 }
22068 } else {
22069 if ( ! $this->has_trial_plan() ) {
22070 // None of the plans have a trial.
22071 $this->_admin_notices->add(
22072 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
22073 $oops_text,
22074 'error'
22075 );
22076
22077 return false;
22078 }
22079
22080 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
22081
22082 $plan = $plans_with_trial[0];
22083 }
22084
22085 $api = $this->get_api_site_scope();
22086 $plan = $api->call( "plans/{$plan->id}/trials.json", 'post' );
22087
22088 if ( ! $this->is_api_result_entity( $plan ) ) {
22089 // Some API error while trying to start the trial.
22090 $this->_admin_notices->add(
22091 $this->get_api_error_message( $plan ),
22092 $oops_text,
22093 'error'
22094 );
22095
22096 return false;
22097 }
22098
22099 // Sync license.
22100 $this->_sync_license();
22101
22102 return $this->is_trial();
22103 }
22104
22105 /**
22106 * Cancel site trial.
22107 *
22108 * @author Vova Feldman (@svovaf)
22109 * @since 1.0.9
22110 *
22111 * @return object
22112 *
22113 * @uses FS_Api
22114 */
22115 private function _cancel_trial() {
22116 $this->_logger->entrance();
22117
22118 if ( ! $this->is_trial() ) {
22119 return (object) array(
22120 'error' => (object) array(
22121 'message' => $this->get_text_inline( 'It looks like you are not in trial mode anymore so there\'s nothing to cancel :)', 'trial-cancel-no-trial-message' )
22122 )
22123 );
22124 }
22125
22126 $trial_plan = $this->get_trial_plan();
22127
22128 $api = $this->get_api_site_scope();
22129 $site = $api->call( 'trials.json', 'delete' );
22130
22131 $trial_cancelled = false;
22132
22133 if ( $this->is_api_result_entity( $site ) ) {
22134 $prev_trial_ends = $this->_site->trial_ends;
22135
22136 if ( $this->is_paid_trial() ) {
22137 $this->_license->expiration = $site->trial_ends;
22138 $this->_license->is_cancelled = true;
22139 $this->_update_site_license( $this->_license );
22140 $this->_store_licenses();
22141
22142 // Clear subscription reference.
22143 $this->_sync_site_subscription( null );
22144 }
22145
22146 // Update site info.
22147 $this->_site = new FS_Site( $site );
22148
22149 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
22150 } else {
22151 // @todo handle different error cases.
22152 }
22153
22154 if ( ! $trial_cancelled ) {
22155 return (object) array(
22156 'error' => (object) array(
22157 'message' => $this->get_text_inline( 'Seems like we are having some temporary issue with your trial cancellation. Please try again in few minutes.', 'trial-cancel-failure-message' )
22158 )
22159 );
22160 }
22161
22162 // Remove previous sticky messages about upgrade or trial (if exist).
22163 $this->_admin_notices->remove_sticky( array(
22164 'trial_started',
22165 'trial_promotion',
22166 'plan_upgraded',
22167 ) );
22168
22169 // Store site updates.
22170 $this->_store_site();
22171
22172 if ( ! $this->is_addon() ||
22173 ! $this->deactivate_premium_only_addon_without_license( true )
22174 ) {
22175 $this->_admin_notices->add(
22176 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
22177 );
22178 }
22179
22180 return $site;
22181 }
22182
22183 /**
22184 * @author Vova Feldman (@svovaf)
22185 * @since 1.0.6
22186 *
22187 * @param bool|number $plugin_id
22188 *
22189 * @return bool
22190 */
22191 private function _is_addon_id( $plugin_id ) {
22192 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
22193 }
22194
22195 /**
22196 * Check if user eligible to download premium version updates.
22197 *
22198 * @author Vova Feldman (@svovaf)
22199 * @since 1.0.6
22200 *
22201 * @return bool
22202 */
22203 private function _can_download_premium() {
22204 return $this->has_any_active_valid_license() ||
22205 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
22206 }
22207
22208 /**
22209 *
22210 * @author Vova Feldman (@svovaf)
22211 * @since 1.0.6
22212 *
22213 * @param bool|number $addon_id
22214 * @param string $type "json" or "zip"
22215 *
22216 * @return string
22217 */
22218 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
22219
22220 $is_addon = $this->_is_addon_id( $addon_id );
22221
22222 $is_premium = null;
22223 if ( ! $is_addon ) {
22224 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
22225 } else if ( $this->is_addon_activated( $addon_id ) ) {
22226 $fs_addon = self::get_instance_by_id( $addon_id );
22227 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
22228 }
22229
22230 // If add-on, then append add-on ID.
22231 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
22232 '/updates/latest.' . $type;
22233
22234 // If add-on and not yet activated, try to fetch based on server licensing.
22235 if ( is_bool( $is_premium ) ) {
22236 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
22237 }
22238
22239 if ( $this->has_secret_key() ) {
22240 $endpoint = add_query_arg( 'type', 'all', $endpoint );
22241 } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) {
22242 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
22243 }
22244
22245 return $endpoint;
22246 }
22247
22248 /**
22249 * @author Vova Feldman (@svovaf)
22250 * @since 1.0.4
22251 *
22252 * @param bool|number $addon_id
22253 * @param bool $flush Since 1.1.7.3
22254 * @param int $expiration Since 1.2.2.7
22255 * @param bool|string $newer_than Since 2.2.1
22256 * @param bool|string $fetch_readme Since 2.2.1
22257 *
22258 * @return object|false Plugin latest tag info.
22259 */
22260 function _fetch_latest_version(
22261 $addon_id = false,
22262 $flush = true,
22263 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22264 $newer_than = false,
22265 $fetch_readme = true
22266 ) {
22267 $this->_logger->entrance();
22268
22269 if ( $this->is_unresolved_clone( true ) ) {
22270 return false;
22271 }
22272
22273 $switch_to_blog_id = null;
22274
22275 /**
22276 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
22277 * @since 1.1.7.4 Also check updates for add-ons.
22278 */
22279 if (
22280 ( ! $this->is_registered() || ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed() ) &&
22281 ! $this->_is_addon_id( $addon_id )
22282 ) {
22283 if ( ! is_multisite() ) {
22284 return false;
22285 }
22286
22287 $installs_map = $this->get_blog_install_map();
22288
22289 foreach ( $installs_map as $blog_id => $install ) {
22290 if ( ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed( $blog_id ) ) {
22291 continue;
22292 }
22293
22294 /**
22295 * @var FS_Site $install
22296 */
22297 if ( $install->is_trial() ) {
22298 $switch_to_blog_id = $blog_id;
22299 break;
22300 }
22301
22302 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
22303 $license = $this->get_license_by_id( $install->license_id );
22304
22305 if ( is_object( $license ) && $license->is_features_enabled() ) {
22306 $switch_to_blog_id = $blog_id;
22307 break;
22308 }
22309 }
22310 }
22311
22312 if ( is_null( $switch_to_blog_id ) ) {
22313 return false;
22314 }
22315 }
22316
22317 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
22318 get_current_blog_id() :
22319 0;
22320
22321 if ( is_numeric( $switch_to_blog_id ) ) {
22322 $this->switch_to_blog( $switch_to_blog_id );
22323 }
22324
22325 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
22326
22327 if ( ! empty( $newer_than ) ) {
22328 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
22329 }
22330
22331 if ( true === $fetch_readme ) {
22332 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
22333 }
22334
22335 $tag = $this->get_api_site_or_plugin_scope()->get(
22336 $latest_version_endpoint,
22337 $flush,
22338 $expiration
22339 );
22340
22341 if ( is_numeric( $switch_to_blog_id ) ) {
22342 $this->switch_to_blog( $current_blog_id );
22343 }
22344
22345 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
22346
22347 $this->_logger->departure( 'Latest version ' . $latest_version );
22348
22349 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
22350 }
22351
22352 #----------------------------------------------------------------------------------
22353 #region Download Plugin
22354 #----------------------------------------------------------------------------------
22355
22356 /**
22357 * Download latest plugin version, based on plan.
22358 *
22359 * Not like _download_latest(), this will redirect the page
22360 * to secure download url to prevent dual download (from FS to WP server,
22361 * and then from WP server to the client / browser).
22362 *
22363 * @author Vova Feldman (@svovaf)
22364 * @since 1.0.9
22365 *
22366 * @param bool|number $plugin_id
22367 *
22368 * @uses FS_Api
22369 * @uses wp_redirect()
22370 */
22371 private function download_latest_directly( $plugin_id = false ) {
22372 $this->_logger->entrance();
22373
22374 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
22375 }
22376
22377 /**
22378 * Get latest plugin FS API download URL.
22379 *
22380 * @author Vova Feldman (@svovaf)
22381 * @since 1.0.9
22382 *
22383 * @param bool|number $plugin_id
22384 *
22385 * @return string
22386 */
22387 private function get_latest_download_api_url( $plugin_id = false ) {
22388 $this->_logger->entrance();
22389
22390 $download_api_url = $this->get_api_site_scope()->get_signed_url(
22391 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
22392 );
22393
22394 return str_replace( 'http:', 'https:', $download_api_url );
22395 }
22396
22397 /**
22398 * Get payment invoice URL.
22399 *
22400 * @author Vova Feldman (@svovaf)
22401 * @since 1.2.0
22402 *
22403 * @param bool|number $payment_id
22404 *
22405 * @return string
22406 */
22407 function _get_invoice_api_url( $payment_id = false ) {
22408 $this->_logger->entrance();
22409
22410 $url = $this->get_api_user_scope()->get_signed_url(
22411 "/payments/{$payment_id}/invoice.pdf"
22412 );
22413
22414 if ( ! fs_starts_with( $url, 'https://' ) ) {
22415 // Always use HTTPS for invoices.
22416 $url = 'https' . substr( $url, 4 );
22417 }
22418
22419 return $url;
22420 }
22421
22422 /**
22423 * Get latest plugin download link.
22424 *
22425 * @author Vova Feldman (@svovaf)
22426 * @since 1.0.9
22427 *
22428 * @param string $label
22429 * @param bool|number $plugin_id
22430 *
22431 * @return string
22432 */
22433 private function get_latest_download_link( $label, $plugin_id = false ) {
22434 return sprintf(
22435 '<a target="_blank" rel="noopener" href="%s">%s</a>',
22436 $this->_get_latest_download_local_url( $plugin_id ),
22437 $label
22438 );
22439 }
22440
22441 /**
22442 * Get latest plugin download local URL.
22443 *
22444 * @author Vova Feldman (@svovaf)
22445 * @since 1.0.9
22446 *
22447 * @param bool|number $plugin_id
22448 *
22449 * @return string
22450 */
22451 function _get_latest_download_local_url( $plugin_id = false ) {
22452 // Add timestamp to protect from caching.
22453 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
22454
22455 if ( ! empty( $plugin_id ) ) {
22456 $params['plugin_id'] = $plugin_id;
22457 } else if ( $this->is_addon() ) {
22458 $params['plugin_id'] = $this->get_id();
22459 }
22460
22461 $fs = $this->is_addon() ?
22462 $this->get_parent_instance() :
22463 $this;
22464
22465 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
22466 }
22467
22468 #endregion Download Plugin ------------------------------------------------------------------
22469
22470 /**
22471 * @author Vova Feldman (@svovaf)
22472 * @since 1.0.4
22473 *
22474 * @uses FS_Api
22475 *
22476 * @param bool $background Hints the method if it's a background updates check. If false, it means that
22477 * was initiated by the admin.
22478 * @param bool|number $plugin_id
22479 * @param bool $flush Since 1.1.7.3
22480 * @param int $expiration Since 1.2.2.7
22481 * @param bool|string $newer_than Since 2.2.1
22482 */
22483 private function check_updates(
22484 $background = false,
22485 $plugin_id = false,
22486 $flush = true,
22487 $expiration = FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION,
22488 $newer_than = false
22489 ) {
22490 $this->_logger->entrance();
22491
22492 // Check if there's a newer version for download.
22493 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than );
22494
22495 $update = null;
22496 if ( is_object( $new_version ) ) {
22497 $update = new FS_Plugin_Tag( $new_version );
22498
22499 if ( ! $background ) {
22500 $this->_admin_notices->add(
22501 sprintf(
22502 /* translators: %s: Numeric version number (e.g. '2.1.9' */
22503 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
22504 $update->version,
22505 sprintf(
22506 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22507 $this->get_account_url( 'download_latest' ),
22508 sprintf(
22509 /* translators: %s: plan name (e.g. latest "Professional" version) */
22510 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
22511 $this->get_plan_title()
22512 )
22513 )
22514 ),
22515 $this->get_text_inline( 'New', 'new' ) . '!'
22516 );
22517 }
22518 } else if ( false === $new_version && ! $background ) {
22519 $this->_admin_notices->add(
22520 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
22521 $this->get_text_inline( 'You are all good!', 'you-are-good' )
22522 );
22523 }
22524
22525 $this->_store_update( $update, true, $plugin_id );
22526 }
22527
22528 /**
22529 * @author Vova Feldman (@svovaf)
22530 * @since 1.0.4
22531 *
22532 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
22533 *
22534 * @return FS_Plugin[]
22535 *
22536 * @uses FS_Api
22537 */
22538 private function sync_addons( $flush = false ) {
22539 $this->_logger->entrance();
22540
22541 $api = $this->get_api_site_or_plugin_scope();
22542
22543 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
22544
22545 /**
22546 * @since 1.2.1
22547 *
22548 * If there's a cached version of the add-ons and not asking
22549 * for a flush, just use the currently stored add-ons.
22550 */
22551 if ( ! $flush && $api->is_cached( $path ) ) {
22552 $addons = self::get_all_addons();
22553
22554 return isset( $addons[ $this->_plugin->id ] ) ?
22555 $addons[ $this->_plugin->id ] :
22556 array();
22557 }
22558
22559 $result = $api->get( $path, $flush );
22560
22561 $addons = array();
22562 if ( $this->is_api_result_object( $result, 'plugins' ) &&
22563 is_array( $result->plugins )
22564 ) {
22565 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
22566 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
22567 }
22568
22569 $this->_store_addons( $addons, true );
22570 }
22571
22572 return $addons;
22573 }
22574
22575 /**
22576 * Handle user email update.
22577 *
22578 * @author Vova Feldman (@svovaf)
22579 * @since 1.0.3
22580 * @uses FS_Api
22581 *
22582 * @param string $new_email
22583 *
22584 * @return object
22585 */
22586 private function update_email( $new_email ) {
22587 $this->_logger->entrance();
22588
22589 $api = $this->get_api_user_scope();
22590 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
22591 'email' => $new_email,
22592 'after_email_confirm_url' => $this->_get_admin_page_url(
22593 'account',
22594 array( 'fs_action' => 'sync_user' )
22595 ),
22596 ) );
22597
22598 if ( ! isset( $user->error ) ) {
22599 $this->_user->email = $user->email;
22600 $this->_user->is_verified = $user->is_verified;
22601 $this->_store_user();
22602 } else {
22603 // handle different error cases.
22604 }
22605
22606 return $user;
22607 }
22608
22609 #----------------------------------------------------------------------------------
22610 #region API Error Handling
22611 #----------------------------------------------------------------------------------
22612
22613 /**
22614 * @author Vova Feldman (@svovaf)
22615 * @since 1.1.1
22616 *
22617 * @param mixed $result
22618 *
22619 * @return bool Is API result contains an error.
22620 */
22621 private function is_api_error( $result ) {
22622 return FS_Api::is_api_error( $result );
22623 }
22624
22625 /**
22626 * Checks if given API result is a non-empty and not an error object.
22627 *
22628 * @author Vova Feldman (@svovaf)
22629 * @since 1.2.1.5
22630 *
22631 * @param mixed $result
22632 * @param string|null $required_property Optional property we want to verify that is set.
22633 *
22634 * @return bool
22635 */
22636 function is_api_result_object( $result, $required_property = null ) {
22637 return FS_Api::is_api_result_object( $result, $required_property );
22638 }
22639
22640 /**
22641 * Checks if given API result is a non-empty entity object with non-empty ID.
22642 *
22643 * @author Vova Feldman (@svovaf)
22644 * @since 1.2.1.5
22645 *
22646 * @param mixed $result
22647 *
22648 * @return bool
22649 */
22650 private function is_api_result_entity( $result ) {
22651 return FS_Api::is_api_result_entity( $result );
22652 }
22653
22654 #endregion
22655
22656 /**
22657 * Make sure a given argument is an array of a specific type.
22658 *
22659 * @author Vova Feldman (@svovaf)
22660 * @since 1.2.1.5
22661 *
22662 * @param mixed $array
22663 * @param string $class
22664 *
22665 * @return bool
22666 */
22667 private function is_array_instanceof( $array, $class ) {
22668 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
22669 }
22670
22671 /**
22672 * Start install ownership change.
22673 *
22674 * @author Vova Feldman (@svovaf)
22675 * @since 1.1.1
22676 * @uses FS_Api
22677 *
22678 * @param string $candidate_email
22679 * @param string $transfer_type
22680 *
22681 * @return bool Is ownership change successfully initiated.
22682 */
22683 private function init_change_owner( $candidate_email, $transfer_type ) {
22684 $this->_logger->entrance();
22685
22686 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
22687 $install_ids = array();
22688
22689 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
22690 $install = $install_info['install'];
22691
22692 if ( $this->_user->id != $install->user_id ) {
22693 // Skip add-on installs that are not owned by the parent product's install's owner.
22694 continue;
22695 }
22696
22697 $install_ids[ $slug ] = $install->id;
22698 }
22699
22700 $api = $this->get_api_site_scope();
22701 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
22702 'email' => $candidate_email,
22703 'transfer_type' => $transfer_type,
22704 'install_ids' => implode( ',', array_values( $install_ids ) ),
22705 'after_confirm_url' => $this->_get_admin_page_url(
22706 'account',
22707 array( 'fs_action' => 'change_owner' )
22708 ),
22709 ) );
22710
22711 return ! $this->is_api_error( $result );
22712 }
22713
22714 /**
22715 * Handle install ownership change.
22716 *
22717 * @author Vova Feldman (@svovaf)
22718 * @since 1.1.1
22719 * @uses FS_Api
22720 *
22721 * @return bool Was ownership change successfully complete.
22722 */
22723 private function complete_change_owner() {
22724 $this->_logger->entrance();
22725
22726 $install_ids = fs_request_get( 'install_ids' );
22727
22728 if ( ! empty( $install_ids ) ) {
22729 $install_ids = explode( ',', $install_ids );
22730
22731 foreach ( $install_ids as $key => $install_id ) {
22732 if ( ! FS_Site::is_valid_id( $install_id ) ) {
22733 unset( $install_ids[ $key ] );
22734 }
22735 }
22736 }
22737
22738 if ( ! is_array( $install_ids ) ) {
22739 $install_ids = array();
22740 }
22741
22742 $user = new FS_User();
22743 $user->id = fs_request_get( 'user_id' );
22744 $user->public_key = fs_request_get_raw( 'user_public_key' );
22745 $user->secret_key = fs_request_get_raw( 'user_secret_key' );
22746
22747 $prev_user = $this->_user;
22748 $this->_user = $user;
22749
22750 $result = $this->get_api_user_scope( true )->get(
22751 "/installs.json?install_ids=" . implode( ',', $install_ids )
22752 );
22753
22754 $current_blog_sites = self::get_all_sites( $this->get_module_type() );
22755
22756 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22757 $site_id_slug_map = array();
22758
22759 foreach ( $current_blog_sites as $slug => $site ) {
22760 $site_id_slug_map[ $site->id ] = $slug;
22761 }
22762
22763 foreach ( $result->installs as $install ) {
22764 $site = new FS_Site( $install );
22765
22766 if ( ! isset( $site_id_slug_map[ $install->id ] ) ) {
22767 continue;
22768 }
22769
22770 $current_blog_sites[ $site_id_slug_map[ $install->id ] ] = clone $site;
22771
22772 if ( $this->_site->id == $site->id ) {
22773 $this->_site = $site;
22774 }
22775 }
22776 }
22777
22778 // Validate install's user and given user.
22779 if ( $user->id != $this->_site->user_id ) {
22780 $this->_user = $prev_user;
22781
22782 return false;
22783 }
22784
22785 $this->set_account_option( 'sites', $current_blog_sites, true );
22786
22787 // Fetch new user information.
22788 $user_result = $this->get_api_user_scope( true )->get();
22789 $user = new FS_User( $user_result );
22790 $this->_user = $user;
22791
22792 $this->_set_account( $user, $this->_site );
22793
22794 $remove_user = true;
22795 $all_modules_sites = self::get_all_modules_sites();
22796
22797 foreach ( $all_modules_sites as $sites_by_module_type ) {
22798 foreach ( $sites_by_module_type as $sites_by_slug ) {
22799 foreach ( $sites_by_slug as $site ) {
22800 if ( $prev_user->id == $site->user_id ) {
22801 $remove_user = false;
22802 break;
22803 }
22804 }
22805
22806 if ( ! $remove_user ) {
22807 break;
22808 }
22809 }
22810
22811 if ( ! $remove_user ) {
22812 break;
22813 }
22814 }
22815
22816 if ( $remove_user ) {
22817 $users = self::get_all_users();
22818
22819 if ( isset( $users[ $prev_user->id ] ) ) {
22820 unset( $users[ $prev_user->id ] );
22821 } else {
22822 // If the prev user wasn't found by the key, iterate over the users collection.
22823 foreach ( $users as $key => $user ) {
22824 if ( $user->id == $prev_user->id ) {
22825 unset( $users[ $key ] );
22826 break;
22827 }
22828 }
22829 }
22830
22831 $this->set_account_option( 'users', $users, true );
22832 }
22833
22834 return true;
22835 }
22836
22837 /**
22838 * Completes ownership change by license.
22839 *
22840 * @author Leo Fajardo (@leorw)
22841 * @since 2.3.2
22842 *
22843 * @param number $user_id
22844 * @param array[string]number $install_ids_by_slug_map
22845 *
22846 */
22847 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
22848 $this->_logger->entrance();
22849
22850 $this->sync_user_by_current_install( $user_id );
22851
22852 $result = $this->get_api_user_scope( true )->get(
22853 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
22854 );
22855
22856 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22857 $sites = self::get_all_sites( $this->get_module_type() );
22858 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
22859
22860 foreach ( $result->installs as $install ) {
22861 $site = new FS_Site( $install );
22862
22863 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
22864 }
22865
22866 $this->set_account_option( 'sites', $sites, true );
22867 }
22868 }
22869
22870 /**
22871 * Handle user name update.
22872 *
22873 * @author Vova Feldman (@svovaf)
22874 * @since 1.0.9
22875 * @uses FS_Api
22876 *
22877 * @return object
22878 */
22879 private function update_user_name() {
22880 $this->_logger->entrance();
22881 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
22882
22883 $api = $this->get_api_user_scope();
22884 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
22885 'name' => $name,
22886 ) );
22887
22888 if ( ! isset( $user->error ) ) {
22889 $this->_user->first = $user->first;
22890 $this->_user->last = $user->last;
22891 $this->_store_user();
22892 } else {
22893 // handle different error cases.
22894
22895 }
22896
22897 return $user;
22898 }
22899
22900 /**
22901 * Verify user email.
22902 *
22903 * @author Vova Feldman (@svovaf)
22904 * @since 1.0.3
22905 * @uses FS_Api
22906 */
22907 private function verify_email() {
22908 $this->_handle_account_user_sync();
22909
22910 if ( $this->_user->is_verified() ) {
22911 return;
22912 }
22913
22914 $api = $this->get_api_site_scope();
22915 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
22916 'after_email_confirm_url' => $this->_get_admin_page_url(
22917 'account',
22918 array( 'fs_action' => 'sync_user' )
22919 )
22920 ) );
22921
22922 if ( ! isset( $result->error ) ) {
22923 $this->_admin_notices->add( sprintf(
22924 $this->get_text_inline( 'Verification mail was just sent to %s. If you can\'t find it after 5 min, please check your spam box.', 'verification-email-sent-message' ),
22925 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
22926 ) );
22927 } else {
22928 // handle different error cases.
22929
22930 }
22931 }
22932
22933 /**
22934 * @author Vova Feldman (@svovaf)
22935 * @since 1.1.2
22936 *
22937 * @param array $params
22938 * @param bool|null $network
22939 *
22940 * @return string
22941 */
22942 function get_activation_url( $params = array(), $network = null ) {
22943 if ( $this->is_addon() && $this->has_free_plan() ) {
22944 /**
22945 * @author Vova Feldman (@svovaf)
22946 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
22947 */
22948 return $this->get_parent_instance()->get_activation_url( $params );
22949 }
22950
22951 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
22952 }
22953
22954 /**
22955 * @author Vova Feldman (@svovaf)
22956 * @since 1.2.1.5
22957 *
22958 * @param array $params
22959 *
22960 * @return string
22961 */
22962 function get_reconnect_url( $params = array() ) {
22963 $params['fs_action'] = 'reset_anonymous_mode';
22964 $params['fs_unique_affix'] = $this->get_unique_affix();
22965
22966 return $this->get_activation_url( $params );
22967 }
22968
22969 /**
22970 * Get the URL of the page that should be loaded after the user connect
22971 * or skip in the opt-in screen.
22972 *
22973 * @author Vova Feldman (@svovaf)
22974 * @since 1.1.3
22975 *
22976 * @param string $filter Filter name.
22977 * @param array $params Since 1.2.2.7
22978 * @param bool|null $network
22979 *
22980 * @return string
22981 */
22982 function get_after_activation_url( $filter, $params = array(), $network = null ) {
22983 if ( $this->show_opt_in_on_themes_page() &&
22984 ( fs_request_has( 'pending_activation' ) ||
22985 // For cases when the first time path is set, even though it's a WP.org theme.
22986 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
22987 ) {
22988 $first_time_path = '';
22989 } else {
22990 $first_time_path = $this->_menu->get_first_time_path(
22991 fs_is_network_admin() && $this->_is_network_active
22992 );
22993 }
22994
22995 if ( $this->_is_network_active &&
22996 fs_is_network_admin() &&
22997 ! $this->_menu->has_network_menu() &&
22998 $this->is_network_registered()
22999 ) {
23000 $target_url = $this->get_account_url();
23001 } else {
23002 // Default plugin's page.
23003 $target_url = $this->_get_admin_page_url( '', array(), $network );
23004 }
23005
23006 return add_query_arg( $params, $this->apply_filters(
23007 $filter,
23008 empty( $first_time_path ) ?
23009 $target_url :
23010 $first_time_path
23011 ) );
23012 }
23013
23014 /**
23015 * Handle account page updates / edits / actions.
23016 *
23017 * @author Vova Feldman (@svovaf)
23018 * @since 1.0.2
23019 *
23020 */
23021 private function _handle_account_edits() {
23022 if ( ! $this->is_user_admin() ) {
23023 return;
23024 }
23025
23026 $action = fs_get_action();
23027
23028 if ( empty( $action ) ) {
23029 return;
23030 }
23031
23032 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
23033 $install_id = fs_request_get( 'install_id', '' );
23034
23035 // Alias.
23036 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
23037
23038 $is_network_action = $this->is_network_level_action();
23039 $blog_id = $this->is_network_level_site_specific_action();
23040 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
23041
23042 if ( is_numeric( $blog_id ) ) {
23043 $this->switch_to_blog( $blog_id );
23044 } else {
23045 $blog_id = '';
23046 }
23047
23048 switch ( $action ) {
23049 case 'opt_in':
23050 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23051
23052 if ( $is_parent_plugin_action ) {
23053 if ( $is_network_action && ! empty( $blog_id ) ) {
23054 if ( ! $this->is_registered() ) {
23055 $this->install_with_user(
23056 $this->get_network_user(),
23057 false,
23058 false,
23059 false,
23060 false
23061 );
23062
23063 $this->_admin_notices->add(
23064 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
23065 $this->get_text_inline( 'Awesome', 'awesome' )
23066 );
23067 }
23068 }
23069 }
23070 break;
23071
23072 case 'toggle_tracking':
23073 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23074
23075 if ( $is_parent_plugin_action ) {
23076 if ( $is_network_action && ! empty( $blog_id ) ) {
23077 if ( $this->is_registered( true ) ) {
23078 if ( $this->is_tracking_prohibited( $blog_id ) ) {
23079 if ( $this->toggle_site_tracking( true, $blog_id ) ) {
23080 $this->_admin_notices->add(
23081 sprintf( $this->get_text_inline( 'Sharing diagnostic data with %s helps to provide functionality that\'s more relevant to your website, avoid WordPress or PHP version incompatibilities that can break your website, and recognize which languages & regions the plugin should be translated and tailored to.', 'opt-out-message-appreciation' ), "<b>{$this->get_plugin_title()}</b>" ),
23082 $this->get_text_inline( 'Thank you!', 'thank-you' )
23083 );
23084 }
23085 } else {
23086 if ( $this->toggle_site_tracking( false, $blog_id ) ) {
23087 $install = $this->get_install_by_blog_id( $blog_id );
23088
23089 $this->_admin_notices->add(
23090 sprintf(
23091 $this->get_text_inline( 'Diagnostic data will no longer be sent from %s to %s.', 'opted-out-successfully' ),
23092 self::get_unfiltered_site_url( $blog_id, true ),
23093 "<b>{$this->get_plugin_title()}</b>"
23094 )
23095 );
23096 }
23097 }
23098 }
23099 }
23100 }
23101
23102 break;
23103
23104 case 'delete_account':
23105 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23106
23107 $is_network_deletion = $is_network_action && empty( $blog_id );
23108
23109 if ( $is_parent_plugin_action ) {
23110 // Delete add-on installs if have any.
23111 $installed_addons = $this->get_installed_addons();
23112 foreach ( $installed_addons as $fs_addon ) {
23113 if ( $is_network_deletion ) {
23114 $fs_addon->delete_network_account_event();
23115 } else {
23116 $fs_addon->delete_account_event();
23117 }
23118 }
23119
23120 if ( $is_network_deletion ) {
23121 $this->delete_network_account_event();
23122 } else {
23123 $this->delete_account_event();
23124 }
23125
23126 // Clear user and site.
23127 $this->_site = null;
23128 $this->_user = null;
23129
23130 $this->maybe_set_slug_and_network_menu_exists_flag();
23131
23132 fs_redirect( $this->get_activation_url() );
23133 } else {
23134 if ( $this->is_addon_activated( $plugin_id ) ) {
23135 $fs_addon = self::get_instance_by_id( $plugin_id );
23136
23137 if ( $is_network_deletion ) {
23138 $fs_addon->delete_network_account_event();
23139 } else {
23140 $fs_addon->delete_account_event();
23141 }
23142
23143 fs_redirect( $this->_get_admin_page_url( 'account' ) );
23144 }
23145 }
23146
23147 return;
23148
23149 case 'downgrade_account':
23150 if ( is_numeric( $blog_id ) ) {
23151 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23152 } else {
23153 check_admin_referer( $action );
23154 }
23155
23156 $switch_to_network_install_blog_after_cancellation = (
23157 is_numeric( $blog_id ) &&
23158 $plugin_id == $this->get_id() &&
23159 ! $this->is_trial()
23160 );
23161
23162 $result = $this->cancel_subscription_or_trial( $plugin_id );
23163 if ( $this->is_api_error( $result ) ) {
23164 $this->_admin_notices->add(
23165 $result->error->message,
23166 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23167 'error'
23168 );
23169 }
23170
23171 if ( $switch_to_network_install_blog_after_cancellation ) {
23172 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23173 }
23174
23175 return;
23176
23177 case 'activate_license':
23178 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23179
23180 $fs = $this;
23181 if ( $plugin_id != $this->get_id() ) {
23182 $fs = $this->is_addon_activated( $plugin_id ) ?
23183 self::get_instance_by_id( $plugin_id ) :
23184 null;
23185 }
23186
23187 if ( is_object( $fs ) ) {
23188 $fs->_activate_license();
23189
23190 /**
23191 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
23192 *
23193 * @author Leo Fajardo (@leorw)
23194 * @since 2.4.0
23195 */
23196 unset( $_REQUEST['plugin_id'] );
23197
23198 if ( $this->is_bundle_license_auto_activation_enabled() ) {
23199 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
23200 }
23201 }
23202
23203 return;
23204
23205 case 'deactivate_license':
23206 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23207
23208 if ( $plugin_id == $this->get_id() ) {
23209 $this->_deactivate_license();
23210
23211 if ( $this->is_only_premium() ) {
23212 // Clear user and site.
23213 $this->_site = null;
23214 $this->_user = null;
23215
23216 if ( ! $is_network_action ) {
23217 fs_redirect( $this->get_activation_url() );
23218 } else if ( is_numeric( $blog_id ) ) {
23219 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23220 }
23221 }
23222 } else {
23223 if ( $this->is_addon_activated( $plugin_id ) ) {
23224 $fs_addon = self::get_instance_by_id( $plugin_id );
23225 $fs_addon->_deactivate_license();
23226 }
23227 }
23228
23229 return;
23230
23231 case 'check_updates':
23232 check_admin_referer( $action );
23233 $this->check_updates();
23234
23235 return;
23236
23237 case 'change_owner':
23238 $state = fs_request_get( 'state', 'init' );
23239 switch ( $state ) {
23240 case 'init':
23241 // The nonce is injected by the error handler in `_email_address_update_ajax_handler` function.
23242 check_admin_referer( 'change_owner' );
23243
23244 $candidate_email = fs_request_get( 'candidate_email' );
23245 $transfer_type = fs_request_get( 'transfer_type' );
23246
23247 if ( $this->init_change_owner( $candidate_email, $transfer_type ) ) {
23248 if ( 'transfer' === $transfer_type ) {
23249 $this->_admin_notices->add( sprintf( $this->get_text_inline( 'A confirmation email was just sent to %s. The email owner must confirm the update within the next 4 hours.', 'change-owner-request-sent-x-transfer' ), '<b>' . $this->_user->email . '</b>' ) );
23250 } else {
23251 $this->_admin_notices->add( sprintf( $this->get_text_inline( 'A confirmation email was just sent to %s. You must confirm the update within the next 4 hours. If you cannot find the email, please check your spam folder.', 'change-owner-request-sent-x' ), '<b>' . $this->_user->email . '</b>' ) );
23252 }
23253 }
23254 break;
23255 case 'owner_confirmed':
23256 // We cannot (or need not to) check the nonce and referer here, because the link comes from the email sent by our API.
23257 $candidate_email = fs_request_get( 'candidate_email', '' );
23258
23259 if ( ! is_email($candidate_email ) ) {
23260 return;
23261 }
23262
23263 $this->_admin_notices->add( sprintf( $this->get_text_inline( 'Thanks for confirming the ownership change. An email was just sent to %s for final approval.', 'change-owner-request_owner-confirmed' ), '<b>' . $candidate_email . '</b>' ) );
23264 break;
23265 case 'candidate_confirmed':
23266 // We do not need to validate the authenticity of this request here, because the `complete_change_owner` does that for us through API calls.
23267 if ( $this->complete_change_owner() ) {
23268 $this->_admin_notices->add_sticky(
23269 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
23270 'ownership_changed',
23271 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
23272 );
23273 } else {
23274 // @todo Handle failed ownership change message.
23275 }
23276 break;
23277 }
23278
23279 return;
23280
23281 case 'update_user_name':
23282 check_admin_referer( 'update_user_name' );
23283
23284 $result = $this->update_user_name();
23285
23286 if ( isset( $result->error ) ) {
23287 $this->_admin_notices->add(
23288 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
23289 $oops_text,
23290 'error'
23291 );
23292 } else {
23293 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
23294 }
23295
23296 return;
23297
23298 #region Actions that might be called from external links (e.g. email)
23299
23300 /**
23301 * !!IMPORTANT!!: We cannot check for a valid nonce in this region, because the links could be coming from emails.
23302 */
23303
23304 case 'cancel_trial':
23305 $result = $this->cancel_subscription_or_trial( $plugin_id );
23306 if ( $this->is_api_error( $result ) ) {
23307 $this->_admin_notices->add(
23308 $result->error->message,
23309 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23310 'error'
23311 );
23312 }
23313
23314 return;
23315
23316 case 'verify_email':
23317 $this->verify_email();
23318
23319 return;
23320
23321 case 'sync_user':
23322 $this->_handle_account_user_sync();
23323
23324 return;
23325
23326 case $this->get_unique_affix() . '_sync_license':
23327 $this->_sync_license();
23328
23329 return;
23330
23331 case 'download_latest':
23332 $this->download_latest_directly( $plugin_id );
23333
23334 return;
23335
23336 #endregion
23337 }
23338
23339 if ( WP_FS__IS_POST_REQUEST ) {
23340 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
23341 foreach ( $properties as $p ) {
23342 if ( 'update_' . $p === $action ) {
23343 check_admin_referer( $action );
23344
23345 $this->_logger->log( $action );
23346
23347 $site_property = substr( $p, strlen( 'site_' ) );
23348 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
23349 $this->get_site()->{$site_property} = $site_property_value;
23350
23351 // Store account after modification.
23352 $this->_store_site();
23353
23354 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
23355
23356 $this->_admin_notices->add( sprintf(
23357 /* translators: %s: User's account property (e.g. email address, name) */
23358 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
23359 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
23360 ) );
23361
23362 return;
23363 }
23364 }
23365 }
23366 }
23367
23368 /**
23369 * Adds CSS classes for the body tag in the admin.
23370 *
23371 * @param string $classes Space-separated string of class names.
23372 *
23373 * @return string $classes FS Admin body tag class names.
23374 */
23375 public function fs_addons_body_class( $classes ) {
23376 $classes .= ' plugins-php';
23377 return $classes;
23378 }
23379
23380 /**
23381 * Account page resources load.
23382 *
23383 * @author Vova Feldman (@svovaf)
23384 * @since 1.0.6
23385 */
23386 function _account_page_load() {
23387 $this->_logger->entrance();
23388
23389 $this->_logger->info( var_export( $_REQUEST, true ) );
23390
23391 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
23392
23393 if ( $this->has_addons() ) {
23394 wp_enqueue_script( 'plugin-install' );
23395 add_thickbox();
23396 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23397 }
23398
23399 if ( $this->has_paid_plan() &&
23400 ! $this->has_any_license() &&
23401 ! $this->is_sync_executed() &&
23402 $this->is_tracking_allowed()
23403 ) {
23404 /**
23405 * If no licenses found and no sync job was executed during the last 24 hours,
23406 * just execute the sync job right away (blocking execution).
23407 *
23408 * @since 1.1.7.3
23409 */
23410 $this->run_manual_sync();
23411 }
23412
23413 $this->_handle_account_edits();
23414
23415 if (
23416 is_object( $this->_license ) &&
23417 $this->_license->user_id == $this->_user->id &&
23418 ! $this->is_whitelabeled( true )
23419 ) {
23420 $this->_admin_notices->add(
23421 sprintf(
23422 $this->get_text_inline( "Is this your client's site? %s if you wish to hide sensitive info like your email, license key, prices, billing address & invoices from the WP Admin.", 'license_not_whitelabeled' ),
23423 sprintf(
23424 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
23425 $this->get_text_inline( 'Click here', 'click-here' )
23426 )
23427 ),
23428 '',
23429 'success',
23430 false,
23431 'license_not_whitelabeled'
23432 );
23433 }
23434
23435 $this->do_action( 'account_page_load_before_departure' );
23436 }
23437
23438 /**
23439 * Renders the "Affiliation" page.
23440 *
23441 * @author Leo Fajardo (@leorw)
23442 * @since 1.2.3
23443 */
23444 function _affiliation_page_render() {
23445 $this->_logger->entrance();
23446
23447 $this->fetch_affiliate_and_terms();
23448
23449 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
23450
23451 $is_bundle_context = $this->has_bundle_context();
23452
23453 $plugin_title = $this->get_plugin_title();
23454
23455 if ( $is_bundle_context ) {
23456 $plugin_title = $this->plugin_affiliate_terms->plugin_title;
23457
23458 // Add the suffix "Bundle" only if the word is not present in the title itself.
23459 if ( false === mb_stripos( $plugin_title, fs_text_inline( 'Bundle', 'bundle' ) ) ) {
23460 $plugin_title = $this->apply_filters(
23461 'formatted_bundle_title',
23462 $plugin_title . ' ' . fs_text_inline( 'Bundle', 'bundle' )
23463 );
23464 }
23465 }
23466
23467 $vars = array(
23468 'id' => $this->_module_id,
23469 'plugin_title' => $plugin_title,
23470 );
23471 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
23472 }
23473
23474
23475 /**
23476 * Render account page.
23477 *
23478 * @author Vova Feldman (@svovaf)
23479 * @since 1.0.0
23480 */
23481 function _account_page_render() {
23482 $this->_logger->entrance();
23483
23484 $template = 'account.php';
23485 $vars = array( 'id' => $this->_module_id );
23486
23487 /**
23488 * Added filter to the template to allow developers wrapping the template
23489 * in custom HTML (e.g. within a wizard/tabs).
23490 *
23491 * @author Vova Feldman (@svovaf)
23492 * @since 1.2.1.6
23493 */
23494 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
23495 }
23496
23497 /**
23498 * Render account connect page.
23499 *
23500 * @author Vova Feldman (@svovaf)
23501 * @since 1.0.7
23502 */
23503 function _connect_page_render() {
23504 $this->_logger->entrance();
23505
23506 $vars = array( 'id' => $this->_module_id );
23507
23508 /**
23509 * Added filter to the template to allow developers wrapping the template
23510 * in custom HTML (e.g. within a wizard/tabs).
23511 *
23512 * @author Vova Feldman (@svovaf)
23513 * @since 1.2.1.6
23514 */
23515 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
23516 }
23517
23518 /**
23519 * Load required resources before add-ons page render.
23520 *
23521 * @author Vova Feldman (@svovaf)
23522 * @since 1.0.6
23523 */
23524 function _addons_page_load() {
23525 $this->_logger->entrance();
23526
23527 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
23528
23529 wp_enqueue_script( 'plugin-install' );
23530 add_thickbox();
23531 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23532
23533 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
23534 $this->_admin_notices->add(
23535 sprintf( $this->get_text_inline( 'Just letting you know that the add-ons information of %s is being pulled from an external server.', 'addons-info-external-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
23536 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
23537 'update-nag'
23538 );
23539 }
23540 }
23541
23542 /**
23543 * Render add-ons page.
23544 *
23545 * @author Vova Feldman (@svovaf)
23546 * @since 1.0.6
23547 */
23548 function _addons_page_render() {
23549 $this->_logger->entrance();
23550
23551 $vars = array( 'id' => $this->_module_id );
23552
23553 /**
23554 * Added filter to the template to allow developers wrapping the template
23555 * in custom HTML (e.g. within a wizard/tabs).
23556 *
23557 * @author Vova Feldman (@svovaf)
23558 * @since 1.2.1.6
23559 */
23560 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
23561 }
23562
23563 /* Pricing & Upgrade
23564 ------------------------------------------------------------------------------------------------------------------*/
23565 /**
23566 * Render pricing page.
23567 *
23568 * @author Vova Feldman (@svovaf)
23569 * @since 1.0.0
23570 */
23571 function _pricing_page_render() {
23572 $this->_logger->entrance();
23573
23574 $vars = array( 'id' => $this->_module_id );
23575
23576 if ( 'true' === fs_request_get( 'checkout', false ) ) {
23577 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
23578 } else {
23579 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
23580 }
23581 }
23582
23583 /**
23584 * @author Leo Fajardo (@leorw)
23585 * @since 2.3.1
23586 */
23587 function _maybe_add_pricing_ajax_handler() {
23588 if ( ! $this->should_use_external_pricing() ) {
23589 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
23590 }
23591 }
23592
23593 /**
23594 * @author Leo Fajardo (@leorw)
23595 * @since 2.3.1
23596 */
23597 function _fs_pricing_ajax_action_handler() {
23598 $this->check_ajax_referer( 'pricing_ajax_action' );
23599
23600 $result = null;
23601 $pricing_action = fs_request_get( 'pricing_action' );
23602
23603 switch ( $pricing_action ) {
23604 case 'fetch_pricing_data':
23605 $params = array(
23606 'is_enriched' => true,
23607 'trial' => fs_request_get_bool( 'trial' ),
23608 'sandbox' => fs_request_get_raw( 'sandbox' ),
23609 's_ctx_type' => fs_request_get_raw( 's_ctx_type' ),
23610 's_ctx_id' => fs_request_get_raw( 's_ctx_id' ),
23611 's_ctx_ts' => fs_request_get_raw( 's_ctx_ts' ),
23612 's_ctx_secure' => fs_request_get_raw( 's_ctx_secure' ),
23613 );
23614
23615 $bundle_id = $this->get_bundle_id();
23616 $bundle_public_key = $this->get_bundle_public_key();
23617
23618 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
23619
23620 if ( ! $has_bundle_context ) {
23621 $api = $this->get_api_plugin_scope();
23622 } else {
23623 $api = FS_Api::instance(
23624 $bundle_id,
23625 'plugin',
23626 $bundle_id,
23627 $bundle_public_key,
23628 ! $this->is_live(),
23629 false,
23630 $this->get_sdk_version()
23631 );
23632
23633 $params['plugin_id'] = $this->get_id();
23634 $params['plugin_public_key'] = $this->get_public_key();
23635 }
23636
23637 $result = $api->get( 'pricing.json?' . http_build_query( $params ) );
23638 break;
23639 case 'start_trial':
23640 $result = $this->opt_in(
23641 false,
23642 false,
23643 false,
23644 false,
23645 false,
23646 fs_request_get( 'plan_id' )
23647 );
23648 }
23649
23650 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
23651 $this->_logger->api_error( $result );
23652
23653 self::shoot_ajax_failure(
23654 isset( $result->error ) ?
23655 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
23656 var_export( $result, true )
23657 );
23658 }
23659
23660 $this->shoot_ajax_success( $result );
23661 }
23662
23663 #----------------------------------------------------------------------------------
23664 #region Contact Us
23665 #----------------------------------------------------------------------------------
23666
23667 /**
23668 * Render contact-us page.
23669 *
23670 * @author Vova Feldman (@svovaf)
23671 * @since 1.0.3
23672 */
23673 function _contact_page_render() {
23674 $this->_logger->entrance();
23675
23676 $vars = array( 'id' => $this->_module_id );
23677
23678 /**
23679 * Added filter to the template to allow developers wrapping the template
23680 * in custom HTML (e.g. within a wizard/tabs).
23681 *
23682 * @author Vova Feldman (@svovaf)
23683 * @since 2.1.3
23684 */
23685 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
23686 }
23687
23688 #endregion ------------------------------------------------------------------------
23689
23690 /**
23691 * Hide all admin notices to prevent distractions.
23692 *
23693 * @author Vova Feldman (@svovaf)
23694 * @since 1.0.3
23695 *
23696 * @uses remove_all_actions()
23697 */
23698 private static function _hide_admin_notices() {
23699 remove_all_actions( 'admin_notices' );
23700 remove_all_actions( 'network_admin_notices' );
23701 remove_all_actions( 'all_admin_notices' );
23702 remove_all_actions( 'user_admin_notices' );
23703 }
23704
23705 static function _clean_admin_content_section_hook() {
23706 $hide_admin_notices = true;
23707
23708 if ( fs_request_is_action( 'allow_clone_resolution_notice' ) ) {
23709 check_admin_referer( 'fs_allow_clone_resolution_notice' );
23710
23711 $hide_admin_notices = false;
23712 }
23713
23714 if ( $hide_admin_notices ) {
23715 self::_hide_admin_notices();
23716 }
23717
23718 // Hide footer.
23719 echo '<style>#wpfooter { display: none !important; }</style>';
23720 }
23721
23722 /**
23723 * Attach to admin_head hook to hide all admin notices.
23724 *
23725 * @author Vova Feldman (@svovaf)
23726 * @since 1.0.3
23727 */
23728 static function _clean_admin_content_section() {
23729 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
23730 }
23731
23732 /* CSS & JavaScript
23733 ------------------------------------------------------------------------------------------------------------------*/
23734 /* function _enqueue_script($handle, $src) {
23735 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
23736
23737 $this->_logger->entrance( 'script = ' . $url );
23738
23739 wp_enqueue_script( $handle, $url );
23740 }*/
23741
23742 /* SDK
23743 ------------------------------------------------------------------------------------------------------------------*/
23744 private $_user_api;
23745
23746 /**
23747 *
23748 * @author Vova Feldman (@svovaf)
23749 * @since 1.0.2
23750 *
23751 * @param bool $flush
23752 *
23753 * @return FS_Api
23754 */
23755 function get_api_user_scope( $flush = false ) {
23756 if ( ! isset( $this->_user_api ) || $flush ) {
23757 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
23758 }
23759
23760 return $this->_user_api;
23761 }
23762
23763 /**
23764 * @author Vova Feldman (@svovaf)
23765 * @since 2.0.0
23766 *
23767 * @param \FS_User $user
23768 *
23769 * @return \FS_Api
23770 */
23771 private function get_api_user_scope_by_user( FS_User $user ) {
23772 return FS_Api::instance(
23773 $this->_module_id,
23774 'user',
23775 $user->id,
23776 $user->public_key,
23777 ! $this->is_live(),
23778 $user->secret_key,
23779 $this->get_sdk_version()
23780 );
23781 }
23782
23783 /**
23784 *
23785 * @author Leo Fajardo (@leorw)
23786 * @since 2.0.0
23787 *
23788 * @param bool $flush
23789 *
23790 * @return FS_Api
23791 */
23792 private function get_current_or_network_user_api_scope( $flush = false ) {
23793 if ( ! $this->_is_network_active ||
23794 ( isset( $this->_user ) && $this->_user instanceof FS_User )
23795 ) {
23796 return $this->get_api_user_scope( $flush );
23797 }
23798
23799 $user = $this->get_current_or_network_user();
23800
23801 $this->_user_api = FS_Api::instance(
23802 $this->_module_id,
23803 'user',
23804 $user->id,
23805 $user->public_key,
23806 ! $this->is_live(),
23807 $user->secret_key,
23808 $this->get_sdk_version()
23809 );
23810
23811 return $this->_user_api;
23812 }
23813
23814 private $_site_api;
23815
23816 /**
23817 *
23818 * @author Vova Feldman (@svovaf)
23819 * @since 1.0.2
23820 *
23821 * @param bool $flush
23822 *
23823 * @return FS_Api
23824 */
23825 private function get_api_site_scope( $flush = false ) {
23826 if ( ! isset( $this->_site_api ) || $flush ) {
23827 $this->_site_api = FS_Api::instance(
23828 $this->_module_id,
23829 'install',
23830 $this->_site->id,
23831 $this->_site->public_key,
23832 ! $this->is_live(),
23833 $this->_site->secret_key,
23834 $this->get_sdk_version(),
23835 self::get_unfiltered_site_url()
23836 );
23837 }
23838
23839 return $this->_site_api;
23840 }
23841
23842 /**
23843 * @author Leo Fajardo (@leorw)
23844 * @since 2.5.0
23845 *
23846 * @param string $path
23847 * @param string $method
23848 * @param array $params
23849 * @param bool $flush_instance
23850 *
23851 * @return array|mixed|string|void
23852 * @throws Freemius_Exception
23853 */
23854 private function api_site_call( $path, $method = 'GET', $params = array(), $flush_instance = false ) {
23855 $result = $this->get_api_site_scope( $flush_instance )->call( $path, $method, $params );
23856
23857 /**
23858 * Checks if the local install's URL is different from the remote install's URL, update the local install if necessary, and then run the clone handler if the install's URL is different from the URL of the site.
23859 *
23860 * @author Leo Fajardo (@leorw)
23861 * @since 2.5.0
23862 */
23863 if (
23864 $this->is_registered() &&
23865 FS_Api::is_api_result_entity( $result ) &&
23866 isset( $result->url )
23867 ) {
23868 $stored_local_url = trailingslashit( $this->_site->url );
23869 $stored_remote_url = trailingslashit( $result->url );
23870
23871 if ( $stored_local_url !== $stored_remote_url ) {
23872 $this->_site->url = $result->url;
23873 $this->_store_site();
23874 }
23875
23876 if ( fs_strip_url_protocol( $stored_remote_url ) !== self::get_unfiltered_site_url( null, true, true ) ) {
23877 FS_Clone_Manager::instance()->maybe_run_clone_resolution();
23878 }
23879 }
23880
23881 return $result;
23882 }
23883
23884 private $_plugin_api;
23885
23886 /**
23887 * Get plugin public API scope.
23888 *
23889 * @author Vova Feldman (@svovaf)
23890 * @since 1.0.7
23891 *
23892 * @return FS_Api
23893 */
23894 function get_api_plugin_scope() {
23895 if ( ! isset( $this->_plugin_api ) ) {
23896 $this->_plugin_api = FS_Api::instance(
23897 $this->_module_id,
23898 'plugin',
23899 $this->_plugin->id,
23900 $this->_plugin->public_key,
23901 ! $this->is_live(),
23902 false,
23903 $this->get_sdk_version()
23904 );
23905 }
23906
23907 return $this->_plugin_api;
23908 }
23909
23910 /**
23911 * Get bundle public API scope.
23912 *
23913 * @author Vova Feldman (@svovaf)
23914 * @since 2.3.1
23915 *
23916 * @return FS_Api
23917 */
23918 function get_api_bundle_scope() {
23919 return FS_Api::instance(
23920 $this->get_bundle_id(),
23921 'plugin',
23922 $this->get_bundle_id(),
23923 $this->get_bundle_public_key(),
23924 ! $this->is_live(),
23925 false,
23926 $this->get_sdk_version()
23927 );
23928 }
23929
23930 /**
23931 * Get site API scope object (fallback to public plugin scope when not registered).
23932 *
23933 * @author Vova Feldman (@svovaf)
23934 * @since 1.0.7
23935 *
23936 * @return FS_Api
23937 */
23938 function get_api_site_or_plugin_scope() {
23939 return $this->is_registered() ?
23940 $this->get_api_site_scope() :
23941 $this->get_api_plugin_scope();
23942 }
23943
23944 /**
23945 * @author Leo Fajardo (@leorw)
23946 * @since 2.2.3.1
23947 *
23948 * @param object $result
23949 */
23950 private function maybe_modify_api_curl_error_message( $result ) {
23951 if (
23952 'cUrlMissing' !== $result->error->type &&
23953 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
23954 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
23955 ) {
23956 return;
23957 }
23958
23959 $result->error->message = $this->esc_html_inline( 'We use PHP cURL library for the API calls, which is a very common library and usually installed and activated out of the box. Unfortunately, cURL is not activated (or disabled) on your server.', 'curl-missing-message' ) .
23960 ' ' .
23961 $this->esc_html_inline(
23962 sprintf(
23963 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
23964 implode(
23965 ', ',
23966 $this->apply_filters( 'api_domains', array(
23967 'api.freemius.com',
23968 'wp.freemius.com'
23969 ) )
23970 )
23971 ),
23972 'connectivity-whitelist'
23973 ) .
23974 ' ' .
23975 sprintf(
23976 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
23977 $this->get_module_type()
23978 );
23979 }
23980
23981 /**
23982 * Show trial promotional notice (if any trial exist).
23983 *
23984 * @author Vova Feldman (@svovaf)
23985 * @since 1.0.9
23986 *
23987 * @param FS_Plugin_Plan[] $plans
23988 */
23989 function _check_for_trial_plans( $plans ) {
23990 /**
23991 * For some reason core's do_action() flattens arrays when it has a single object item. Therefore, we need to restructure the array as expected.
23992 *
23993 * @author Vova Feldman (@svovaf)
23994 * @since 2.1.2
23995 */
23996 if ( ! is_array( $plans ) && is_object( $plans ) ) {
23997 $plans = array( $plans );
23998 }
23999
24000 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
24001 $plans = array();
24002 }
24003
24004 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
24005 }
24006
24007 /**
24008 * During trial promotion the "upgrade" submenu item turns to
24009 * "start trial" to encourage the trial. Since we want to keep
24010 * the same menu item handler and there's no robust way to
24011 * add new arguments to the menu item link's querystring,
24012 * use JavaScript to find the menu item and update the href of
24013 * the link.
24014 *
24015 * @author Vova Feldman (@svovaf)
24016 * @since 1.2.1.5
24017 */
24018 function _fix_start_trial_menu_item_url() {
24019 $template_args = array( 'id' => $this->_module_id );
24020 fs_require_template( 'add-trial-to-pricing.php', $template_args );
24021 }
24022
24023 /**
24024 * Check if module is currently in a trial promotion mode.
24025 *
24026 * @author Vova Feldman (@svovaf)
24027 * @since 1.2.2.7
24028 *
24029 * @return bool
24030 */
24031 function is_in_trial_promotion() {
24032 return $this->_admin_notices->has_sticky( 'trial_promotion' );
24033 }
24034
24035 /**
24036 * Show trial promotional notice (if any trial exist).
24037 *
24038 * @author Vova Feldman (@svovaf)
24039 * @since 1.0.9
24040 *
24041 * @return bool If trial notice added.
24042 */
24043 function _add_trial_notice() {
24044 if ( ! $this->is_user_admin() ) {
24045 return false;
24046 }
24047
24048 if ( ! $this->is_user_in_admin() ) {
24049 return false;
24050 }
24051
24052 if ( $this->_is_network_active ) {
24053 if ( fs_is_network_admin() ) {
24054 // Network level trial is disabled at the moment.
24055 return false;
24056 }
24057
24058 if ( ! $this->is_delegated_connection() ) {
24059 // Only delegated sites should support trials.
24060 return false;
24061 }
24062 }
24063
24064 // Check if trial message is already shown.
24065 if ( $this->is_in_trial_promotion() ) {
24066 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
24067
24068 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
24069
24070 return false;
24071 }
24072
24073 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
24074 // Don't show trial if running the premium code, unless running in DEV mode.
24075 return false;
24076 }
24077
24078 if ( ! $this->has_trial_plan() ) {
24079 // No plans with trial.
24080 return false;
24081 }
24082
24083 if ( ! $this->apply_filters( 'show_trial', true ) ) {
24084 // Developer explicitly asked not to show the trial promo.
24085 return false;
24086 }
24087
24088 if ( $this->is_registered() ) {
24089 // Check if trial already utilized.
24090 if ( $this->_site->is_trial_utilized() ) {
24091 return false;
24092 }
24093
24094 if ( $this->is_paying_or_trial() ) {
24095 // Don't show trial if paying or already in trial.
24096 return false;
24097 }
24098 }
24099
24100 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24101 // If not yet opted-in/skipped, or pending activation, don't show trial.
24102 return false;
24103 }
24104
24105 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
24106 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
24107
24108 // Show promotion if never shown before and 24 hours after initial activation with FS.
24109 if ( ! $was_promotion_shown_before &&
24110 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
24111 ) {
24112 return false;
24113 }
24114
24115 // OR if promotion was shown before, try showing it every 30 days.
24116 if ( $was_promotion_shown_before &&
24117 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
24118 ) {
24119 return false;
24120 }
24121
24122 $trial_period = $this->_trial_days;
24123 $require_payment = $this->_is_trial_require_payment;
24124 $trial_url = $this->get_trial_url();
24125 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
24126
24127 if ( $this->is_registered() ) {
24128 // If opted-in, override trial with up to date data from API.
24129 $trial_plans = FS_Plan_Manager::instance()->get_visible_trial_plans( $this->_plans );
24130 $trial_plans_count = count( $trial_plans );
24131
24132 if ( 0 === $trial_plans_count ) {
24133 // If there's no plans with a trial just exit.
24134 return false;
24135 }
24136
24137 /**
24138 * @var FS_Plugin_Plan $paid_plan
24139 */
24140 $paid_plan = $trial_plans[0];
24141 $require_payment = $paid_plan->is_require_subscription;
24142 $trial_period = $paid_plan->trial_period;
24143
24144 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
24145
24146 if ( $total_paid_plans !== $trial_plans_count ) {
24147 // Not all paid plans have a trial - generate a string of those that have it.
24148 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
24149 $plans_string .= sprintf(
24150 ' <a href="%s">%s</a>',
24151 $trial_url,
24152 $trial_plans[ $i ]->title
24153 );
24154
24155 if ( $i < $trial_plans_count - 2 ) {
24156 $plans_string .= ', ';
24157 } else if ( $i == $trial_plans_count - 2 ) {
24158 $plans_string .= ' and ';
24159 }
24160 }
24161 }
24162 }
24163
24164 $message = sprintf(
24165 $this->get_text_x_inline( 'Hey', 'exclamation', 'hey' ) . '! ' . $this->get_text_inline( 'How do you like %s so far? Test all our %s premium features with a %d-day free trial.', 'trial-x-promotion-message' ),
24166 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
24167 $plans_string,
24168 $trial_period
24169 );
24170
24171 // "No Credit-Card Required" or "No Commitment for N Days".
24172 $cc_string = $require_payment ?
24173 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
24174 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
24175
24176
24177 // Start trial button.
24178 $button = ' ' . sprintf(
24179 '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24180 $trial_url,
24181 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
24182 );
24183
24184 $this->_admin_notices->add_sticky(
24185 $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
24186 'trial_promotion',
24187 '',
24188 'promotion'
24189 );
24190
24191 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
24192
24193 return true;
24194 }
24195
24196 /**
24197 * Lets users/customers know that the product has an affiliate program.
24198 *
24199 * @author Leo Fajardo (@leorw)
24200 * @since 1.2.2.11
24201 *
24202 * @return bool Returns true if the notice has been added.
24203 */
24204 function _add_affiliate_program_notice() {
24205 if ( ! $this->is_user_admin() ) {
24206 return false;
24207 }
24208
24209 if ( ! $this->is_user_in_admin() ) {
24210 return false;
24211 }
24212
24213 // Check if the notice is already shown.
24214 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
24215 return false;
24216 }
24217
24218 if (
24219 // Product has no affiliate program.
24220 ! $this->has_affiliate_program() ||
24221 // User has applied for an affiliate account.
24222 ! empty( $this->_storage->affiliate_application_data )
24223 ) {
24224 return false;
24225 }
24226
24227 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
24228 // Developer explicitly asked not to show the notice about the affiliate program.
24229 return false;
24230 }
24231
24232 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24233 // If not yet opted in/skipped, or pending activation, don't show the notice.
24234 return false;
24235 }
24236
24237 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
24238 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
24239
24240 /**
24241 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
24242 * activation with FS.
24243 */
24244 if ( $was_notice_shown_before ||
24245 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
24246 ) {
24247 return false;
24248 }
24249
24250 if ( ! $this->is_paying() &&
24251 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
24252 ) {
24253 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
24254 return false;
24255 }
24256
24257 $message = sprintf(
24258 $this->get_text_inline( 'Hey there, did you know that %s has an affiliate program? If you like the %s you can become our ambassador and earn some cash!', 'become-an-ambassador-admin-notice' ),
24259 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
24260 $this->get_module_label( true )
24261 );
24262
24263 // HTML code for the "Learn more..." button.
24264 $button = ' ' . sprintf(
24265 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24266 $this->_get_admin_page_url( 'affiliation' ),
24267 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
24268 );
24269
24270 $this->_admin_notices->add_sticky(
24271 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
24272 'affiliate_program',
24273 '',
24274 'promotion'
24275 );
24276
24277 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
24278
24279 return true;
24280 }
24281
24282 /**
24283 * @author Vova Feldman (@svovaf)
24284 * @since 1.2.1.5
24285 */
24286 function _enqueue_common_css() {
24287 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
24288 // Add basic CSS for admin-notices and menu-item colors.
24289 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
24290 }
24291 }
24292
24293 /**
24294 * @author Leo Fajardo (@leorw)
24295 * @since 1.2.2
24296 */
24297 function _show_theme_activation_optin_dialog() {
24298 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
24299
24300 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
24301 }
24302
24303 /**
24304 * @author Leo Fajardo (@leorw)
24305 * @since 1.2.2
24306 */
24307 function _add_fs_theme_activation_dialog() {
24308 global $pagenow;
24309
24310 if ( 'themes.php' !== $pagenow ) {
24311 return;
24312 }
24313
24314 $vars = array( 'id' => $this->_module_id );
24315 fs_require_once_template( 'connect.php', $vars );
24316 }
24317
24318 /* Action Links
24319 ------------------------------------------------------------------------------------------------------------------*/
24320 private $_action_links_hooked = false;
24321 private $_action_links = array();
24322
24323 /**
24324 * Hook to plugin action links filter.
24325 *
24326 * @author Vova Feldman (@svovaf)
24327 * @since 1.0.0
24328 */
24329 private function hook_plugin_action_links() {
24330 $this->_logger->entrance();
24331
24332 $this->_action_links_hooked = true;
24333
24334 $this->_logger->log( 'Adding action links hooks.' );
24335
24336 // Add action link to settings page.
24337 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
24338 &$this,
24339 '_modify_plugin_action_links_hook'
24340 ), WP_FS__DEFAULT_PRIORITY, 2 );
24341 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
24342 &$this,
24343 '_modify_plugin_action_links_hook'
24344 ), WP_FS__DEFAULT_PRIORITY, 2 );
24345 }
24346
24347 /**
24348 * Add plugin action link.
24349 *
24350 * @author Vova Feldman (@svovaf)
24351 * @since 1.0.0
24352 *
24353 * @param $label
24354 * @param $url
24355 * @param bool $external
24356 * @param int $priority
24357 * @param bool $key
24358 */
24359 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
24360 $this->_logger->entrance();
24361
24362 if ( ! isset( $this->_action_links[ $priority ] ) ) {
24363 $this->_action_links[ $priority ] = array();
24364 }
24365
24366 if ( false === $key ) {
24367 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
24368 }
24369
24370 $this->_action_links[ $priority ][] = array(
24371 'label' => $label,
24372 'href' => $url,
24373 'key' => $key,
24374 'external' => $external
24375 );
24376 }
24377
24378 /**
24379 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
24380 *
24381 * @author Vova Feldman (@svovaf)
24382 * @since 1.0.0
24383 */
24384 function _add_upgrade_action_link() {
24385 $this->_logger->entrance();
24386
24387 $is_activation_mode = $this->is_activation_mode();
24388
24389 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
24390
24391 /**
24392 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
24393 * and "Add-Ons" menus should be added.
24394 *
24395 * @author Leo Fajardo (@leorw)
24396 * @since 2.3.0
24397 */
24398 $add_upgrade_link = (
24399 $add_action_links ||
24400 ( $is_activation_mode && $this->is_only_premium() )
24401 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
24402
24403 $add_addons_link = ( $add_action_links && $this->has_addons() );
24404
24405 if ( ! $add_upgrade_link && ! $add_addons_link ) {
24406 return;
24407 }
24408
24409 if (
24410 $add_upgrade_link &&
24411 $this->is_pricing_page_visible() &&
24412 $this->is_submenu_item_visible( 'pricing' )
24413 ) {
24414 $this->add_plugin_action_link(
24415 $this->get_text_inline( 'Upgrade', 'upgrade' ),
24416 $this->get_upgrade_url(),
24417 false,
24418 7,
24419 'upgrade'
24420 );
24421 }
24422
24423 if (
24424 $add_addons_link &&
24425 $this->has_addons() &&
24426 $this->is_submenu_item_visible( 'addons' )
24427 ) {
24428 $this->add_plugin_action_link(
24429 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
24430 $this->_get_admin_page_url( 'addons' ),
24431 false,
24432 9,
24433 'addons'
24434 );
24435 }
24436 }
24437
24438 /**
24439 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
24440 *
24441 * @author Leo Fajardo (@leorw)
24442 * @since 1.1.9
24443 */
24444 function _add_license_action_link() {
24445 $this->_logger->entrance();
24446
24447 if ( ! self::is_ajax() ) {
24448 // Inject license activation dialog UI and client side code.
24449 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
24450 }
24451
24452 $link_text = $this->is_free_plan() ?
24453 $this->get_text_inline( 'Activate License', 'activate-license' ) :
24454 $this->get_text_inline( 'Change License', 'change-license' );
24455
24456 $this->add_plugin_action_link(
24457 $link_text,
24458 '#',
24459 false,
24460 11,
24461 ( 'activate-license ' . $this->get_unique_affix() )
24462 );
24463 }
24464
24465 /**
24466 * @author Leo Fajardo (@leorw)
24467 * @since 2.0.2
24468 */
24469 function _add_premium_version_upgrade_selection_action() {
24470 $this->_logger->entrance();
24471
24472 if ( ! self::is_ajax() ) {
24473 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
24474 }
24475 }
24476
24477 /**
24478 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
24479 *
24480 * @author Leo Fajardo (@leorw)
24481 * @since 1.2.1.5
24482 */
24483 function _add_tracking_links() {
24484 if ( ! current_user_can( 'manage_options' ) ) {
24485 return;
24486 }
24487
24488 $this->_logger->entrance();
24489
24490 if ( $this->is_only_premium() && $this->is_free_plan() ) {
24491 // Don't add tracking links for premium-only products that were opted-in by relation (add-on or a parent product) before activating any license.
24492 return;
24493 }
24494
24495 if (
24496 $this->is_addon() &&
24497 ! $this->is_only_premium()
24498 ) {
24499 $parent = $this->get_parent_instance();
24500
24501 if ( is_object( $parent ) && $parent->is_anonymous() ) {
24502 return;
24503 }
24504 }
24505
24506 if ( fs_is_network_admin() ) {
24507 if ( ! $this->_is_network_active ) {
24508 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
24509 return;
24510 } else if ( $this->is_network_delegated_connection() ) {
24511 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
24512 return;
24513 }
24514 } else {
24515 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
24516 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
24517 return;
24518 }
24519 }
24520
24521 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
24522 if ( ! $this->is_registered() && $this->is_anonymous() ) {
24523 $this->connect_again();
24524
24525 return;
24526 }
24527 }
24528
24529 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
24530 ( $this->is_theme() && ! self::is_themes_page() )
24531 ) {
24532 // Only show tracking links on the plugins and themes pages.
24533 return;
24534 }
24535
24536 if (
24537 $this->is_activation_mode() &&
24538 $this->is_premium() &&
24539 ! $this->is_registered()
24540 ) {
24541 // If not yet registered and running the premium code base, a license activation link will already be shown.
24542 return;
24543 }
24544
24545 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24546 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
24547 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
24548 return;
24549 }
24550 }
24551
24552 if ( $this->add_ajax_action( 'toggle_permission_tracking', array( &$this, '_toggle_permission_tracking_callback' ) ) ) {
24553 return;
24554 }
24555
24556 $link_text_id = '';
24557 $url = '#';
24558
24559 if ( $this->is_registered( true ) ) {
24560 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24561 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
24562 } else {
24563 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24564 }
24565 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
24566 /**
24567 * Show opt-in link only if skipped or in activation mode.
24568 */
24569 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24570
24571 $params = ! $this->is_anonymous() ?
24572 array() :
24573 array(
24574 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
24575 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
24576 );
24577
24578 $url = $this->get_activation_url( $params );
24579 }
24580
24581 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
24582
24583 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
24584 $this->add_plugin_action_link(
24585 $link_text_id,
24586 $url,
24587 false,
24588 13,
24589 "opt-in-or-opt-out {$this->_slug}"
24590 );
24591 }
24592 }
24593
24594 /**
24595 * Get the URL of the page that should be loaded right after the plugin activation.
24596 *
24597 * @author Vova Feldman (@svovaf)
24598 * @since 1.1.7.4
24599 *
24600 * @return string
24601 */
24602 function get_after_plugin_activation_redirect_url() {
24603 $url = false;
24604
24605 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
24606 $first_time_path = $this->_menu->get_first_time_path(
24607 fs_is_network_admin() && $this->_is_network_active
24608 );
24609
24610 if ( $this->is_activation_mode() ) {
24611 $url = $this->get_activation_url();
24612 } else if ( ! empty( $first_time_path ) ) {
24613 $url = $first_time_path;
24614 } else {
24615 $page = '';
24616 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
24617 if ( $this->is_network_registered() ) {
24618 $page = 'account';
24619 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
24620 $this->maybe_set_slug_and_network_menu_exists_flag();
24621 }
24622 }
24623
24624 $url = $this->_get_admin_page_url( $page );
24625 }
24626 } else {
24627 $plugin_fs = false;
24628
24629 if ( $this->is_parent_plugin_installed() ) {
24630 $plugin_fs = self::get_parent_instance();
24631 }
24632
24633 if ( is_object( $plugin_fs ) ) {
24634 if ( ! $plugin_fs->is_registered() ) {
24635 // Forward to parent plugin connect when parent not registered.
24636 $url = $plugin_fs->get_activation_url();
24637 } else {
24638 // Forward to account page.
24639 $url = $plugin_fs->_get_admin_page_url( 'account' );
24640 }
24641 }
24642 }
24643
24644 return $url;
24645 }
24646
24647 /**
24648 * Forward page to activation page.
24649 *
24650 * @author Vova Feldman (@svovaf)
24651 * @since 1.0.3
24652 */
24653 function _redirect_on_activation_hook() {
24654 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
24655 $url = $this->get_after_plugin_activation_redirect_url();
24656
24657 if ( is_string( $url ) ) {
24658 fs_redirect( $url );
24659 }
24660 }
24661 }
24662
24663 /**
24664 * Modify plugin's page action links collection.
24665 *
24666 * @author Vova Feldman (@svovaf)
24667 * @since 1.0.0
24668 *
24669 * @param array $links
24670 * @param $file
24671 *
24672 * @return array
24673 */
24674 function _modify_plugin_action_links_hook( $links, $file ) {
24675 $this->_logger->entrance();
24676
24677 $passed_deactivate = false;
24678 $deactivate_link = '';
24679 $before_deactivate = array();
24680 $after_deactivate = array();
24681 foreach ( $links as $key => $link ) {
24682 if ( 'deactivate' === $key ) {
24683 $deactivate_link = $link;
24684 $passed_deactivate = true;
24685 continue;
24686 }
24687
24688 if ( ! $passed_deactivate ) {
24689 $before_deactivate[ $key ] = $link;
24690 } else {
24691 $after_deactivate[ $key ] = $link;
24692 }
24693 }
24694
24695 ksort( $this->_action_links );
24696
24697 foreach ( $this->_action_links as $new_links ) {
24698 foreach ( $new_links as $link ) {
24699 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
24700 }
24701 }
24702
24703 if ( ! empty( $deactivate_link ) ) {
24704 /**
24705 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
24706 *
24707 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
24708 */
24709 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
24710
24711 // Append deactivation link.
24712 $before_deactivate['deactivate'] = $deactivate_link;
24713 }
24714
24715 return array_merge( $before_deactivate, $after_deactivate );
24716 }
24717
24718 /**
24719 * Adds admin message.
24720 *
24721 * @author Vova Feldman (@svovaf)
24722 * @since 1.0.4
24723 *
24724 * @param string $message
24725 * @param string $title
24726 * @param string $type
24727 */
24728 function add_admin_message( $message, $title = '', $type = 'success' ) {
24729 $this->_admin_notices->add( $message, $title, $type );
24730 }
24731
24732 /**
24733 * Adds sticky admin message.
24734 *
24735 * @author Vova Feldman (@svovaf)
24736 * @since 1.1.0
24737 *
24738 * @param string $message
24739 * @param string $id
24740 * @param string $title
24741 * @param string $type
24742 */
24743 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
24744 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
24745 }
24746
24747 /**
24748 * Check if the paid version of the module is installed.
24749 *
24750 * @author Vova Feldman (@svovaf)
24751 * @since 2.2.0
24752 *
24753 * @return bool
24754 */
24755 private function is_premium_version_installed() {
24756 $premium_plugin_basename = $this->premium_plugin_basename();
24757
24758 if ( $this->is_theme() ) {
24759 return $this->can_activate_theme( $this->get_premium_slug() );
24760 }
24761
24762 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_plugin_basename ) );
24763 }
24764
24765 /**
24766 * Helper function that returns the final steps for the upgrade completion.
24767 *
24768 * If the module is already running the premium code, returns an empty string.
24769 *
24770 * @author Vova Feldman (@svovaf)
24771 * @since 1.2.1
24772 *
24773 * @param string $plan_title
24774 *
24775 * @return string
24776 */
24777 private function get_complete_upgrade_instructions( $plan_title = '' ) {
24778 $this->_logger->entrance();
24779
24780 $activate_license_string = $this->get_license_network_activation_notice();
24781
24782 if ( ! $this->has_premium_version() || $this->is_premium() ) {
24783 return '' . $activate_license_string;
24784 }
24785
24786 if ( empty( $plan_title ) ) {
24787 $plan_title = $this->get_plan_title();
24788 }
24789
24790 if ( $this->is_premium_version_installed() ) {
24791 /**
24792 * If the premium version is already installed, instead of showing the installation instructions,
24793 * tell the current user to activate it.
24794 *
24795 * @author Leo Fajardo (@leorw)
24796 * @since 2.2.1
24797 */
24798 $premium_theme_slug_or_plugin_basename = $this->is_theme() ?
24799 $this->get_premium_slug() :
24800 $this->premium_plugin_basename();
24801
24802 return sprintf(
24803 /* translators: %1$s: Product title; %2$s: Plan title */
24804 $this->get_text_inline( ' The paid version of %1$s is already installed. Please activate it to start benefiting the %2$s features. %3$s', 'activate-premium-version' ),
24805 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24806 $plan_title,
24807 sprintf(
24808 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
24809 ( $this->is_theme() ?
24810 wp_nonce_url( 'themes.php?action=activate&amp;stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) :
24811 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ),
24812 esc_html( sprintf(
24813 /* translators: %s: Plan title */
24814 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
24815 $plan_title
24816 ) )
24817 )
24818 );
24819 } else {
24820 // @since 1.2.1.5 The free version is auto deactivated.
24821 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
24822 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
24823 '';
24824
24825 return sprintf(
24826 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
24827 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
24828 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
24829 $this->get_latest_download_link( sprintf(
24830 /* translators: %s: Plan title */
24831 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
24832 $plan_title
24833 ) ),
24834 $deactivation_step,
24835 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
24836 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/wp-' . $this->_module_type . '-upload' ),
24837 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
24838 );
24839 }
24840 }
24841
24842 /**
24843 * @author Leo Fajardo (@leorw)
24844 * @since 2.5.3
24845 *
24846 * @param string $message_before_the_instructions
24847 * @param string $message_id
24848 * @param string $plan_title
24849 */
24850 private function add_complete_upgrade_instructions_notice(
24851 $message_before_the_instructions,
24852 $message_id,
24853 $plan_title = ''
24854 ) {
24855 $this->_admin_notices->add_sticky(
24856 $message_before_the_instructions .
24857 $this->get_complete_upgrade_instructions( $plan_title ),
24858 $message_id,
24859 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
24860 );
24861 }
24862
24863 /**
24864 * @author Leo Fajardo (@leorw)
24865 * @since 2.5.3
24866 *
24867 * @param bool $is_upgrade
24868 */
24869 private function add_after_plan_activation_or_upgrade_instructions_notice( $is_upgrade = true ) {
24870 $this->add_complete_upgrade_instructions_notice(
24871 $is_upgrade ?
24872 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) :
24873 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ),
24874 'plan_upgraded'
24875 );
24876 }
24877
24878 /**
24879 * @author Leo Fajardo (@leorw)
24880 * @since 2.1.0
24881 *
24882 * @param string $url
24883 * @param array $request
24884 */
24885 private static function enrich_request_for_debug( &$url, &$request ) {
24886 if ( WP_FS__DEBUG_SDK || isset( $_COOKIE['XDEBUG_SESSION'] ) ) {
24887 $url = add_query_arg( 'XDEBUG_SESSION_START', rand( 0, 9999999 ), $url );
24888 $url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url );
24889
24890 $request['cookies'] = array(
24891 new WP_Http_Cookie( array(
24892 'name' => 'XDEBUG_SESSION',
24893 'value' => 'PHPSTORM',
24894 ) )
24895 );
24896 }
24897 }
24898
24899 /**
24900 * @author Leo Fajardo (@leorw)
24901 * @since 2.1.0
24902 *
24903 * @param string $url
24904 * @param array $request
24905 * @param int $success_cache_expiration
24906 * @param int $failure_cache_expiration
24907 * @param bool $maybe_enrich_request_for_debug
24908 *
24909 * @return WP_Error|array
24910 */
24911 static function safe_remote_post(
24912 &$url,
24913 $request,
24914 $success_cache_expiration = 0,
24915 $failure_cache_expiration = 0,
24916 $maybe_enrich_request_for_debug = true
24917 ) {
24918 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
24919
24920 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
24921
24922 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
24923 get_transient( $cache_key ) :
24924 false;
24925
24926 if ( false === $response ) {
24927 if ( $maybe_enrich_request_for_debug ) {
24928 self::enrich_request_for_debug( $url, $request );
24929 }
24930
24931 if ( ! isset( $request['method'] ) ) {
24932 $request['method'] = 'POST';
24933 }
24934
24935 $response = FS_Api::remote_request( $url, $request );
24936
24937 if (
24938 'https://' === substr( $url, 0, 8 ) &&
24939 FS_Api::is_ssl_error_response( $response )
24940 ) {
24941 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
24942 $url = 'http://' . substr( $url, 8 );
24943
24944 $request['timeout'] = 15;
24945 $response = FS_Api::remote_request( $url, $request );
24946 }
24947
24948 if ( false !== $cache_key ) {
24949 set_transient(
24950 $cache_key,
24951 $response,
24952 ( ( $response instanceof WP_Error ) ?
24953 $failure_cache_expiration :
24954 $success_cache_expiration )
24955 );
24956 }
24957 }
24958
24959 return $response;
24960 }
24961
24962 /**
24963 * This method is used to enrich the after upgrade notice instructions when the upgraded
24964 * license cannot be activated network wide (license quota isn't large enough).
24965 *
24966 * @author Vova Feldman (@svovaf)
24967 * @since 2.0.0
24968 *
24969 * @return string
24970 */
24971 private function get_license_network_activation_notice() {
24972 if ( ! $this->_is_network_active ) {
24973 // Module isn't network level activated.
24974 return '';
24975 }
24976
24977 if ( ! fs_is_network_admin() ) {
24978 // Not network level admin.
24979 return '';
24980 }
24981
24982 if ( get_blog_count() == 1 ) {
24983 // There's only a single site in the network so if there's a context license it was already activated.
24984 return '';
24985 }
24986
24987 if ( ! is_object( $this->_license ) ) {
24988 // No context license.
24989 return '';
24990 }
24991
24992 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
24993 // License was already utilized (this is not 100% the case if all the network is localhost sites and the license can be utilized on unlimited localhost sites).
24994 return '';
24995 }
24996
24997 if ( $this->can_activate_license_on_network( $this->_license ) ) {
24998 // License can be activated on all the network, so probably, the license is already activate on all the network (that's how the after upgrade sync works).
24999 return '';
25000 }
25001
25002 return sprintf(
25003 $this->get_text_inline( '%sClick here%s to choose the sites where you\'d like to activate the license on.', 'network-choose-sites-for-license' ),
25004 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
25005 '</a>'
25006 );
25007 }
25008
25009 /**
25010 * @author Vova Feldman (@svovaf)
25011 * @since 1.2.1.7
25012 *
25013 * @param string $key
25014 *
25015 * @return string
25016 */
25017 function get_text( $key ) {
25018 return fs_text( $key, $this->_slug );
25019 }
25020
25021 /**
25022 * @author Vova Feldman (@svovaf)
25023 * @since 1.2.3
25024 *
25025 * @param string $text Translatable string.
25026 * @param string $key String key for overrides.
25027 *
25028 * @return string
25029 */
25030 function get_text_inline( $text, $key = '' ) {
25031 return _fs_text_inline( $text, $key, $this->_slug );
25032 }
25033
25034 /**
25035 * @author Vova Feldman (@svovaf)
25036 * @since 1.2.3
25037 *
25038 * @param string $text Translatable string.
25039 * @param string $context Context information for the translators.
25040 * @param string $key String key for overrides.
25041 *
25042 * @return string
25043 */
25044 function get_text_x_inline( $text, $context, $key ) {
25045 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
25046 }
25047
25048 /**
25049 * @author Vova Feldman (@svovaf)
25050 * @since 1.2.3
25051 *
25052 * @param string $text Translatable string.
25053 * @param string $key String key for overrides.
25054 *
25055 * @return string
25056 */
25057 function esc_html_inline( $text, $key ) {
25058 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
25059 }
25060
25061 #----------------------------------------------------------------------------------
25062 #region Versioning
25063 #----------------------------------------------------------------------------------
25064
25065 /**
25066 * Check if Freemius in SDK upgrade mode.
25067 *
25068 * @author Vova Feldman (@svovaf)
25069 * @since 1.0.9
25070 *
25071 * @return bool
25072 */
25073 function is_sdk_upgrade_mode() {
25074 return isset( $this->_storage->sdk_upgrade_mode ) ?
25075 $this->_storage->sdk_upgrade_mode :
25076 false;
25077 }
25078
25079 /**
25080 * Turn SDK upgrade mode off.
25081 *
25082 * @author Vova Feldman (@svovaf)
25083 * @since 1.0.9
25084 */
25085 function set_sdk_upgrade_complete() {
25086 $this->_storage->sdk_upgrade_mode = false;
25087 }
25088
25089 /**
25090 * Check if plugin upgrade mode.
25091 *
25092 * @author Vova Feldman (@svovaf)
25093 * @since 1.0.9
25094 *
25095 * @return bool
25096 */
25097 function is_plugin_upgrade_mode() {
25098 return isset( $this->_storage->plugin_upgrade_mode ) ?
25099 $this->_storage->plugin_upgrade_mode :
25100 false;
25101 }
25102
25103 /**
25104 * Turn plugin upgrade mode off.
25105 *
25106 * @author Vova Feldman (@svovaf)
25107 * @since 1.0.9
25108 */
25109 function set_plugin_upgrade_complete() {
25110 $this->_storage->plugin_upgrade_mode = false;
25111
25112 $license_migration = ! empty( $this->_storage->license_migration ) ?
25113 $this->_storage->license_migration :
25114 array();
25115
25116 $license_migration['is_migrating'] = false;
25117
25118 $this->_storage->license_migration = $license_migration;
25119 }
25120
25121 #endregion
25122
25123 #----------------------------------------------------------------------------------
25124 #region Permissions
25125 #----------------------------------------------------------------------------------
25126
25127 /**
25128 * Check if specific permission requested.
25129 *
25130 * @author Vova Feldman (@svovaf)
25131 * @since 1.1.6
25132 *
25133 * @param string $permission
25134 *
25135 * @return bool
25136 */
25137 function is_permission_requested( $permission ) {
25138 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
25139 }
25140
25141 #endregion
25142
25143 #----------------------------------------------------------------------------------
25144 #region Auto Activation
25145 #----------------------------------------------------------------------------------
25146
25147 /**
25148 * Hints the SDK if running an auto-installation.
25149 *
25150 * @var bool
25151 */
25152 private $_isAutoInstall = false;
25153
25154 /**
25155 * After upgrade callback to install and auto activate a plugin.
25156 * This code will only be executed on explicit request from the user,
25157 * following the practice Jetpack are using with their theme installations.
25158 *
25159 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
25160 *
25161 * @author Vova Feldman (@svovaf)
25162 * @since 1.2.1.7
25163 */
25164 function _install_premium_version_ajax_action() {
25165 $this->_logger->entrance();
25166
25167 $this->check_ajax_referer( 'install_premium_version' );
25168
25169 if ( ! $this->is_registered() ) {
25170 // Not registered.
25171 self::shoot_ajax_failure( array(
25172 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
25173 'code' => 'premium_installed',
25174 ) );
25175 }
25176
25177 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
25178
25179 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25180 // Invalid ID.
25181 self::shoot_ajax_failure( array(
25182 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25183 'code' => 'invalid_module_id',
25184 ) );
25185 }
25186
25187 if ( $plugin_id == $this->get_id() ) {
25188 if ( $this->is_premium() ) {
25189 // Already using the premium code version.
25190 self::shoot_ajax_failure( array(
25191 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
25192 'code' => 'premium_installed',
25193 ) );
25194 }
25195 if ( ! $this->can_use_premium_code() ) {
25196 // Don't have access to the premium code.
25197 self::shoot_ajax_failure( array(
25198 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
25199 'code' => 'invalid_license',
25200 ) );
25201 }
25202 if ( ! $this->has_release_on_freemius() ) {
25203 // Plugin is a serviceware, no premium code version.
25204 self::shoot_ajax_failure( array(
25205 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
25206 'code' => 'premium_version_missing',
25207 ) );
25208 }
25209 } else {
25210 $addon = $this->get_addon( $plugin_id );
25211
25212 if ( ! is_object( $addon ) ) {
25213 // Invalid add-on ID.
25214 self::shoot_ajax_failure( array(
25215 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25216 'code' => 'invalid_module_id',
25217 ) );
25218 }
25219
25220 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25221 // Premium add-on version is already activated.
25222 self::shoot_ajax_failure( array(
25223 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
25224 'code' => 'premium_installed',
25225 ) );
25226 }
25227 }
25228
25229 $this->_isAutoInstall = true;
25230
25231 // Try to install and activate.
25232 $updater = FS_Plugin_Updater::instance( $this );
25233 $result = $updater->install_and_activate_plugin( $plugin_id );
25234
25235 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
25236 self::shoot_ajax_failure( array(
25237 'message' => $result['message'],
25238 'code' => $result['code'],
25239 ) );
25240 }
25241
25242 self::shoot_ajax_success( $result );
25243 }
25244
25245 /**
25246 * Displays module activation dialog box after a successful upgrade
25247 * where the user explicitly requested to auto download and install
25248 * the premium version.
25249 *
25250 * @author Vova Feldman (@svovaf)
25251 * @since 1.2.1.7
25252 */
25253 function _add_auto_installation_dialog_box() {
25254 $this->_logger->entrance();
25255
25256 if ( ! $this->is_registered() ) {
25257 // Not registered.
25258 return;
25259 }
25260
25261 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
25262
25263 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25264 // Invalid module ID.
25265 return;
25266 }
25267
25268 if ( $plugin_id == $this->get_id() ) {
25269 if ( $this->is_premium() ) {
25270 // Already using the premium code version.
25271 return;
25272 }
25273 if ( ! $this->can_use_premium_code() ) {
25274 // Don't have access to the premium code.
25275 return;
25276 }
25277 if ( ! $this->has_release_on_freemius() ) {
25278 // Plugin is a serviceware, no premium code version.
25279 return;
25280 }
25281 } else {
25282 $addon = $this->get_addon( $plugin_id );
25283
25284 if ( ! is_object( $addon ) ) {
25285 // Invalid add-on ID.
25286 return;
25287 }
25288
25289 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25290 // Premium add-on version is already activated.
25291 return;
25292 }
25293 }
25294
25295 $vars = array(
25296 'id' => $this->_module_id,
25297 'target_module_id' => $plugin_id,
25298 'slug' => $this->_slug,
25299 );
25300
25301 fs_require_template( 'auto-installation.php', $vars );
25302 }
25303
25304 #endregion
25305
25306 #--------------------------------------------------------------------------------
25307 #region Tabs Integration
25308 #--------------------------------------------------------------------------------
25309
25310 #region Module's Original Tabs
25311
25312 /**
25313 * Inject a JavaScript logic to capture the theme tabs HTML.
25314 *
25315 * @author Vova Feldman (@svovaf)
25316 * @since 1.2.2.7
25317 */
25318 function _tabs_capture() {
25319 $this->_logger->entrance();
25320
25321 if (
25322 ! $this->is_product_settings_page() ||
25323 ! $this->should_page_include_tabs() ||
25324 ! $this->is_matching_url( $this->main_menu_url() )
25325 ) {
25326 return;
25327 }
25328
25329 $params = array(
25330 'id' => $this->_module_id,
25331 );
25332
25333 fs_require_once_template( 'tabs-capture-js.php', $params );
25334 }
25335
25336 /**
25337 * Cache theme's tabs HTML for a week. The cache will also be set as expired
25338 * after version and type (free/premium) changes, in addition to the week period.
25339 *
25340 * @author Vova Feldman (@svovaf)
25341 * @since 1.2.2.7
25342 */
25343 function _store_tabs_ajax_action() {
25344 $this->_logger->entrance();
25345
25346 $this->check_ajax_referer( 'store_tabs' );
25347
25348 // Init filesystem if not yet initiated.
25349 WP_Filesystem();
25350
25351 // Get POST body HTML data.
25352 global $wp_filesystem;
25353 $tabs_html = $wp_filesystem->get_contents( "php://input" );
25354
25355 if ( is_string( $tabs_html ) ) {
25356 $tabs_html = trim( $tabs_html );
25357 }
25358
25359 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
25360 self::shoot_ajax_failure();
25361 }
25362
25363 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25364
25365 self::shoot_ajax_success();
25366 }
25367
25368 /**
25369 * Cache theme's settings page custom styles. The cache will also be set as expired
25370 * after version and type (free/premium) changes, in addition to the week period.
25371 *
25372 * @author Vova Feldman (@svovaf)
25373 * @since 1.2.2.7
25374 */
25375 function _store_tabs_styles() {
25376 $this->_logger->entrance();
25377
25378 if (
25379 ! $this->is_product_settings_page() ||
25380 ! $this->should_page_include_tabs() ||
25381 ! $this->is_matching_url( $this->main_menu_url() )
25382 ) {
25383 return;
25384 }
25385
25386 $wp_styles = wp_styles();
25387
25388 $theme_styles_url = get_template_directory_uri();
25389
25390 $stylesheets = array();
25391 foreach ( $wp_styles->queue as $handler ) {
25392 if ( fs_starts_with( $handler, 'fs_' ) ) {
25393 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
25394 continue;
25395 }
25396
25397 /**
25398 * @var _WP_Dependency $stylesheet
25399 */
25400 $stylesheet = $wp_styles->registered[ $handler ];
25401
25402 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
25403 $stylesheets[] = $stylesheet->src;
25404 }
25405 }
25406
25407 if ( ! empty( $stylesheets ) ) {
25408 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25409 }
25410 }
25411
25412 /**
25413 * Check if module's original settings page has any tabs.
25414 *
25415 * @author Vova Feldman (@svovaf)
25416 * @since 1.2.2.7
25417 *
25418 * @return bool
25419 */
25420 private function has_tabs() {
25421 return $this->_cache->has( 'tabs' );
25422 }
25423
25424 /**
25425 * Get module's settings page HTML content, starting
25426 * from the beginning of the <div class="wrap"> element,
25427 * until the tabs HTML (including).
25428 *
25429 * @author Vova Feldman (@svovaf)
25430 * @since 1.2.2.7
25431 *
25432 * @return string
25433 */
25434 private function get_tabs_html() {
25435 $this->_logger->entrance();
25436
25437 return $this->_cache->get( 'tabs' );
25438 }
25439
25440 /**
25441 * Check if page should include tabs.
25442 *
25443 * @author Vova Feldman (@svovaf)
25444 * @since 1.2.2.7
25445 *
25446 * @return bool
25447 */
25448 private function should_page_include_tabs() {
25449 if ( ! $this->has_settings_menu() ) {
25450 // Don't add tabs if no settings at all.
25451 return false;
25452 }
25453
25454 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
25455 // Only add tabs to themes for now.
25456 return false;
25457 }
25458
25459 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
25460 // Only add tabs to monetizing themes.
25461 return false;
25462 }
25463
25464 if ( ! $this->is_product_settings_page() ) {
25465 // Only add tabs if browsing one of the product's setting pages.
25466 return false;
25467 }
25468
25469 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
25470 // Don't include tabs in the activation page.
25471 return false;
25472 }
25473
25474 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
25475 // Don't add tabs on checkout page, we want to reduce distractions
25476 // as much as possible.
25477 return false;
25478 }
25479
25480 return true;
25481 }
25482
25483 /**
25484 * Add the tabs HTML before the setting's page content and
25485 * enqueue any required stylesheets.
25486 *
25487 * @author Vova Feldman (@svovaf)
25488 * @since 1.2.2.7
25489 *
25490 * @return bool If tabs were included.
25491 */
25492 function _add_tabs_before_content() {
25493 $this->_logger->entrance();
25494
25495 if ( ! $this->should_page_include_tabs() ) {
25496 return false;
25497 }
25498
25499 $tabs_html = $this->get_tabs_html();
25500
25501 if ( empty( $tabs_html ) ) {
25502 return false;
25503 }
25504
25505 /**
25506 * Enqueue the original stylesheets that are included in the
25507 * theme settings page. That way, if the theme settings has
25508 * some custom _styled_ content above the tabs UI, this
25509 * will make sure that the styling is preserved.
25510 */
25511 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
25512 if ( is_array( $stylesheets ) ) {
25513 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
25514 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
25515 }
25516 }
25517
25518 // Cut closing </div> tag.
25519 echo substr( trim( $tabs_html ), 0, - 6 );
25520
25521 return true;
25522 }
25523
25524 /**
25525 * Add the tabs closing HTML after the setting's page content.
25526 *
25527 * @author Vova Feldman (@svovaf)
25528 * @since 1.2.2.7
25529 *
25530 * @return bool If tabs closing HTML was included.
25531 */
25532 function _add_tabs_after_content() {
25533 $this->_logger->entrance();
25534
25535 if ( ! $this->should_page_include_tabs() ) {
25536 return false;
25537 }
25538
25539 echo '</div>';
25540
25541 return true;
25542 }
25543
25544 #endregion
25545
25546 /**
25547 * Add in-page JavaScript to inject the Freemius tabs into
25548 * the module's setting tabs section.
25549 *
25550 * @author Vova Feldman (@svovaf)
25551 * @since 1.2.2.7
25552 */
25553 function _add_freemius_tabs() {
25554 $this->_logger->entrance();
25555
25556 if ( ! $this->should_page_include_tabs() ) {
25557 return;
25558 }
25559
25560 $params = array( 'id' => $this->_module_id );
25561 fs_require_once_template( 'tabs.php', $params );
25562 }
25563
25564 #endregion
25565
25566 #--------------------------------------------------------------------------------
25567 #region Customizer Integration for Themes
25568 #--------------------------------------------------------------------------------
25569
25570 /**
25571 * @author Vova Feldman (@svovaf)
25572 * @since 1.2.2.7
25573 *
25574 * @param WP_Customize_Manager $customizer
25575 */
25576 function _customizer_register( $customizer ) {
25577 $this->_logger->entrance();
25578
25579 if ( $this->is_pricing_page_visible() ) {
25580 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
25581
25582 $customizer->add_section( 'freemius_upsell', array(
25583 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
25584 'priority' => 1,
25585 ) );
25586 $customizer->add_setting( 'freemius_upsell', array(
25587 'sanitize_callback' => 'esc_html',
25588 ) );
25589
25590 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
25591 'fs' => $this,
25592 'section' => 'freemius_upsell',
25593 'priority' => 100,
25594 ) ) );
25595 }
25596
25597 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
25598 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
25599
25600 // Main Documentation Link In Customizer Root.
25601 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
25602 'fs' => $this,
25603 'priority' => 1000,
25604 ) ) );
25605 }
25606 }
25607
25608 #endregion
25609
25610 /**
25611 * If the theme has a paid version, add some custom
25612 * styling to the theme's premium version (if exists)
25613 * to highlight that it's the premium version of the
25614 * same theme, making it easier for identification
25615 * after the user upgrades and upload it to the site.
25616 *
25617 * @author Vova Feldman (@svovaf)
25618 * @since 1.2.2.7
25619 */
25620 function _style_premium_theme() {
25621 $this->_logger->entrance();
25622
25623 if ( ! self::is_themes_page() ) {
25624 // Only include in the themes page.
25625 return;
25626 }
25627
25628 if ( ! $this->has_paid_plan() ) {
25629 // Only include if has any paid plans.
25630 return;
25631 }
25632
25633 $params = null;
25634 fs_require_once_template( '/js/jquery.content-change.php', $params );
25635
25636 $params = array(
25637 'slug' => $this->_slug,
25638 'id' => $this->_module_id,
25639 );
25640
25641 fs_require_template( '/js/style-premium-theme.php', $params );
25642 }
25643
25644 /**
25645 * This method will return the absolute URL of the module's local icon.
25646 *
25647 * When you are running your plugin or theme on a **localhost** environment, if the icon
25648 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
25649 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
25650 * If an icon is found, this method will automatically attempt to download the icon and store it
25651 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
25652 *
25653 * It's important to mention that this method is NOT phoning home since the developer will deploy
25654 * the product with the local icon in the assets folder. The download process just simplifies
25655 * the process for the developer.
25656 *
25657 * @author Vova Feldman (@svovaf)
25658 * @since 2.0.0
25659 *
25660 * @return string
25661 */
25662 function get_local_icon_url() {
25663 global $fs_active_plugins;
25664
25665 /**
25666 * @since 1.1.7.5
25667 */
25668 $local_path = $this->apply_filters( 'plugin_icon', false );
25669
25670 if ( is_string( $local_path ) ) {
25671 $icons = array( $local_path );
25672 } else {
25673 $img_dir = WP_FS__DIR_IMG;
25674
25675 // Locate the main assets folder.
25676 if ( 1 < count( $fs_active_plugins->plugins ) ) {
25677 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
25678
25679 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
25680 if ( $data->plugin_path == $this->get_plugin_basename() ) {
25681 $img_dir = $plugin_or_theme_img_dir
25682 . '/'
25683 /**
25684 * The basename will be `themes` or the basename of a custom themes directory.
25685 *
25686 * @author Leo Fajardo (@leorw)
25687 * @since 2.2.3
25688 */
25689 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
25690 . '/assets/img';
25691
25692 break;
25693 }
25694 }
25695 }
25696
25697 // Try to locate the icon in the assets folder.
25698 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
25699
25700 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
25701 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
25702 $icons = array(
25703 fs_normalize_path( $img_dir . '/theme-icon.png' )
25704 );
25705 } else {
25706 $icon_found = false;
25707 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
25708
25709 if ( ! function_exists( 'get_filesystem_method' ) ) {
25710 require_once ABSPATH . 'wp-admin/includes/file.php';
25711 }
25712
25713 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
25714
25715 /**
25716 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
25717 *
25718 * This code will only be executed once during the testing
25719 * of the plugin in a local environment. The plugin icon file WILL
25720 * already exist in the assets folder when the plugin is deployed to
25721 * the repository.
25722 */
25723 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
25724 // Fetch icon from Freemius.
25725 $icon = $this->fetch_remote_icon_url();
25726
25727 // Fetch icon from WordPress.org.
25728 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
25729 if ( ! function_exists( 'plugins_api' ) ) {
25730 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
25731 }
25732
25733 $plugin_information = plugins_api( 'plugin_information', array(
25734 'slug' => $this->_slug,
25735 'fields' => array(
25736 'sections' => false,
25737 'tags' => false,
25738 'icons' => true
25739 )
25740 ) );
25741
25742 if (
25743 ! is_wp_error( $plugin_information )
25744 && isset( $plugin_information->icons )
25745 && ! empty( $plugin_information->icons )
25746 ) {
25747 /**
25748 * Get the smallest icon.
25749 *
25750 * @author Leo Fajardo (@leorw)
25751 * @since 1.2.2
25752 */
25753 $icon = end( $plugin_information->icons );
25754 }
25755 }
25756
25757 if ( ! empty( $icon ) ) {
25758 if ( 0 !== strpos( $icon, 'http' ) ) {
25759 $icon = 'http:' . $icon;
25760 }
25761
25762 /**
25763 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
25764 *
25765 * @author Leo Fajardo (@leorw)
25766 * @since 1.2.2
25767 */
25768 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
25769
25770 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
25771
25772 // Try to download the icon.
25773 $icon_found = fs_download_image( $icon, $local_path );
25774 }
25775 }
25776
25777 if ( ! $icon_found ) {
25778 // No icons found, fallback to default icon.
25779 if ( $have_write_permissions ) {
25780 // If have write permissions, copy default icon.
25781 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
25782 } else {
25783 // If doesn't have write permissions, use default icon path.
25784 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
25785 }
25786 }
25787
25788 $icons = array( $local_path );
25789 }
25790 }
25791 }
25792
25793 $icon_dir = dirname( $icons[0] );
25794
25795 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
25796 }
25797
25798 /**
25799 * Fetch module's extended info.
25800 *
25801 * @author Vova Feldman (@svovaf)
25802 * @since 2.0.0
25803 *
25804 * @return object|mixed
25805 */
25806 private function fetch_module_info() {
25807 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
25808 }
25809
25810 /**
25811 * Fetch module's remote icon URL.
25812 *
25813 * @author Vova Feldman (@svovaf)
25814 * @since 2.0.0
25815 *
25816 * @return string
25817 */
25818 function fetch_remote_icon_url() {
25819 $info = $this->fetch_module_info();
25820
25821 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
25822 $info->icon :
25823 '';
25824 }
25825
25826 #--------------------------------------------------------------------------------
25827 #region GDPR
25828 #--------------------------------------------------------------------------------
25829
25830 /**
25831 * @author Leo Fajardo (@leorw)
25832 * @since 2.1.0
25833 *
25834 * @param array $user_plugins
25835 *
25836 * @return string
25837 */
25838 private function get_gdpr_admin_notice_string( $user_plugins ) {
25839 $this->_logger->entrance();
25840
25841 $addons = self::get_all_addons();
25842
25843 foreach ( $user_plugins as $user_plugin ) {
25844 $has_addons = isset( $addons[ $user_plugin->id ] );
25845
25846 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
25847 if ( $this->_module_id == $user_plugin->id ) {
25848 $addons = $this->get_addons();
25849 $has_addons = ( ! empty( $addons ) );
25850 } else {
25851 $plugin_api = FS_Api::instance(
25852 $user_plugin->id,
25853 'plugin',
25854 $user_plugin->id,
25855 $user_plugin->public_key,
25856 ! $user_plugin->is_live,
25857 false,
25858 $this->get_sdk_version()
25859 );
25860
25861 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
25862
25863 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
25864 is_array( $addons_result->plugins ) &&
25865 ! empty( $addons_result->plugins )
25866 ) {
25867 $has_addons = true;
25868 }
25869 }
25870 }
25871
25872 $user_plugin->has_addons = $has_addons;
25873 }
25874
25875 $is_single_parent_product = ( 1 === count( $user_plugins ) );
25876
25877 $multiple_products_text = '';
25878
25879 if ( $is_single_parent_product ) {
25880 $single_parent_product = reset( $user_plugins );
25881
25882 $thank_you = sprintf(
25883 "<span data-plugin-id='%d'>%s</span>",
25884 $single_parent_product->id,
25885 sprintf(
25886 $single_parent_product->has_addons ?
25887 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
25888 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
25889 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
25890 )
25891 );
25892
25893 $already_opted_in = sprintf(
25894 $this->get_text_inline( "You've already opted-in to our usage-tracking, which helps us keep improving the %s.", 'already-opted-in-to-product-usage-tracking' ),
25895 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
25896 );
25897 } else {
25898 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
25899 $already_opted_in = $this->get_text_inline( "You've already opted-in to our usage-tracking, which helps us keep improving them.", 'already-opted-in-to-products-usage-tracking' );
25900
25901 $products_and_add_ons = '';
25902 foreach ( $user_plugins as $user_plugin ) {
25903 if ( ! empty( $products_and_add_ons ) ) {
25904 $products_and_add_ons .= ', ';
25905 }
25906
25907 if ( ! $user_plugin->has_addons ) {
25908 $products_and_add_ons .= sprintf(
25909 "<span data-plugin-id='%d'>%s</span>",
25910 $user_plugin->id,
25911 $user_plugin->title
25912 );
25913 } else {
25914 $products_and_add_ons .= sprintf(
25915 "<span data-plugin-id='%d'>%s</span>",
25916 $user_plugin->id,
25917 sprintf(
25918 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
25919 $user_plugin->title
25920 )
25921 );
25922 }
25923 }
25924
25925 $multiple_products_text = sprintf(
25926 "<small class='products'><strong>%s:</strong> %s</small>",
25927 $this->get_text_inline( 'Products', 'products' ),
25928 $products_and_add_ons
25929 );
25930 }
25931
25932 $actions = sprintf(
25933 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
25934 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
25935 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
25936 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
25937 sprintf(
25938 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
25939 '<span class="underlined">',
25940 '</span>'
25941 )
25942 );
25943
25944 return sprintf(
25945 '%s %s %s',
25946 $thank_you,
25947 $already_opted_in,
25948 sprintf( $this->get_text_inline( 'Due to the new %sEU General Data Protection Regulation (GDPR)%s compliance requirements it is required that you provide your explicit consent, again, confirming that you are onboard :-)', 'due-to-gdpr-compliance-requirements' ), '<a href="https://ec.europa.eu/info/law/law-topic/data-protection_en/" target="_blank" rel="noopener noreferrer">', '</a>' ) .
25949 '<br><br>' .
25950 '<b>' . $this->get_text_inline( "Please let us know if you'd like us to contact you for security & feature updates, educational content, and occasional offers:", 'contact-for-updates' ) . '</b>' .
25951 $actions .
25952 ( $is_single_parent_product ? '' : $multiple_products_text )
25953 );
25954 }
25955
25956 /**
25957 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
25958 * plugins and themes they've opted in to.
25959 *
25960 * @author Leo Fajardo (@leorw)
25961 * @since 2.1.0
25962 *
25963 * @param string $user_email
25964 * @param string $license_key
25965 * @param array $plugin_ids
25966 * @param string|null $license_key
25967 *
25968 * @return array|false
25969 */
25970 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
25971 $request = array(
25972 'method' => 'POST',
25973 'body' => array(),
25974 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
25975 );
25976
25977 if ( is_string( $user_email ) ) {
25978 $request['body']['email'] = $user_email;
25979 } else {
25980 $request['body']['license_key'] = $license_key;
25981 }
25982
25983 $result = array();
25984
25985 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
25986 $total_plugin_ids = count( $plugin_ids );
25987
25988 $plugin_ids_count_per_request = 10;
25989 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
25990 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
25991
25992 $request['body']['plugin_ids'] = $plugin_ids_set;
25993
25994 $response = self::safe_remote_post(
25995 $url,
25996 $request,
25997 WP_FS__TIME_24_HOURS_IN_SEC,
25998 WP_FS__TIME_12_HOURS_IN_SEC
25999 );
26000
26001 if ( ! is_wp_error( $response ) ) {
26002 $decoded = is_string( $response['body'] ) ?
26003 json_decode( $response['body'] ) :
26004 null;
26005
26006 if (
26007 !is_object($decoded) ||
26008 !isset($decoded->success) ||
26009 true !== $decoded->success ||
26010 !isset( $decoded->data ) ||
26011 !is_array( $decoded->data )
26012 ) {
26013 return false;
26014 }
26015
26016 $result = array_merge( $result, $decoded->data );
26017 }
26018 }
26019
26020 return $result;
26021 }
26022
26023 /**
26024 * @author Leo Fajardo (@leorw)
26025 * @since 2.1.0
26026 */
26027 function _maybe_show_gdpr_admin_notice() {
26028 if ( ! $this->is_user_in_admin() ) {
26029 return;
26030 }
26031
26032 if ( ! $this->should_handle_gdpr_admin_notice() ) {
26033 return;
26034 }
26035
26036 if ( ! $this->is_user_admin() ) {
26037 return;
26038 }
26039
26040 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26041
26042 $lock = FS_User_Lock::instance();
26043
26044 /**
26045 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
26046 */
26047 if ( ! $lock->try_lock( 60 ) ) {
26048 return;
26049 }
26050
26051 /**
26052 * @var $current_wp_user WP_User
26053 */
26054 $current_wp_user = self::_get_current_wp_user();
26055
26056 /**
26057 * @var FS_User $current_fs_user
26058 */
26059 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
26060
26061 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
26062
26063 if ( ! is_object( $current_fs_user ) ) {
26064 // 10-year lock.
26065 $lock->lock( $ten_years_in_sec );
26066
26067 return;
26068 }
26069
26070 $gdpr = FS_GDPR_Manager::instance();
26071
26072 if ( $gdpr->is_opt_in_notice_shown() ) {
26073 // 30-day lock.
26074 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26075
26076 return;
26077 }
26078
26079 if ( ! $gdpr->should_show_opt_in_notice() ) {
26080 // 10-year lock.
26081 $lock->lock( $ten_years_in_sec );
26082
26083 return;
26084 }
26085
26086 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
26087 $was_notice_shown_before = ( false !== $last_time_notice_shown );
26088
26089 if ( $was_notice_shown_before &&
26090 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
26091 ) {
26092 // If the notice was shown before, show it again after 30 days from the last time it was shown.
26093 return;
26094 }
26095
26096 /**
26097 * Find all plugin IDs that were installed by the current admin.
26098 */
26099 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
26100
26101 if ( empty( $plugin_ids_map )) {
26102 $lock->lock( $ten_years_in_sec );
26103
26104 return;
26105 }
26106
26107 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26108 $current_fs_user->email,
26109 null,
26110 array_keys( $plugin_ids_map )
26111 );
26112
26113 if ( empty( $user_plugins ) ) {
26114 $lock->lock(
26115 is_array($user_plugins) ?
26116 $ten_years_in_sec :
26117 // Lock for 24-hours on errors.
26118 WP_FS__TIME_24_HOURS_IN_SEC
26119 );
26120
26121 return;
26122 }
26123
26124 $has_unset_marketing_optin = false;
26125
26126 foreach ( $user_plugins as $user_plugin ) {
26127 if ( true == $user_plugin->is_marketing_allowed ) {
26128 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
26129 }
26130
26131 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
26132 $has_unset_marketing_optin = true;
26133 }
26134 }
26135
26136 if ( empty( $plugin_ids_map ) ||
26137 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
26138 ) {
26139 $lock->lock( $ten_years_in_sec );
26140
26141 return;
26142 }
26143
26144 $modules = array_merge(
26145 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26146 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26147 );
26148
26149 foreach ( $modules as $module ) {
26150 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
26151 $plugin_ids_map[ $module->id ] = $module;
26152 }
26153 }
26154
26155 $plugin_title = null;
26156 if ( 1 === count( $plugin_ids_map ) ) {
26157 $module = reset( $plugin_ids_map );
26158 $plugin_title = $module->title;
26159 }
26160
26161 $gdpr->add_opt_in_sticky_notice(
26162 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
26163 $plugin_title
26164 );
26165
26166 $this->add_gdpr_optin_ajax_handler_and_style();
26167
26168 $gdpr->notice_was_just_shown();
26169
26170 // 30-day lock.
26171 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26172 }
26173
26174 /**
26175 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
26176 * marketing.
26177 *
26178 * @author Leo Fajardo (@leorw)
26179 * @since 2.1.0
26180 */
26181 private function disable_opt_in_notice_and_lock_user() {
26182 FS_GDPR_Manager::instance()->disable_opt_in_notice();
26183
26184 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26185
26186 // 10-year lock.
26187 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26188 }
26189
26190 /**
26191 * @author Leo Fajardo (@leorw)
26192 * @since 2.5.4
26193 */
26194 static function _add_api_connectivity_notice_handler_js() {
26195 fs_require_once_template( 'api-connectivity-message-js.php' );
26196 }
26197
26198 /**
26199 * @author Leo Fajardo (@leorw)
26200 * @since 2.1.0
26201 */
26202 function _add_gdpr_optin_js() {
26203 $vars = array( 'id' => $this->_module_id );
26204
26205 fs_require_once_template( 'gdpr-optin-js.php', $vars );
26206 }
26207
26208 /**
26209 * @author Leo Fajardo (@leorw)
26210 * @since 2.1.0
26211 */
26212 function enqueue_gdpr_optin_notice_style() {
26213 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
26214 }
26215
26216 /**
26217 * @author Leo Fajardo (@leorw)
26218 * @since 2.1.0
26219 */
26220 function _maybe_add_gdpr_optin_ajax_handler() {
26221 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
26222
26223 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
26224 $this->add_gdpr_optin_ajax_handler_and_style();
26225 }
26226 }
26227
26228 /**
26229 * @author Leo Fajardo (@leorw)
26230 * @since 2.1.0
26231 */
26232 function _fetch_is_marketing_required_flag_value_ajax_action() {
26233 $this->_logger->entrance();
26234
26235 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
26236
26237 $license_key = fs_request_get_raw( 'license_key' );
26238
26239 if ( empty($license_key) ) {
26240 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
26241 }
26242
26243 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26244 null,
26245 $license_key,
26246 array( $this->_module_id )
26247 );
26248
26249 if ( ! is_array( $user_plugins ) ||
26250 empty($user_plugins) ||
26251 !isset($user_plugins[0]->plugin_id) ||
26252 $user_plugins[0]->plugin_id != $this->_module_id
26253 ) {
26254 /**
26255 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
26256 *
26257 * @author Vova Feldman (@svovaf)
26258 */
26259 self::shoot_ajax_success( array(
26260 'is_marketing_allowed' => null,
26261 'license_owner_id' => null
26262 ) );
26263 }
26264
26265 self::shoot_ajax_success( array(
26266 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
26267 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
26268 ) );
26269 }
26270
26271 /**
26272 * @author Leo Fajardo (@leorw)
26273 * @since 2.3.2
26274 *
26275 * @param number[] $install_ids
26276 *
26277 * @return array {
26278 * An array of objects containing the installs' licenses owners data.
26279 *
26280 * @property number $id User ID.
26281 * @property string $email User email (can be masked email).
26282 * }
26283 */
26284 private function fetch_installs_licenses_owners_data( $install_ids ) {
26285 $this->_logger->entrance();
26286
26287 $response = $this->get_api_user_scope()->get(
26288 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
26289 );
26290
26291 $license_owners = array();
26292
26293 if ( $this->is_api_result_object( $response, 'owners' ) ) {
26294 $license_owners = $response->owners;
26295 }
26296
26297 return $license_owners;
26298 }
26299
26300 /**
26301 * @author Leo Fajardo (@leorw)
26302 * @since 2.1.0
26303 */
26304 private function add_gdpr_optin_ajax_handler_and_style() {
26305 // Add GDPR action AJAX callback.
26306 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
26307
26308 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
26309 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
26310 }
26311
26312 /**
26313 * @author Leo Fajardo (@leorw)
26314 * @since 2.1.0
26315 */
26316 function _gdpr_optin_ajax_action() {
26317 $this->_logger->entrance();
26318
26319 $this->check_ajax_referer( 'gdpr_optin_action' );
26320
26321 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
26322 self::shoot_ajax_failure();
26323 }
26324
26325 $current_wp_user = self::_get_current_wp_user();
26326
26327 $plugin_ids = fs_request_get( 'plugin_ids', array() );
26328 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
26329 self::shoot_ajax_failure();
26330 }
26331
26332 $modules = array_merge(
26333 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26334 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26335 );
26336
26337 foreach ( $modules as $key => $module ) {
26338 if ( ! in_array( $module->id, $plugin_ids ) ) {
26339 unset( $modules[ $key ] );
26340 }
26341 }
26342
26343 if ( empty( $modules ) ) {
26344 self::shoot_ajax_failure();
26345 }
26346
26347 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
26348
26349 foreach ( $modules as $module ) {
26350 $user_api->call( "?plugin_id={$module->id}", 'put', array(
26351 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
26352 ) );
26353 }
26354
26355 FS_GDPR_Manager::instance()->remove_opt_in_notice();
26356
26357 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26358
26359 // 10-year lock.
26360 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26361
26362 self::shoot_ajax_success();
26363 }
26364
26365 /**
26366 * Checks if the GDPR admin notice should be handled. By default, this logic is off, unless the integrator adds the special 'handle_gdpr_admin_notice' filter.
26367 *
26368 * @author Vova Feldman (@svovaf)
26369 * @since 2.1.0
26370 *
26371 * @return bool
26372 */
26373 private function should_handle_gdpr_admin_notice() {
26374 return $this->apply_filters(
26375 'handle_gdpr_admin_notice',
26376 // Default to false.
26377 false
26378 );
26379 }
26380
26381 #endregion
26382
26383 #----------------------------------------------------------------------------------
26384 #region Marketing
26385 #----------------------------------------------------------------------------------
26386
26387 /**
26388 * Check if current user purchased any other plugins before.
26389 *
26390 * @author Vova Feldman (@svovaf)
26391 * @since 1.0.9
26392 *
26393 * @return bool
26394 */
26395 function has_purchased_before() {
26396 // TODO: Implement has_purchased_before() method.
26397 throw new Exception( 'not implemented' );
26398 }
26399
26400 /**
26401 * Check if current user classified as an agency.
26402 *
26403 * @author Vova Feldman (@svovaf)
26404 * @since 1.0.9
26405 *
26406 * @return bool
26407 */
26408 function is_agency() {
26409 // TODO: Implement is_agency() method.
26410 throw new Exception( 'not implemented' );
26411 }
26412
26413 /**
26414 * Check if current user classified as a developer.
26415 *
26416 * @author Vova Feldman (@svovaf)
26417 * @since 1.0.9
26418 *
26419 * @return bool
26420 */
26421 function is_developer() {
26422 // TODO: Implement is_developer() method.
26423 throw new Exception( 'not implemented' );
26424 }
26425
26426 /**
26427 * Check if current user classified as a business.
26428 *
26429 * @author Vova Feldman (@svovaf)
26430 * @since 1.0.9
26431 *
26432 * @return bool
26433 */
26434 function is_business() {
26435 // TODO: Implement is_business() method.
26436 throw new Exception( 'not implemented' );
26437 }
26438
26439 #endregion
26440
26441 #----------------------------------------------------------------------------------
26442 #region Helper
26443 #----------------------------------------------------------------------------------
26444
26445 /**
26446 * If running with a secret key, assume it's the developer and show pending plans as well.
26447 *
26448 * @author Vova Feldman (@svovaf)
26449 * @since 2.1.2
26450 *
26451 * @param string $path
26452 *
26453 * @return string
26454 */
26455 function add_show_pending( $path ) {
26456 if ( ! $this->has_secret_key() ) {
26457 return $path;
26458 }
26459
26460 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
26461 }
26462
26463 #endregion
26464 }
26465