PluginProbe
Adminify – White Label, Admin Menu Editor, Login Customizer / 3.1.7
Adminify – White Label, Admin Menu Editor, Login Customizer v3.1.7
4.3.2 4.3.1 4.3.0 4.2.26 4.2.25 4.2.24 4.2.23 4.2.22 4.2.21 4.2.20 4.2.19 4.2.18 4.2.17 4.2.16 4.2.15 4.2.14 4.2.13 4.2.12 4.2.11 4.2.10 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 All 165 releases
adminify / Libs / freemius / includes / class-freemius.php

class-freemius.php in Adminify – White Label, Admin Menu Editor, Login Customizer 3.1.7, at Libs/freemius/includes/class-freemius.php

26,430 lines 966.6 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 // @todo - Remove this check once the garbage collector is ready to be out of beta.
1361 if ( true !== fs_get_optional_constant( 'WP_FS__ENABLE_GARBAGE_COLLECTOR', false ) ) {
1362 return;
1363 }
1364
1365 if ( ! $this->is_user_in_admin() ) {
1366 return;
1367 }
1368
1369 require_once WP_FS__DIR_INCLUDES . '/class-fs-lock.php';
1370
1371 $lock = new FS_Lock( 'garbage_collection' );
1372
1373 if ( $lock->is_locked() ) {
1374 return;
1375 }
1376
1377 // Create a 1-day lock.
1378 $lock->lock( WP_FS__TIME_24_HOURS_IN_SEC );
1379
1380 FS_Garbage_Collector::instance()->clean();
1381 }
1382
1383 /**
1384 * Opens the support forum subemenu item in a new browser page.
1385 *
1386 * @author Vova Feldman (@svovaf)
1387 * @since 2.1.4
1388 */
1389 static function _open_support_forum_in_new_page() {
1390 ?>
1391 <script type="text/javascript">
1392 (function ($) {
1393 $('.fs-submenu-item.wp-support-forum').parent().attr( { target: '_blank', rel: 'noopener noreferrer' } );
1394 })(jQuery);
1395 </script>
1396 <?php
1397 }
1398
1399 /**
1400 * @author Vova Feldman (@svovaf)
1401 * @since 1.0.9
1402 */
1403 private function register_constructor_hooks() {
1404 $this->_logger->entrance();
1405
1406 if ( is_admin() ) {
1407 add_action( 'admin_init', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
1408
1409 if ( $this->is_plugin() ) {
1410 if ( self::is_plugin_install_page() && true !== fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) {
1411 /**
1412 * Unless the `fs_allow_updater_and_dialog` URL param exists and its value is `true`, make
1413 * Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php)
1414 * so that they won't interfere with the .org plugins' functionalities on that page (e.g.
1415 * updating of a .org plugin).
1416 */
1417 add_filter( 'site_transient_update_plugins', array( 'Freemius', '_remove_fs_updates_from_plugin_install_page' ), 10, 2 );
1418 } else if ( self::is_plugins_page() || self::is_updates_page() ) {
1419 /**
1420 * 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.
1421 *
1422 * @author Leo Fajardo (@leorw)
1423 * @since 2.2.3
1424 */
1425 add_action( 'admin_footer', array( 'Freemius', '_prepend_fs_allow_updater_and_dialog_flag_url_param' ) );
1426 }
1427
1428 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
1429
1430 /**
1431 * @since 1.2.2
1432 *
1433 * Hook to both free and premium version activations to support
1434 * auto deactivation on the other version activation.
1435 */
1436 register_activation_hook(
1437 $plugin_dir . $this->_free_plugin_basename,
1438 array( &$this, '_activate_plugin_event_hook' )
1439 );
1440
1441 register_activation_hook(
1442 $plugin_dir . $this->premium_plugin_basename(),
1443 array( &$this, '_activate_plugin_event_hook' )
1444 );
1445 } else {
1446 add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
1447
1448 add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
1449 }
1450
1451 /**
1452 * Part of the mechanism to identify new plugin install vs. plugin update.
1453 *
1454 * @author Vova Feldman (@svovaf)
1455 * @since 1.1.9
1456 */
1457 if ( empty( $this->_storage->was_plugin_loaded ) ) {
1458 /**
1459 * During the plugin activation (not theme), 'plugins_loaded' will be already executed
1460 * when the logic gets here since the activation logic first add the activate plugins,
1461 * then triggers 'plugins_loaded', and only then include the code of the plugin that
1462 * is activated. Which means that _plugins_loaded() will NOT be executed during the
1463 * plugin activation, and that IS intentional.
1464 *
1465 * @author Vova Feldman (@svovaf)
1466 */
1467 if ( $this->is_plugin() &&
1468 $this->is_activation_mode( false ) &&
1469 0 == did_action( 'plugins_loaded' )
1470 ) {
1471 add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
1472 } else {
1473 // If was activated before, then it was already loaded before.
1474 $this->_plugins_loaded();
1475 }
1476 }
1477
1478 add_action( 'plugins_loaded', array( &$this, '_run_garbage_collector' ) );
1479
1480 if ( ! self::is_ajax() ) {
1481 if ( ! $this->is_addon() ) {
1482 add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
1483 }
1484 }
1485
1486 if ( $this->_storage->handle_gdpr_admin_notice ) {
1487 add_action( 'init', array( &$this, '_maybe_show_gdpr_admin_notice' ) );
1488 }
1489
1490 add_action( 'init', array( &$this, '_maybe_add_gdpr_optin_ajax_handler') );
1491 add_action( 'init', array( &$this, '_maybe_add_pricing_ajax_handler' ) );
1492 }
1493
1494 if ( $this->is_plugin() ) {
1495 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1496 add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
1497 } else {
1498 add_action( 'wp_initialize_site', array( $this, '_after_wp_initialize_site_callback' ), 11, 2 );
1499 }
1500
1501 register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
1502 }
1503
1504 if ( is_multisite() ) {
1505 add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
1506 add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) );
1507 add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) );
1508
1509 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1510 add_action( 'deleted_blog', array( $this, '_after_site_deleted_callback' ), 10, 2 );
1511 } else {
1512 add_action( 'wp_delete_site', array( $this, '_after_wpsite_deleted_callback' ) );
1513 }
1514
1515 add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) );
1516 add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) );
1517 add_action( 'make_ham_blog', array( &$this, '_after_site_reactivated_callback' ) );
1518 }
1519
1520 if ( $this->is_theme() &&
1521 self::is_customizer() &&
1522 $this->apply_filters( 'show_customizer_upsell', true )
1523 ) {
1524 // Register customizer upsell.
1525 add_action( 'customize_register', array( &$this, '_customizer_register' ) );
1526 }
1527
1528 add_action( 'admin_init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
1529
1530 if ( $this->is_theme() && ! $this->is_migration() ) {
1531 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1532 }
1533
1534 add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
1535 add_action( 'admin_init', array( &$this, '_add_premium_version_upgrade_selection' ) );
1536 add_action( 'admin_init', array( &$this, '_add_beta_mode_update_handler' ) );
1537 add_action( 'admin_init', array( &$this, '_add_user_change_option' ) );
1538 add_action( 'admin_init', array( &$this, '_add_email_address_update_option' ) );
1539
1540 $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
1541 $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
1542 $this->add_ajax_action( 'set_data_debug_mode', array( &$this, '_set_data_debug_mode' ) );
1543 $this->add_ajax_action( 'toggle_whitelabel_mode', array( &$this, '_toggle_whitelabel_mode_ajax_handler' ) );
1544
1545 if ( $this->_is_network_active && fs_is_network_admin() ) {
1546 $this->add_ajax_action( 'network_activate', array( &$this, '_network_activate_ajax_action' ) );
1547 }
1548
1549 $this->add_ajax_action( 'install_premium_version', array(
1550 &$this,
1551 '_install_premium_version_ajax_action'
1552 ) );
1553
1554 $this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
1555
1556 $this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
1557
1558 $this->add_action( 'sdk_version_update', array( &$this, '_sdk_version_update' ), WP_FS__DEFAULT_PRIORITY, 2 );
1559
1560 $this->add_action(
1561 'plugin_version_update',
1562 array( &$this, '_after_version_update' ),
1563 WP_FS__DEFAULT_PRIORITY,
1564 2
1565 );
1566 $this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
1567
1568 add_action( 'admin_init', array( &$this, '_add_trial_notice' ) ); // @phpstan-ignore-line
1569 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) ); // @phpstan-ignore-line
1570 add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) );
1571
1572 /**
1573 * Handle request to reset anonymous mode for `get_reconnect_url()` or reset the pending activation mode.
1574 *
1575 * @author Vova Feldman (@svovaf)
1576 * @since 1.2.1.5
1577 */
1578 if (
1579 (
1580 fs_request_is_action( 'reset_anonymous_mode' ) ||
1581 fs_request_is_action( 'reset_pending_activation_mode' )
1582 ) &&
1583 $this->get_unique_affix() === fs_request_get_raw( 'fs_unique_affix' )
1584 ) {
1585 add_action( 'admin_init', array( &$this, 'connect_again' ) );
1586 }
1587 }
1588
1589 /**
1590 * Register the required hooks right after the settings parse is completed.
1591 *
1592 * @author Vova Feldman (@svovaf)
1593 * @since 2.3.1
1594 */
1595 private function register_after_settings_parse_hooks() {
1596 if ( is_admin() &&
1597 $this->is_theme() &&
1598 $this->is_premium() &&
1599 ! $this->has_active_valid_license()
1600 ) {
1601 $this->add_ajax_action(
1602 'delete_theme_update_data',
1603 array( &$this, '_delete_theme_update_data_action' )
1604 );
1605 }
1606
1607 if ( $this->show_settings_with_tabs() ) {
1608 /**
1609 * Include the required hooks to capture the theme settings' page tabs
1610 * and cache them.
1611 *
1612 * @author Vova Feldman (@svovaf)
1613 * @since 1.2.2.7
1614 */
1615 if ( ! $this->_cache->has_valid( 'tabs' ) ) {
1616 add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
1617 // Add license activation AJAX callback.
1618 $this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
1619
1620 add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
1621 }
1622
1623 add_action(
1624 'admin_footer',
1625 array( &$this, '_add_freemius_tabs' ),
1626 /**
1627 * The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
1628 * That's why the priority is 11 while the tabs capture logic is added
1629 * with priority 10.
1630 *
1631 * @author Vova Feldman (@svovaf)
1632 */
1633 11
1634 );
1635 }
1636
1637 if ( ! self::is_ajax() ) {
1638 if ( ! $this->is_addon() || $this->is_only_premium() ) {
1639 add_action(
1640 ( $this->_is_network_active && fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu',
1641 array( &$this, '_prepare_admin_menu' ),
1642 WP_FS__LOWEST_PRIORITY
1643 );
1644 }
1645 }
1646 }
1647
1648 /**
1649 * Makes Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php) so that
1650 * they won't interfere with the .org plugins' functionalities on that page (e.g. updating of a .org plugin).
1651 *
1652 * @author Leo Fajardo (@leorw)
1653 * @since 2.2.3
1654 *
1655 * @param object $updates
1656 * @param string|null $transient
1657 *
1658 * @return object
1659 */
1660 static function _remove_fs_updates_from_plugin_install_page( $updates, $transient = null ) {
1661 if ( is_object( $updates ) && isset( $updates->response ) ) {
1662 foreach ( $updates->response as $file => $plugin ) {
1663 if ( isset( $plugin->package ) && false !== strpos( $plugin->package, 'api.freemius' ) ) {
1664 unset( $updates->response[ $file ] );
1665 }
1666 }
1667 }
1668
1669 return $updates;
1670 }
1671
1672 /**
1673 * Prepends the `fs_allow_updater_and_dialog` param to the plugin information URLs to tell the SDK to handle
1674 * the information that is shown on the plugin details dialog that is shown when the relevant link is clicked.
1675 *
1676 * @author Leo Fajardo (@leorw)
1677 * @since 2.2.3
1678 *
1679 * @return void
1680 */
1681 static function _prepend_fs_allow_updater_and_dialog_flag_url_param() {
1682 $slug_basename_map = array();
1683 foreach ( self::$_instances as $instance ) {
1684 if ( ! $instance->is_plugin() ) {
1685 continue;
1686 }
1687
1688 $slug_basename_map[ $instance->get_slug() ] = $instance->premium_plugin_basename();
1689 }
1690 ?>
1691 <script type="text/javascript">
1692 (function( $ ) {
1693 var slugBasenameMap = <?php echo json_encode( $slug_basename_map ) ?>;
1694 for ( var slug in slugBasenameMap ) {
1695 var basename = slugBasenameMap[ slug ];
1696
1697 // Try to get the plugin rows if on the "Plugins" page.
1698 var $pluginRows = $( '.wp-list-table.plugins tr[data-plugin="' + basename + '"]');
1699
1700 if ( 0 === $pluginRows.length ) {
1701 // Try to get the plugin rows if on the "Updates" page.
1702 var $pluginCheckbox = $( '#update-plugins-table input[type="checkbox"][value="' + basename + '"]' );
1703 if ( 0 !== $pluginCheckbox.length ) {
1704 $pluginRows = $pluginCheckbox.parents( 'tr:first' );
1705 }
1706 }
1707
1708 if ( 0 === $pluginRows.length ) {
1709 // No plugin rows found.
1710 continue;
1711 }
1712
1713 // Find the "View details" links and add the `fs_allow_updater_and_dialog` param to the URL.
1714 $pluginRows.find( 'a[href*="plugin-install.php?tab=plugin-information"]' ).each(function() {
1715 var $this = $( this ),
1716 href = $this.attr( 'href' ).replace( '?tab=', '?fs_allow_updater_and_dialog=true&tab=');
1717
1718 $this.attr( 'href', href );
1719 });
1720 }
1721 })( jQuery );
1722 </script>
1723 <?php
1724 }
1725
1726 /**
1727 * @author Leo Fajardo (@leorw)
1728 * @since 2.3.0
1729 */
1730 static function _maybe_add_beta_label_styles() {
1731 $has_any_beta_version = false;
1732
1733 foreach ( self::$_instances as $instance ) {
1734 if ( $instance->is_beta() ) {
1735 $has_any_beta_version = true;
1736 break;
1737 }
1738 }
1739
1740 if ( $has_any_beta_version ) {
1741 fs_enqueue_local_style( 'fs_plugins', '/admin/plugins.css' );
1742 }
1743 }
1744
1745 /**
1746 * @author Leo Fajardo (@leorw)
1747 * @since 2.3.0
1748 */
1749 static function _maybe_add_beta_label_to_plugins_and_handle_confirmation() {
1750 $beta_data = array();
1751
1752 foreach ( self::$_instances as $instance ) {
1753 if ( ! $instance->is_premium() ) {
1754 continue;
1755 }
1756
1757 /**
1758 * If there's an available beta version update, a confirmation message will be shown when the
1759 * "Update now" link on the "Plugins" or "Themes" page is clicked.
1760 */
1761 $has_beta_update = $instance->has_beta_update();
1762
1763 $is_beta = (
1764 // The "Beta" label is added separately for themes.
1765 $instance->is_plugin() &&
1766 $instance->is_beta()
1767 );
1768
1769 if ( ! $is_beta && ! $has_beta_update ) {
1770 continue;
1771 }
1772
1773 $beta_data[ $instance->get_plugin_basename() ] = array( 'is_installed_version_beta' => $is_beta );
1774
1775 if ( ! $has_beta_update ) {
1776 continue;
1777 }
1778
1779 $beta_data[ $instance->get_plugin_basename() ]['beta_version_update_confirmation_message'] = sprintf(
1780 '%s %s',
1781 sprintf(
1782 fs_esc_attr_inline(
1783 '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.',
1784 'beta-version-update-caution',
1785 $instance->get_slug()
1786 ),
1787 $instance->get_plugin_title()
1788 ),
1789 fs_esc_attr_inline( 'Would you like to proceed with the update?', 'update-confirmation', $instance->get_slug() )
1790 );
1791 }
1792
1793 if ( empty( $beta_data ) ) {
1794 return;
1795 }
1796 ?>
1797 <script type="text/javascript">
1798 ( function( $ ) {
1799 var betaData = <?php echo json_encode( $beta_data ) ?>;
1800
1801 for ( var pluginBasename in betaData ) {
1802 if ( ! betaData.hasOwnProperty( pluginBasename ) ) {
1803 continue;
1804 }
1805
1806 if ( ! betaData[ pluginBasename ].is_installed_version_beta ) {
1807 continue;
1808 }
1809
1810 var $parentContainer = $( '.wp-list-table.plugins tr[data-plugin="' + pluginBasename + '"]' );
1811 if ( 0 === $parentContainer.length ) {
1812 continue;
1813 }
1814
1815 $parentContainer.find( '.plugin-title > strong:first-child').append(
1816 '<span class="fs-tag fs-info"><?php fs_esc_js_echo_inline( 'Beta', 'beta' ) ?></span>'
1817 );
1818 }
1819
1820 setTimeout( function() {
1821 // Wait a little bit before adding the event handler, otherwise, it will be overridden by the core WP logic.
1822 $( '.plugins .update-message .update-link, .themes .theme .update-message' ).on( 'click', function() {
1823 var $parentContainer = $( this ).parents( 'tr:first' );
1824 pluginBasename = ( 0 !== $parentContainer.length ) ?
1825 $parentContainer.data( 'plugin' ) :
1826 $( this ).parents( '.theme:first' ).data( 'slug' );
1827
1828 if (
1829 betaData[ pluginBasename ] &&
1830 betaData[ pluginBasename ].beta_version_update_confirmation_message &&
1831 ! confirm( betaData[ pluginBasename ].beta_version_update_confirmation_message )
1832 ) {
1833 return false;
1834 }
1835 } );
1836 }, 20 );
1837 } )( jQuery );
1838 </script>
1839 <?php
1840 }
1841
1842 /**
1843 * Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
1844 * could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
1845 * plugin will trigger inclusion of the free or premium version and if one of them is active during the
1846 * uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
1847 *
1848 * @author Leo Fajardo (@leorw)
1849 *
1850 * @since 1.2.0
1851 */
1852 private function unregister_uninstall_hook() {
1853 $uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
1854 unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
1855 unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
1856
1857 update_option( 'uninstall_plugins', $uninstallable_plugins );
1858 }
1859
1860 /**
1861 * @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
1862 *
1863 * @param bool $store_prev_path
1864 */
1865 private function clear_module_main_file_cache( $store_prev_path = true ) {
1866 if ( ! isset( $this->_storage->plugin_main_file ) ||
1867 empty( $this->_storage->plugin_main_file->path )
1868 ) {
1869 return;
1870 }
1871
1872 if ( ! $store_prev_path ) {
1873 /**
1874 * Storing the previous path is not needed when clearing the cache after an SDK version update since
1875 * the main purpose of the cache clearing in that event is to correct a wrong plugin main file path
1876 * which causes data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
1877 *
1878 * @author Leo Fajardo (@leorw)
1879 * @since 2.2.1
1880 */
1881 unset( $this->_storage->plugin_main_file->path );
1882 } else {
1883 $plugin_main_file = clone $this->_storage->plugin_main_file;
1884
1885 // Store cached path (2nd layer cache).
1886 $plugin_main_file->prev_path = $plugin_main_file->path;
1887
1888 // Clear cached path.
1889 unset( $plugin_main_file->path );
1890
1891 $this->_storage->plugin_main_file = $plugin_main_file;
1892 }
1893
1894 /**
1895 * Clear global cached path.
1896 *
1897 * @author Leo Fajardo (@leorw)
1898 * @since 1.2.2
1899 */
1900 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
1901 unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
1902 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
1903 }
1904
1905 /**
1906 * @author Leo Fajardo (@leorw)
1907 * @since 2.0.0
1908 */
1909 function _hook_action_links_and_register_account_hooks() {
1910 if ( $this->is_migration() ) {
1911 return;
1912 }
1913
1914 if (
1915 ( self::is_plugins_page() && $this->is_plugin() ) ||
1916 ( self::is_themes_page() && $this->is_theme() ) ||
1917 fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' )
1918 ) {
1919 $this->_add_tracking_links();
1920 }
1921
1922 if ( self::is_plugins_page() && $this->is_plugin() ) {
1923 $this->hook_plugin_action_links();
1924 }
1925
1926 $this->_register_account_hooks();
1927 }
1928
1929 /**
1930 * @author Vova Feldman (@svovaf)
1931 * @since 1.0.9
1932 */
1933 private function _register_account_hooks() {
1934 if ( ! is_admin() ) {
1935 return;
1936 }
1937
1938 /**
1939 * Always show the deactivation feedback form since we added
1940 * automatic free version deactivation upon premium code activation.
1941 *
1942 * @since 1.2.1.6
1943 */
1944 $this->add_ajax_action(
1945 'submit_uninstall_reason',
1946 array( &$this, '_submit_uninstall_reason_action' )
1947 );
1948
1949 $this->add_ajax_action(
1950 'cancel_subscription_or_trial',
1951 array( &$this, 'cancel_subscription_or_trial_ajax_action' )
1952 );
1953
1954 if ( ! $this->is_addon() || $this->is_parent_plugin_installed() ) {
1955 if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
1956 ( $this->is_theme() && self::is_themes_page() )
1957 ) {
1958 add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
1959 }
1960 }
1961 }
1962
1963 /**
1964 * Leverage backtrace to find caller plugin file path.
1965 *
1966 * @param bool $is_init Is initiation sequence.
1967 * @param string $main_file Since 2.5.0 expects the module's main file path to potentially purge the cached path.
1968 *
1969 * @return string
1970 * @since 1.0.6
1971 *
1972 * @author Vova Feldman (@svovaf)
1973 */
1974 private function _find_caller_plugin_file( $is_init = false, $main_file = '' ) {
1975 // Try to load the cached value of the file path.
1976 if ( isset( $this->_storage->plugin_main_file ) ) {
1977 $plugin_main_file = $this->_storage->plugin_main_file;
1978 if ( ! empty( $plugin_main_file->path ) ) {
1979 $absolute_path = $this->get_absolute_path( $plugin_main_file->path );
1980 if ( file_exists( $absolute_path ) ) {
1981 if ( $is_init && $absolute_path !== $this->get_absolute_path( $main_file ) ) {
1982 // Update cached path if not matching the actual path.
1983 $plugin_main_file->path = $main_file;
1984 $this->_storage->plugin_main_file = $plugin_main_file;
1985 }
1986
1987 return $absolute_path;
1988 }
1989 }
1990 }
1991
1992 /**
1993 * @since 1.2.1
1994 *
1995 * `clear_module_main_file_cache()` is clearing the plugin's cached path on
1996 * deactivation. Therefore, if any plugin/theme was initiating `Freemius`
1997 * with that plugin's slug, it was overriding the empty plugin path with a wrong path.
1998 *
1999 * So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
2000 * when the class instantiator isn't the module.
2001 */
2002 if ( ! $is_init ) {
2003 // Fetch prev path cache.
2004 if ( isset( $this->_storage->plugin_main_file ) &&
2005 ! empty( $this->_storage->plugin_main_file->prev_path )
2006 ) {
2007 $absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
2008 if ( file_exists( $absolute_path ) ) {
2009 return $absolute_path;
2010 }
2011 }
2012
2013 wp_die(
2014 $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' ) .
2015 " Module: {$this->_slug}; SDK: " . WP_FS__SDK_VERSION . ";",
2016 $this->get_text_inline( 'Error', 'error' ),
2017 array( 'back_link' => true )
2018 );
2019 }
2020
2021 /**
2022 * @since 1.2.1
2023 *
2024 * Only the original instantiator that calls dynamic_init can modify the module's path.
2025 */
2026 // Find caller module.
2027 $this->_storage->plugin_main_file = (object) array(
2028 'path' => $main_file,
2029 );
2030
2031 return $this->get_absolute_path( $main_file );
2032 }
2033
2034 /**
2035 * @author Leo Fajardo (@leorw)
2036 * @since 1.2.3
2037 *
2038 * @param string $path
2039 *
2040 * @return string
2041 */
2042 private function get_relative_path( $path ) {
2043 $module_root_dir = $this->get_module_root_dir_path();
2044 if ( 0 === strpos( $path, $module_root_dir ) ) {
2045 $path = substr( $path, strlen( $module_root_dir ) );
2046 }
2047
2048 return $path;
2049 }
2050
2051 /**
2052 * @author Leo Fajardo (@leorw)
2053 * @since 1.2.3
2054 *
2055 * @param string $path
2056 * @param string|bool $module_type
2057 *
2058 * @return string
2059 */
2060 private function get_absolute_path( $path, $module_type = false ) {
2061 $module_root_dir = $this->get_module_root_dir_path( $module_type );
2062 if ( 0 !== strpos( $path, $module_root_dir ) ) {
2063 $path = fs_normalize_path( $module_root_dir . $path );
2064 }
2065
2066 return $path;
2067 }
2068
2069 /**
2070 * @author Leo Fajardo (@leorw)
2071 * @since 1.2.3
2072 *
2073 * @param string|bool $module_type
2074 *
2075 * @return string
2076 */
2077 private function get_module_root_dir_path( $module_type = false ) {
2078 $is_plugin = empty( $module_type ) ?
2079 $this->is_plugin() :
2080 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
2081
2082 return fs_normalize_path( trailingslashit( $is_plugin ?
2083 WP_PLUGIN_DIR :
2084 get_theme_root( get_stylesheet() ) ) );
2085 }
2086
2087 /**
2088 * @author Leo Fajardo (@leorw)
2089 *
2090 * @param number $module_id
2091 * @param string $slug
2092 *
2093 * @return string Since 2.5.0 return the module's main file path.
2094 *
2095 * @since 1.2.2
2096 */
2097 private function store_id_slug_type_path_map( $module_id, $slug ) {
2098 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
2099
2100 $store_option = false;
2101
2102 if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
2103 $id_slug_type_path_map[ $module_id ] = array(
2104 'slug' => $slug
2105 );
2106
2107 $store_option = true;
2108 } else if (
2109 isset( $id_slug_type_path_map[ $module_id ]['slug'] ) &&
2110 $slug !== $id_slug_type_path_map[ $module_id ]['slug']
2111 ) {
2112 $id_slug_type_path_map[ $module_id ]['slug'] = $slug;
2113 $store_option = true;
2114 }
2115
2116 $find_caller = empty( $id_slug_type_path_map[ $module_id ]['path'] );
2117
2118 if ( ! $find_caller ) {
2119 /**
2120 * This verification is for cases when suddenly the same module
2121 * is installed but with a different folder name.
2122 *
2123 * @author Vova Feldman (@svovaf)
2124 * @since 1.2.3
2125 */
2126 $find_caller = ! file_exists( $this->get_absolute_path(
2127 $id_slug_type_path_map[ $module_id ]['path'],
2128 $id_slug_type_path_map[ $module_id ]['type']
2129 ) );
2130 }
2131
2132 foreach ( $id_slug_type_path_map as $id => $data ) {
2133 if ( empty( $id ) ) {
2134 // Remove maps with empty module ID.
2135 unset( $id_slug_type_path_map[ $id ] );
2136 $store_option = true;
2137 continue;
2138 }
2139
2140 /**
2141 * 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.
2142 *
2143 * @author Vova Feldman (@svovaf)
2144 * @since 2.5.0
2145 */
2146 if ( ! $find_caller ) {
2147 if ( $id == $module_id ) {
2148 continue;
2149 }
2150
2151 if (
2152 isset( $data['path'] ) &&
2153 $data['path'] === $id_slug_type_path_map[ $module_id ]['path']
2154 ) {
2155 $find_caller = true;
2156 }
2157 }
2158 }
2159
2160 if ( $find_caller ) {
2161 $caller_main_file_and_type = $this->get_caller_main_file_and_type( $module_id );
2162
2163 $id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type;
2164 $id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path;
2165
2166 $store_option = true;
2167 }
2168
2169 if ( $store_option ) {
2170 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
2171 }
2172
2173 return $id_slug_type_path_map[ $module_id ]['path'];
2174 }
2175
2176 /**
2177 * Identifies the caller type: plugin or theme.
2178 *
2179 * @author Leo Fajardo (@leorw)
2180 * @since 1.2.2
2181 *
2182 * @author Vova Feldman (@svovaf)
2183 * @since 1.2.2.3 Find the earliest module in the call stack that calls to the SDK. This fix is for cases when
2184 * add-ons are relying on loading the SDK from the parent module, and also allows themes including the
2185 * SDK an internal file instead of directly from functions.php.
2186 * @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic.
2187 *
2188 * @param number $module_id @since 2.5.0
2189 */
2190 private function get_caller_main_file_and_type( $module_id ) {
2191 self::require_plugin_essentials();
2192
2193 $all_plugins = fs_get_plugins( true );
2194 $all_plugins_paths = array();
2195
2196 // Get active plugin's main files real full names (might be symlinks).
2197 foreach ( $all_plugins as $relative_path => $data ) {
2198 if ( false === strpos( fs_normalize_path( $relative_path ), '/' ) ) {
2199 /**
2200 * Ignore plugins that don't have a folder (e.g. Hello Dolly) since they
2201 * can't really include the SDK.
2202 *
2203 * @author Vova Feldman
2204 * @since 1.2.1.7
2205 */
2206 continue;
2207 }
2208
2209 $all_plugins_paths[] = fs_normalize_path( realpath( WP_PLUGIN_DIR . '/' . $relative_path ) );
2210 }
2211
2212 $caller_file_candidate = false;
2213 $caller_map = array();
2214 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2215 $themes_dir = fs_normalize_path( get_theme_root( get_stylesheet() ) );
2216 $plugin_dir_to_skip = false;
2217
2218 for ( $i = 1, $bt = debug_backtrace(), $len = count( $bt ); $i < $len; $i ++ ) {
2219 if ( empty( $bt[ $i ]['file'] ) ) {
2220 continue;
2221 }
2222
2223 if ( $i > 1 && ! empty( $bt[ $i - 1 ]['file'] ) && $bt[ $i ]['file'] === $bt[ $i - 1 ]['file'] ) {
2224 // If file same as the prev file in the stack, skip it.
2225 continue;
2226 }
2227
2228 if ( ! empty( $bt[ $i ]['function'] ) && in_array( $bt[ $i ]['function'], array(
2229 'do_action',
2230 'apply_filter',
2231 // The string split is stupid, but otherwise, theme check
2232 // throws info notices.
2233 'requir' . 'e_once',
2234 'requir' . 'e',
2235 'includ' . 'e_once',
2236 'includ' . 'e',
2237 'install_and_activate_plugin',
2238 'try_activate_plugin',
2239 'activate_plugin'
2240 ) )
2241 ) {
2242 if ( 'activate_plugin' === $bt[ $i ]['function'] ) {
2243 /**
2244 * Store the directory of the activator plugin so that any other file that starts with it
2245 * cannot be mistakenly chosen as a candidate caller file.
2246 *
2247 * @author Leo Fajardo
2248 *
2249 * @since 2.3.0
2250 */
2251 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2252
2253 foreach ( $all_plugins_paths as $plugin_path ) {
2254 $plugin_dir = fs_normalize_path( dirname( $plugin_path ) . '/' );
2255 if ( false !== strpos( $caller_file_path, $plugin_dir ) ) {
2256 $plugin_dir_to_skip = $plugin_dir;
2257
2258 break;
2259 }
2260 }
2261 }
2262
2263 // Ignore call stack hooks and files inclusion.
2264 continue;
2265 }
2266
2267 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2268
2269 if ( ! empty( $plugin_dir_to_skip ) ) {
2270 /**
2271 * Skip if it's an activator plugin file to avoid mistakenly choosing it as a candidate caller file.
2272 *
2273 * @author Leo Fajardo
2274 *
2275 * @since 2.3.0
2276 */
2277 if ( 0 === strpos( $caller_file_path, $plugin_dir_to_skip ) ) {
2278 continue;
2279 }
2280 }
2281
2282 if ( 'functions.php' === basename( $caller_file_path ) ) {
2283 /**
2284 * 1. Assumes that theme's starting execution file is functions.php.
2285 * 2. This complex logic fixes symlink issues (e.g. with Vargant).
2286 *
2287 * @author Vova Feldman (@svovaf)
2288 * @since 1.2.2.5
2289 */
2290
2291 if ( $caller_file_path == fs_normalize_path( realpath( trailingslashit( $themes_dir ) . basename( dirname( $caller_file_path ) ) . '/' . basename( $caller_file_path ) ) ) ) {
2292 $module_type = WP_FS__MODULE_TYPE_THEME;
2293
2294 /**
2295 * Relative path of the theme, e.g.:
2296 * `my-theme/functions.php`
2297 *
2298 * @author Leo Fajardo (@leorw)
2299 */
2300 $caller_file_candidate = basename( dirname( $caller_file_path ) ) .
2301 '/' .
2302 basename( $caller_file_path );
2303
2304 continue;
2305 }
2306 }
2307
2308 $caller_file_hash = md5( $caller_file_path );
2309
2310 if ( ! isset( $caller_map[ $caller_file_hash ] ) ) {
2311 foreach ( $all_plugins_paths as $plugin_path ) {
2312 if ( empty( $plugin_path ) ) {
2313 continue;
2314 }
2315
2316 if ( false !== strpos( $caller_file_path, fs_normalize_path( dirname( $plugin_path ) . '/' ) ) ) {
2317 $caller_map[ $caller_file_hash ] = fs_normalize_path( $plugin_path );
2318 break;
2319 }
2320 }
2321 }
2322
2323 if ( isset( $caller_map[ $caller_file_hash ] ) ) {
2324 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2325 $caller_file_candidate = plugin_basename( $caller_map[ $caller_file_hash ] );
2326 }
2327 }
2328
2329 $caller_main_file_and_type = (object) array(
2330 'module_type' => $module_type,
2331 'path' => $caller_file_candidate
2332 );
2333
2334 return apply_filters( "fs_{$module_id}_caller_main_file_and_type", $caller_main_file_and_type );
2335 }
2336
2337 #----------------------------------------------------------------------------------
2338 #region Deactivation Feedback Form
2339 #----------------------------------------------------------------------------------
2340
2341 /**
2342 * Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
2343 * page.
2344 *
2345 * @author Vova Feldman (@svovaf)
2346 * @author Leo Fajardo (@leorw)
2347 *
2348 * @since 1.1.2
2349 */
2350 function _add_deactivation_feedback_dialog_box() {
2351 if (
2352 $this->is_clone() ||
2353 ( is_object( $this->_site ) && ! $this->is_registered() )
2354 ) {
2355 return;
2356 }
2357
2358 $subscription_cancellation_dialog_box_template_params = $this->apply_filters( 'show_deactivation_subscription_cancellation', true ) ?
2359 $this->_get_subscription_cancellation_dialog_box_template_params() :
2360 array();
2361
2362 /**
2363 * @since 2.3.0 Developers can optionally hide the deactivation feedback form using the 'show_deactivation_feedback_form' filter.
2364 */
2365 $show_deactivation_feedback_form = ! self::is_deactivation_snoozed();
2366 if ( $this->has_filter( 'show_deactivation_feedback_form' ) ) {
2367 $show_deactivation_feedback_form = $this->apply_filters( 'show_deactivation_feedback_form', true );
2368 } else if ( $this->is_addon() ) {
2369 /**
2370 * If the add-on's 'show_deactivation_feedback_form' is not set, try to inherit the value from the parent.
2371 */
2372 $show_deactivation_feedback_form = $this->get_parent_instance()->apply_filters( 'show_deactivation_feedback_form', true );
2373 }
2374
2375 $uninstall_confirmation_message = $this->apply_filters( 'uninstall_confirmation_message', '' );
2376
2377 if (
2378 empty( $subscription_cancellation_dialog_box_template_params ) &&
2379 ! $show_deactivation_feedback_form &&
2380 empty( $uninstall_confirmation_message )
2381 ) {
2382 return;
2383 }
2384
2385 $vars = array( 'id' => $this->_module_id );
2386
2387 if ( $show_deactivation_feedback_form ) {
2388 /* Check the type of user:
2389 * 1. Long-term (long-term)
2390 * 2. Non-registered and non-anonymous short-term (non-registered-and-non-anonymous-short-term).
2391 * 3. Short-term (short-term)
2392 */
2393 $is_long_term_user = true;
2394
2395 // Check if the site is at least 2 days old.
2396 $time_installed = $this->_storage->install_timestamp;
2397
2398 // Difference in seconds.
2399 $date_diff = time() - $time_installed;
2400
2401 // Convert seconds to days.
2402 $date_diff_days = floor( $date_diff / ( 60 * 60 * 24 ) );
2403
2404 if ( $date_diff_days < 2 ) {
2405 $is_long_term_user = false;
2406 }
2407
2408 $is_long_term_user = $this->apply_filters( 'is_long_term_user', $is_long_term_user );
2409
2410 if ( $is_long_term_user ) {
2411 $user_type = 'long-term';
2412 } else {
2413 if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
2414 $user_type = 'non-registered-and-non-anonymous-short-term';
2415 } else {
2416 $user_type = 'short-term';
2417 }
2418 }
2419
2420 $uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
2421
2422 $vars['reasons'] = $uninstall_reasons;
2423 }
2424
2425 $vars['subscription_cancellation_dialog_box_template_params'] = &$subscription_cancellation_dialog_box_template_params;
2426 $vars['show_deactivation_feedback_form'] = $show_deactivation_feedback_form;
2427 $vars['uninstall_confirmation_message'] = $uninstall_confirmation_message;
2428
2429 /**
2430 * Load the HTML template for the deactivation feedback dialog box.
2431 *
2432 * @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.
2433 */
2434 fs_require_template( 'forms/deactivation/form.php', $vars );
2435 }
2436
2437 /**
2438 * @author Leo Fajardo (@leorw)
2439 * @since 1.1.2
2440 *
2441 * @param string $user_type
2442 *
2443 * @return array The uninstall reasons for the specified user type.
2444 */
2445 function _get_uninstall_reasons( $user_type = 'long-term' ) {
2446 $module_type = $this->_module_type;
2447
2448 $internal_message_template_var = array(
2449 'id' => $this->_module_id
2450 );
2451
2452 $plan = $this->get_plan();
2453
2454 if ( $this->is_registered() && is_object( $plan ) && $plan->has_technical_support() ) {
2455 $contact_support_template = fs_get_template( 'forms/deactivation/contact.php', $internal_message_template_var );
2456 } else {
2457 $contact_support_template = '';
2458 }
2459
2460 $reason_found_better_plugin = array(
2461 'id' => self::REASON_FOUND_A_BETTER_PLUGIN,
2462 'text' => sprintf( $this->get_text_inline( 'I found a better %s', 'reason-found-a-better-plugin' ), $module_type ),
2463 'input_type' => 'textfield',
2464 'input_placeholder' => sprintf( $this->get_text_inline( "What's the %s's name?", 'placeholder-plugin-name' ), $module_type ),
2465 );
2466
2467 $reason_temporary_deactivation = array(
2468 'id' => self::REASON_TEMPORARY_DEACTIVATION,
2469 'text' => sprintf(
2470 $this->get_text_inline( "It's a temporary %s - I'm troubleshooting an issue", 'reason-temporary-x' ),
2471 strtolower( $this->is_plugin() ?
2472 $this->get_text_inline( 'Deactivation', 'deactivation' ) :
2473 $this->get_text_inline( 'Theme Switch', 'theme-switch' )
2474 )
2475 ),
2476 'input_type' => '',
2477 'input_placeholder' => ''
2478 );
2479
2480 $reason_other = array(
2481 'id' => self::REASON_OTHER,
2482 'text' => $this->get_text_inline( 'Other', 'reason-other' ),
2483 'input_type' => 'textfield',
2484 'input_placeholder' => ''
2485 );
2486
2487 $long_term_user_reasons = array(
2488 array(
2489 'id' => self::REASON_NO_LONGER_NEEDED,
2490 'text' => sprintf( $this->get_text_inline( 'I no longer need the %s', 'reason-no-longer-needed' ), $module_type ),
2491 'input_type' => '',
2492 'input_placeholder' => ''
2493 ),
2494 $reason_found_better_plugin,
2495 array(
2496 'id' => self::REASON_NEEDED_FOR_A_SHORT_PERIOD,
2497 'text' => sprintf( $this->get_text_inline( 'I only needed the %s for a short period', 'reason-needed-for-a-short-period' ), $module_type ),
2498 'input_type' => '',
2499 'input_placeholder' => ''
2500 ),
2501 array(
2502 'id' => self::REASON_BROKE_MY_SITE,
2503 'text' => sprintf( $this->get_text_inline( 'The %s broke my site', 'reason-broke-my-site' ), $module_type ),
2504 'input_type' => '',
2505 'input_placeholder' => '',
2506 'internal_message' => $contact_support_template
2507 ),
2508 array(
2509 'id' => self::REASON_SUDDENLY_STOPPED_WORKING,
2510 'text' => sprintf( $this->get_text_inline( 'The %s suddenly stopped working', 'reason-suddenly-stopped-working' ), $module_type ),
2511 'input_type' => '',
2512 'input_placeholder' => '',
2513 'internal_message' => $contact_support_template
2514 )
2515 );
2516
2517 if ( $this->is_paying() ) {
2518 $long_term_user_reasons[] = array(
2519 'id' => self::REASON_CANT_PAY_ANYMORE,
2520 'text' => $this->get_text_inline( "I can't pay for it anymore", 'reason-cant-pay-anymore' ),
2521 'input_type' => 'textfield',
2522 'input_placeholder' => $this->get_text_inline( 'What price would you feel comfortable paying?', 'placeholder-comfortable-price' )
2523 );
2524 }
2525
2526 $reason_dont_share_info = array(
2527 'id' => self::REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION,
2528 'text' => $this->get_text_inline( "I don't like to share my information with you", 'reason-dont-like-to-share-my-information' ),
2529 'input_type' => '',
2530 'input_placeholder' => ''
2531 );
2532
2533 /**
2534 * If the current user has selected the "don't share data" reason in the deactivation feedback modal, inform the
2535 * user by showing additional message that he doesn't have to share data and can just choose to skip the opt-in
2536 * (the Skip button is included in the message to show). This message will only be shown if anonymous mode is
2537 * enabled and the user's account is currently not in pending activation state (similar to the way the Skip
2538 * button in the opt-in form is shown/hidden).
2539 */
2540 if ( $this->is_enable_anonymous() && ! $this->is_pending_activation() ) {
2541 $reason_dont_share_info['internal_message'] = fs_get_template( 'forms/deactivation/retry-skip.php', $internal_message_template_var );
2542 }
2543
2544 $uninstall_reasons = array(
2545 'long-term' => $long_term_user_reasons,
2546 'non-registered-and-non-anonymous-short-term' => array(
2547 array(
2548 'id' => self::REASON_DIDNT_WORK,
2549 'text' => sprintf( $this->get_text_inline( "The %s didn't work", 'reason-didnt-work' ), $module_type ),
2550 'input_type' => '',
2551 'input_placeholder' => ''
2552 ),
2553 $reason_dont_share_info,
2554 $reason_found_better_plugin
2555 ),
2556 'short-term' => array(
2557 array(
2558 'id' => self::REASON_COULDNT_MAKE_IT_WORK,
2559 'text' => $this->get_text_inline( "I couldn't understand how to make it work", 'reason-couldnt-make-it-work' ),
2560 'input_type' => '',
2561 'input_placeholder' => '',
2562 'internal_message' => $contact_support_template
2563 ),
2564 $reason_found_better_plugin,
2565 array(
2566 'id' => self::REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE,
2567 '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 ),
2568 'input_type' => 'textarea',
2569 'input_placeholder' => $this->get_text_inline( 'What feature?', 'placeholder-feature' )
2570 ),
2571 array(
2572 'id' => self::REASON_NOT_WORKING,
2573 'text' => sprintf( $this->get_text_inline( 'The %s is not working', 'reason-not-working' ), $module_type ),
2574 'input_type' => 'textarea',
2575 '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' )
2576 ),
2577 array(
2578 'id' => self::REASON_NOT_WHAT_I_WAS_LOOKING_FOR,
2579 'text' => $this->get_text_inline( "It's not what I was looking for", 'reason-not-what-i-was-looking-for' ),
2580 'input_type' => 'textarea',
2581 'input_placeholder' => $this->get_text_inline( "What you've been looking for?", 'placeholder-what-youve-been-looking-for' )
2582 ),
2583 array(
2584 'id' => self::REASON_DIDNT_WORK_AS_EXPECTED,
2585 'text' => sprintf( $this->get_text_inline( "The %s didn't work as expected", 'reason-didnt-work-as-expected' ), $module_type ),
2586 'input_type' => 'textarea',
2587 'input_placeholder' => $this->get_text_inline( 'What did you expect?', 'placeholder-what-did-you-expect' )
2588 )
2589 )
2590 );
2591
2592 // Randomize the reasons for the current user type.
2593 shuffle( $uninstall_reasons[ $user_type ] );
2594
2595 // Keep the following reasons as the last items in the list.
2596 $uninstall_reasons[ $user_type ][] = $reason_temporary_deactivation;
2597 $uninstall_reasons[ $user_type ][] = $reason_other;
2598
2599 $uninstall_reasons = $this->apply_filters( 'uninstall_reasons', $uninstall_reasons );
2600
2601 return $uninstall_reasons[ $user_type ];
2602 }
2603
2604 /**
2605 * Called after the user has submitted his reason for deactivating the plugin.
2606 *
2607 * @author Leo Fajardo (@leorw)
2608 * @since 1.1.2
2609 */
2610 function _submit_uninstall_reason_action() {
2611 $this->_logger->entrance();
2612
2613 $this->check_ajax_referer( 'submit_uninstall_reason' );
2614
2615 $reason_id = fs_request_get( 'reason_id' );
2616
2617 // Check if the given reason ID is an unsigned integer.
2618 if ( ! ctype_digit( $reason_id ) ) {
2619 exit;
2620 }
2621
2622 $reason_info = trim( fs_request_get( 'reason_info', '' ) );
2623 if ( ! empty( $reason_info ) ) {
2624 $reason_info = substr( $reason_info, 0, 128 );
2625 }
2626
2627 $reason = (object) array(
2628 'id' => $reason_id,
2629 'info' => $reason_info,
2630 'is_anonymous' => fs_request_get_bool( 'is_anonymous' )
2631 );
2632
2633 $this->_storage->store( 'uninstall_reason', $reason );
2634
2635 if ( self::REASON_TEMPORARY_DEACTIVATION == $reason->id ) {
2636 $snooze_period = fs_request_get( 'snooze_period' );
2637
2638 if ( is_numeric( $snooze_period ) && 0 < $snooze_period ) {
2639 self::snooze_deactivation_form( (int) $snooze_period );
2640 }
2641 }
2642
2643 /**
2644 * If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do
2645 * not support uninstall hook.
2646 *
2647 * @author Leo Fajardo (@leorw)
2648 * @since 1.2.2
2649 */
2650 if ( $this->is_theme() ) {
2651 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
2652 FS_Plugin_Updater::instance( $this )->delete_update_data();
2653 }
2654
2655 $this->_uninstall_plugin_event( false );
2656 $this->remove_sdk_reference();
2657 }
2658
2659 // Print '1' for successful operation.
2660 echo 1;
2661 exit;
2662 }
2663
2664 #--------------------------------------------------------------------------------
2665 #region Deactivation Feedback Snoozing
2666 #--------------------------------------------------------------------------------
2667
2668 /**
2669 * @author Vova Feldman (@svovaf)
2670 * @since 2.4.3
2671 *
2672 * @param int $period
2673 *
2674 * @return bool True if the value was set, false otherwise.
2675 */
2676 private static function snooze_deactivation_form( $period ) {
2677 return ( 0 < $period && self::reset_deactivation_snoozing( $period ) );
2678 }
2679
2680 /**
2681 * Check if deactivation feedback form is snoozed.
2682 *
2683 * @author Vova Feldman (@svovaf)
2684 * @since 2.4.3
2685 *
2686 * @return bool
2687 */
2688 static function is_deactivation_snoozed() {
2689 $is_snoozed = ( ! is_multisite() || fs_is_network_admin() ) ?
2690 get_transient( 'fs_snooze_period' ) :
2691 get_site_transient( 'fs_snooze_period' );
2692
2693
2694 return ( 'true' === $is_snoozed );
2695 }
2696
2697 /**
2698 * Reset deactivation snoozing. When `$period` is `0` will stop deactivation snoozing by deleting the transients. Otherwise, will set the transients for the selected period.
2699 *
2700 * @param int $period Period in seconds.
2701 *
2702 * @author Vova Feldman (@svovaf)
2703 * @since 2.4.3
2704 */
2705 private static function reset_deactivation_snoozing( $period = 0 ) {
2706 $value = ( 0 === $period ) ? null : 'true';
2707
2708 if ( ! is_multisite() || fs_is_network_admin() ) {
2709 return set_transient( 'fs_snooze_period', $value, $period );
2710 } else {
2711 return set_site_transient( 'fs_snooze_period', $value, $period );
2712 }
2713 }
2714
2715 /**
2716 * The deactivation snooze expiration UNIX timestamp (in sec).
2717 *
2718 * @author Vova Feldman (@svovaf)
2719 * @since 2.4.3
2720 *
2721 * @return int
2722 */
2723 static function deactivation_snooze_expires_at() {
2724 return ( ! is_multisite() || fs_is_network_admin() ) ?
2725 (int) get_option( '_transient_timeout_fs_snooze_period' ) :
2726 (int) get_site_option( '_site_transient_timeout_fs_snooze_period' );
2727 }
2728
2729 #endregion
2730
2731 /**
2732 * @author Leo Fajardo (@leorw)
2733 * @since 2.1.4
2734 */
2735 function cancel_subscription_or_trial_ajax_action() {
2736 $this->_logger->entrance();
2737
2738 $this->check_ajax_referer( 'cancel_subscription_or_trial' );
2739
2740 $result = $this->cancel_subscription_or_trial( fs_request_get( 'plugin_id', $this->get_id() ), false );
2741
2742 if ( $this->is_api_error( $result ) ) {
2743 $this->shoot_ajax_failure( $result->error->message );
2744 }
2745
2746 $this->shoot_ajax_success();
2747 }
2748
2749 /**
2750 * @author Leo Fajardo (@leorw)
2751 * @since 2.1.4
2752 *
2753 * @param number $plugin_id
2754 *
2755 * @return object
2756 */
2757 private function cancel_subscription_or_trial( $plugin_id ) {
2758 $fs = null;
2759 if ( $plugin_id == $this->get_id() ) {
2760 $fs = $this;
2761 } else if ( $this->is_addon_activated( $plugin_id ) ) {
2762 $fs = self::get_instance_by_id( $plugin_id );
2763 }
2764
2765 $result = null;
2766
2767 if ( ! is_null( $fs ) ) {
2768 $result = $fs->is_paid_trial() ?
2769 $fs->_cancel_trial() :
2770 $fs->_downgrade_site();
2771 }
2772
2773 return $result;
2774 }
2775
2776 /**
2777 * @author Leo Fajardo (@leorw)
2778 * @since 2.0.2
2779 */
2780 function _delete_theme_update_data_action() {
2781 FS_Plugin_Updater::instance( $this )->delete_update_data();
2782 }
2783
2784 #endregion
2785
2786 #----------------------------------------------------------------------------------
2787 #region Instance
2788 #----------------------------------------------------------------------------------
2789
2790 /**
2791 * Main singleton instance.
2792 *
2793 * @author Vova Feldman (@svovaf)
2794 * @since 1.0.0
2795 *
2796 * @param number $module_id
2797 * @param string|bool $slug
2798 * @param bool $is_init Is initiation sequence.
2799 *
2800 * @return Freemius|false
2801 */
2802 static function instance( $module_id, $slug = false, $is_init = false ) {
2803 if ( empty( $module_id ) ) {
2804 return false;
2805 }
2806
2807 /**
2808 * 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.
2809 */
2810 self::_load_required_static();
2811
2812 if ( ! is_numeric( $module_id ) ) {
2813 if ( ! $is_init && true === $slug ) {
2814 $is_init = true;
2815 }
2816
2817 $slug = $module_id;
2818
2819 $module = FS_Plugin_Manager::instance( $slug )->get();
2820
2821 if ( is_object( $module ) ) {
2822 $module_id = $module->id;
2823 }
2824 }
2825
2826 $key = 'm_' . $module_id;
2827
2828 if ( ! isset( self::$_instances[ $key ] ) ) {
2829 self::$_instances[ $key ] = new Freemius( $module_id, $slug, $is_init );
2830 }
2831
2832 return self::$_instances[ $key ];
2833 }
2834
2835 /**
2836 * @author Vova Feldman (@svovaf)
2837 * @since 1.0.6
2838 *
2839 * @param number $addon_id
2840 *
2841 * @return bool
2842 */
2843 private static function has_instance( $addon_id ) {
2844 return isset( self::$_instances[ 'm_' . $addon_id ] );
2845 }
2846
2847 /**
2848 * @author Leo Fajardo (@leorw)
2849 * @since 1.2.2
2850 *
2851 * @param string|number $id_or_slug
2852 * @param string $module_type
2853 *
2854 * @return number|false
2855 */
2856 private static function get_module_id( $id_or_slug, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2857 if ( is_numeric( $id_or_slug ) ) {
2858 return $id_or_slug;
2859 }
2860
2861 foreach ( self::$_instances as $instance ) {
2862 // Also check the module type since there can be a plugin and a theme with the same slug.
2863 if ( ( $module_type === $instance->get_module_type() ) && ( $id_or_slug === $instance->get_slug() ) ) {
2864 return $instance->get_id();
2865 }
2866 }
2867
2868 return false;
2869 }
2870
2871 /**
2872 * @author Vova Feldman (@svovaf)
2873 * @since 1.0.6
2874 *
2875 * @param number $id
2876 *
2877 * @return false|Freemius
2878 */
2879 static function get_instance_by_id( $id ) {
2880 return isset ( self::$_instances[ 'm_' . $id ] ) ?
2881 self::$_instances[ 'm_' . $id ] :
2882 false;
2883 }
2884
2885 /**
2886 *
2887 * @author Vova Feldman (@svovaf)
2888 * @since 1.0.1
2889 *
2890 * @param string $plugin_file
2891 * @param string $module_type
2892 *
2893 * @return false|Freemius
2894 */
2895 static function get_instance_by_file( $plugin_file, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2896 $slug = self::find_slug_by_basename( $plugin_file );
2897
2898 return ( false !== $slug ) ?
2899 self::instance( self::get_module_id( $slug, $module_type ) ) :
2900 false;
2901 }
2902
2903 /**
2904 * @author Vova Feldman (@svovaf)
2905 * @since 1.0.6
2906 *
2907 * @return false|Freemius
2908 */
2909 function get_parent_instance() {
2910 return self::get_instance_by_id( $this->_plugin->parent_plugin_id );
2911 }
2912
2913 /**
2914 * @author Vova Feldman (@svovaf)
2915 * @since 1.0.6
2916 *
2917 * @param string|number $id_or_slug
2918 *
2919 * @return false|Freemius
2920 */
2921 function get_addon_instance( $id_or_slug ) {
2922 $addon_id = self::get_module_id( $id_or_slug );
2923
2924 return self::instance( $addon_id );
2925 }
2926
2927 /**
2928 * @return Freemius[]
2929 */
2930 static function _get_all_instances() {
2931 return self::$_instances;
2932 }
2933
2934 #endregion ------------------------------------------------------------------
2935
2936 /**
2937 * @author Vova Feldman (@svovaf)
2938 * @since 1.0.6
2939 *
2940 * @return bool
2941 */
2942 function is_parent_plugin_installed() {
2943 $is_active = self::has_instance( $this->_plugin->parent_plugin_id );
2944
2945 if ( $is_active ) {
2946 return true;
2947 }
2948
2949 /**
2950 * Parent module might be a theme. If that's the case, the add-on's FS
2951 * instance will be loaded prior to the theme's FS instance, therefore,
2952 * we need to check if it's active with a "look ahead".
2953 *
2954 * @author Vova Feldman
2955 * @since 1.2.2.3
2956 */
2957 global $fs_active_plugins;
2958 if ( is_object( $fs_active_plugins ) && is_array( $fs_active_plugins->plugins ) ) {
2959 $active_theme = wp_get_theme();
2960
2961 foreach ( $fs_active_plugins->plugins as $sdk => $module ) {
2962 if ( WP_FS__MODULE_TYPE_THEME === $module->type ) {
2963 if ( $module->plugin_path == $active_theme->get_stylesheet() ) {
2964 // Parent module is a theme and it's currently active.
2965 return true;
2966 }
2967 }
2968 }
2969 }
2970
2971 return false;
2972 }
2973
2974 /**
2975 * Check if add-on parent plugin in activation mode.
2976 *
2977 * @author Vova Feldman (@svovaf)
2978 * @since 1.0.7
2979 *
2980 * @return bool
2981 */
2982 function is_parent_in_activation() {
2983 $parent_fs = $this->get_parent_instance();
2984 if ( ! is_object( $parent_fs ) ) {
2985 return false;
2986 }
2987
2988 return ( $parent_fs->is_activation_mode() );
2989 }
2990
2991 /**
2992 * Is plugin in activation mode.
2993 *
2994 * @author Vova Feldman (@svovaf)
2995 * @since 1.0.7
2996 *
2997 * @param bool $and_on
2998 *
2999 * @return bool
3000 */
3001 function is_activation_mode( $and_on = true ) {
3002 return fs_is_network_admin() ?
3003 $this->is_network_activation_mode( $and_on ) :
3004 $this->is_site_activation_mode( $and_on );
3005 }
3006
3007 /**
3008 * Is plugin in activation mode.
3009 *
3010 * @author Vova Feldman (@svovaf)
3011 * @since 1.0.7
3012 *
3013 * @param bool $and_on
3014 *
3015 * @return bool
3016 */
3017 function is_site_activation_mode( $and_on = true ) {
3018 return (
3019 ( $this->is_on() || ! $and_on ) &&
3020 (
3021 ( $this->is_premium() && true === $this->_storage->require_license_activation ) ||
3022 (
3023 ( ! $this->is_registered() ||
3024 ( $this->is_only_premium() && ! $this->has_features_enabled_license() ) ) &&
3025 ( ! $this->is_enable_anonymous() ||
3026 ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) )
3027 )
3028 )
3029 );
3030 }
3031
3032 /**
3033 * Checks if the SDK in network activation mode.
3034 *
3035 * @author Leo Fajardo (@leorw)
3036 * @since 2.0.0
3037 *
3038 * @param bool $and_on
3039 *
3040 * @return bool
3041 */
3042 private function is_network_activation_mode( $and_on = true ) {
3043 if ( ! $this->_is_network_active ) {
3044 // Not network activated.
3045 return false;
3046 }
3047
3048 if ( $this->is_network_upgrade_mode() ) {
3049 // Special flag to enforce network activation mode to decide what to do with the sites that are not yet opted-in nor skipped.
3050 return true;
3051 }
3052
3053 if ( ! $this->is_site_activation_mode( $and_on ) ) {
3054 // 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.
3055 return false;
3056 }
3057
3058 if ( $this->is_network_delegated_connection() ) {
3059 // Super-admin delegated the connection to the site admins -> not activation mode.
3060 return false;
3061 }
3062
3063 if ( $this->is_network_anonymous() && true !== $this->_storage->require_license_activation ) {
3064 // Super-admin skipped the connection network wide -> not activation mode.
3065 return false;
3066 }
3067
3068 if ( $this->is_network_registered() ) {
3069 // Super-admin connected at least one site -> not activation mode.
3070 return false;
3071 }
3072
3073 return true;
3074 }
3075
3076 /**
3077 * Check if current page is the opt-in/pending-activation page.
3078 *
3079 * @author Vova Feldman (@svovaf)
3080 * @since 1.2.1.7
3081 *
3082 * @return bool
3083 */
3084 function is_activation_page() {
3085 if ( $this->_menu->is_activation_page( $this->show_opt_in_on_themes_page() ) ) {
3086 return true;
3087 }
3088
3089 if ( ! $this->is_activation_mode() ) {
3090 return false;
3091 }
3092
3093 // Check if current page is matching the activation page.
3094 return $this->is_matching_url( $this->get_activation_url() );
3095 }
3096
3097 /**
3098 * Check if URL path's are matching and that all querystring
3099 * arguments of the $sub_url exist in the $url with the same values.
3100 *
3101 * WARNING:
3102 * 1. This method doesn't check if the sub/domain are matching.
3103 * 2. Ignore case sensitivity.
3104 *
3105 * @author Vova Feldman (@svovaf)
3106 * @since 1.2.1.7
3107 *
3108 * @param string $sub_url
3109 * @param string $url If argument is not set, check if the sub_url matching the current's page URL.
3110 *
3111 * @return bool
3112 */
3113 private function is_matching_url( $sub_url, $url = '' ) {
3114 if ( empty( $url ) ) {
3115 $url = $_SERVER['REQUEST_URI'];
3116 }
3117
3118 $url = strtolower( $url );
3119 $sub_url = strtolower( $sub_url );
3120
3121 if ( parse_url( $sub_url, PHP_URL_PATH ) !== parse_url( $url, PHP_URL_PATH ) ) {
3122 // Different path - DO NOT OVERRIDE PAGE.
3123 return false;
3124 }
3125
3126 $url_params = fs_parse_url_params( $url );
3127 $sub_url_params = fs_parse_url_params( $sub_url );
3128
3129 foreach ( $sub_url_params as $key => $val ) {
3130 if ( ! isset( $url_params[ $key ] ) || $val != $url_params[ $key ] ) {
3131 // Not matching query string - DO NOT OVERRIDE PAGE.
3132 return false;
3133 }
3134 }
3135
3136 return true;
3137 }
3138
3139 /**
3140 * Get the basenames of all active plugins for specific blog. Including network activated plugins.
3141 *
3142 * @author Vova Feldman (@svovaf)
3143 * @since 2.0.0
3144 *
3145 * @param int $blog_id
3146 *
3147 * @return string[]
3148 */
3149 private static function get_active_plugins_basenames( $blog_id = 0 ) {
3150 if ( is_multisite() && $blog_id > 0 ) {
3151 $active_basenames = get_blog_option( $blog_id, 'active_plugins' );
3152 } else {
3153 $active_basenames = get_option( 'active_plugins' );
3154 }
3155
3156 if ( ! is_array( $active_basenames ) ) {
3157 $active_basenames = array();
3158 }
3159
3160 if ( is_multisite() ) {
3161 $network_active_basenames = get_site_option( 'active_sitewide_plugins' );
3162
3163 if ( is_array( $network_active_basenames ) && ! empty( $network_active_basenames ) ) {
3164 $active_basenames = array_merge( $active_basenames, array_keys( $network_active_basenames ) );
3165 }
3166 }
3167
3168 return $active_basenames;
3169 }
3170
3171 /**
3172 * @author Leo Fajardo (@leorw)
3173 * @since 2.3.0
3174 *
3175 * @param int $blog_id
3176 *
3177 * @return array
3178 */
3179 static function get_active_plugins_directories_map( $blog_id = 0 ) {
3180 $active_basenames = self::get_active_plugins_basenames( $blog_id );
3181
3182 $map = array();
3183
3184 foreach ( $active_basenames as $active_basename ) {
3185 $active_basename = fs_normalize_path( $active_basename );
3186
3187 if ( false === strpos( $active_basename, '/' ) ) {
3188 continue;
3189 }
3190
3191 $map[ dirname( $active_basename ) ] = true;
3192 }
3193
3194 return $map;
3195 }
3196
3197 /**
3198 * Get collection of all active plugins. Including network activated plugins.
3199 *
3200 * @author Vova Feldman (@svovaf)
3201 * @since 1.0.9
3202 *
3203 * @param int $blog_id Since 2.0.0
3204 *
3205 * @return array[string]array
3206 */
3207 private static function get_active_plugins( $blog_id = 0 ) {
3208 self::require_plugin_essentials();
3209
3210 $active_plugin = array();
3211 $all_plugins = fs_get_plugins();
3212 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3213
3214 foreach ( $active_plugins_basenames as $plugin_basename ) {
3215 $active_plugin[ $plugin_basename ] = $all_plugins[ $plugin_basename ];
3216 }
3217
3218 return $active_plugin;
3219 }
3220
3221 /**
3222 * Get collection of all site active plugins for a specified blog.
3223 *
3224 * @author Vova Feldman (@svovaf)
3225 * @since 2.0.0
3226 *
3227 * @param int $blog_id
3228 *
3229 * @return array[string]array
3230 */
3231 private static function get_site_active_plugins( $blog_id = 0 ) {
3232 $active_basenames = ( is_multisite() && $blog_id > 0 ) ?
3233 get_blog_option( $blog_id, 'active_plugins' ) :
3234 get_option( 'active_plugins' );
3235
3236 $active = array();
3237
3238 if ( ! is_array( $active_basenames ) ) {
3239 return $active;
3240 }
3241
3242 foreach ( $active_basenames as $basename ) {
3243 $active[ $basename ] = array(
3244 'is_active' => true,
3245 'Version' => '1.0', // Dummy version.
3246 'slug' => self::get_plugin_slug( $basename ),
3247 );
3248 }
3249
3250 return $active;
3251 }
3252
3253 /**
3254 * Get collection of all plugins with their activation status for a specified blog.
3255 *
3256 * @author Vova Feldman (@svovaf)
3257 * @since 1.1.8
3258 *
3259 * @param int $blog_id Since 2.0.0
3260 *
3261 * @return array Key is the plugin file path and the value is an array of the plugin data.
3262 */
3263 private static function get_all_plugins( $blog_id = 0 ) {
3264 self::require_plugin_essentials();
3265
3266 $all_plugins = fs_get_plugins();
3267
3268 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3269
3270 foreach ( $all_plugins as $basename => &$data ) {
3271 // By default set to inactive (next foreach update the active plugins).
3272 $data['is_active'] = false;
3273 // Enrich with plugin slug.
3274 $data['slug'] = self::get_plugin_slug( $basename );
3275 }
3276
3277 // Flag active plugins.
3278 foreach ( $active_plugins_basenames as $basename ) {
3279 if ( isset( $all_plugins[ $basename ] ) ) {
3280 $all_plugins[ $basename ]['is_active'] = true;
3281 }
3282 }
3283
3284 return $all_plugins;
3285 }
3286
3287 /**
3288 * Get collection of all plugins and if they are network level activated.
3289 *
3290 * @author Vova Feldman (@svovaf)
3291 * @since 2.0.0
3292 *
3293 * @return array Key is the plugin basename and the value is an array of the plugin data.
3294 */
3295 private static function get_network_plugins() {
3296 self::require_plugin_essentials();
3297
3298 $all_plugins = fs_get_plugins();
3299
3300 $network_active_basenames = is_multisite() ?
3301 get_site_option( 'active_sitewide_plugins' ) :
3302 array();
3303
3304 foreach ( $all_plugins as $basename => &$data ) {
3305 // By default set to inactive (next foreach update the active plugins).
3306 $data['is_active'] = false;
3307 // Enrich with plugin slug.
3308 $data['slug'] = self::get_plugin_slug( $basename );
3309 }
3310
3311 // Flag active plugins.
3312 foreach ( $network_active_basenames as $basename ) {
3313 if ( isset( $all_plugins[ $basename ] ) ) {
3314 $all_plugins[ $basename ]['is_active'] = true;
3315 }
3316 }
3317
3318 return $all_plugins;
3319 }
3320
3321 /**
3322 * Cached result of get_site_transient( 'update_plugins' )
3323 *
3324 * @author Vova Feldman (@svovaf)
3325 * @since 1.1.8
3326 *
3327 * @var object
3328 */
3329 private static $_plugins_info;
3330
3331 /**
3332 * Helper function to get specified plugin's slug.
3333 *
3334 * @author Vova Feldman (@svovaf)
3335 * @since 1.1.8
3336 *
3337 * @param $basename
3338 *
3339 * @return string
3340 */
3341 private static function get_plugin_slug( $basename ) {
3342 if ( ! isset( self::$_plugins_info ) ) {
3343 self::$_plugins_info = get_site_transient( 'update_plugins' );
3344 }
3345
3346 $slug = '';
3347
3348 if ( is_object( self::$_plugins_info ) ) {
3349 if ( isset( self::$_plugins_info->no_update ) &&
3350 isset( self::$_plugins_info->no_update[ $basename ] ) &&
3351 ! empty( self::$_plugins_info->no_update[ $basename ]->slug )
3352 ) {
3353 $slug = self::$_plugins_info->no_update[ $basename ]->slug;
3354 } else if ( isset( self::$_plugins_info->response ) &&
3355 isset( self::$_plugins_info->response[ $basename ] ) &&
3356 ! empty( self::$_plugins_info->response[ $basename ]->slug )
3357 ) {
3358 $slug = self::$_plugins_info->response[ $basename ]->slug;
3359 }
3360 }
3361
3362 if ( empty( $slug ) ) {
3363 // Try to find slug from FS data.
3364 $slug = self::find_slug_by_basename( $basename );
3365 }
3366
3367 if ( empty( $slug ) ) {
3368 // Fallback to plugin's folder name.
3369 $slug = dirname( $basename );
3370 }
3371
3372 return $slug;
3373 }
3374
3375 private static $_statics_loaded = false;
3376
3377 /**
3378 * Load static resources.
3379 *
3380 * @author Vova Feldman (@svovaf)
3381 * @since 1.0.1
3382 */
3383 private static function _load_required_static() {
3384 if ( self::$_statics_loaded ) {
3385 return;
3386 }
3387
3388 self::$_static_logger = FS_Logger::get_logger( WP_FS__SLUG, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
3389
3390 self::$_static_logger->entrance();
3391
3392 self::$_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
3393
3394 if ( is_multisite() ) {
3395 $has_skipped_migration = (
3396 // 'id_slug_type_path_map' - was never stored on older versions, therefore, not exists on the site level.
3397 null === self::$_accounts->get_option( 'id_slug_type_path_map', null, false ) &&
3398 // 'file_slug_map' stored on the site level, so it was running an SDK version before it was integrated with MS-network.
3399 null !== self::$_accounts->get_option( 'file_slug_map', null, false )
3400 );
3401
3402 /**
3403 * If the file_slug_map exists on the site level but doesn't exist on the
3404 * network level storage, it means that we need to process the storage with migration.
3405 *
3406 * 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.
3407 *
3408 * @author Vova Feldman (@svovaf)
3409 * @since 2.0.0
3410 */
3411 if (
3412 ( $has_skipped_migration && true !== self::$_accounts->get_option( 'ms_migration_complete', false, true ) ) ||
3413 ( null === self::$_accounts->get_option( 'file_slug_map', null, true ) &&
3414 null !== self::$_accounts->get_option( 'file_slug_map', null, false ) )
3415 ) {
3416 self::migrate_options_to_network();
3417 }
3418 }
3419
3420 self::$_global_admin_notices = FS_Admin_Notices::instance( 'global' );
3421
3422 if ( ! WP_FS__DEMO_MODE ) {
3423 add_action( ( fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu', array(
3424 'Freemius',
3425 '_add_debug_section'
3426 ) );
3427 }
3428
3429 add_action( "wp_ajax_fs_toggle_debug_mode", array( 'Freemius', '_toggle_debug_mode' ) );
3430
3431 self::add_ajax_action_static( 'get_debug_log', array( 'Freemius', '_get_debug_log' ) );
3432
3433 self::add_ajax_action_static( 'get_db_option', array( 'Freemius', '_get_db_option' ) );
3434
3435 self::add_ajax_action_static( 'set_db_option', array( 'Freemius', '_set_db_option' ) );
3436
3437 if ( 0 == did_action( 'plugins_loaded' ) ) {
3438 add_action( 'plugins_loaded', array( 'Freemius', '_load_textdomain' ), 1 );
3439 }
3440
3441 $clone_manager = FS_Clone_Manager::instance();
3442 add_action( 'init', array( $clone_manager, '_init' ) );
3443
3444 add_action( 'admin_footer', array( 'Freemius', '_open_support_forum_in_new_page' ) );
3445
3446 if ( self::is_plugins_page() || self::is_themes_page() ) {
3447 add_action( 'admin_print_footer_scripts', array( 'Freemius', '_maybe_add_beta_label_styles' ), 9 );
3448
3449 /**
3450 * Specifically use this hook so that the JS event handlers will work properly on the "Themes"
3451 * page.
3452 *
3453 * @author Leo Fajardo (@leorw)
3454 * @since 2.3.0
3455 */
3456 add_action( 'admin_footer-' . self::get_current_page(), array( 'Freemius', '_maybe_add_beta_label_to_plugins_and_handle_confirmation') );
3457 }
3458
3459 self::$_statics_loaded = true;
3460 }
3461
3462 #--------------------------------------------------------------------------------
3463 #region Clone
3464 #--------------------------------------------------------------------------------
3465
3466 /**
3467 * @author Leo Fajardo (@leorw)
3468 * @since 2.5.0
3469 *
3470 * @param bool $only_if_manual_resolution_is_not_hidden
3471 *
3472 * @return bool
3473 */
3474 private function is_unresolved_clone( $only_if_manual_resolution_is_not_hidden = false ) {
3475 if ( ! $this->is_clone( $only_if_manual_resolution_is_not_hidden ) ) {
3476 return false;
3477 }
3478
3479 return FS_Clone_Manager::instance()->has_temporary_duplicate_mode_expired();
3480 }
3481
3482 /**
3483 * @author Leo Fajardo (@leorw)
3484 * @since 2.5.0
3485 *
3486 * @param bool $only_if_manual_resolution_is_not_hidden
3487 */
3488 function is_clone( $only_if_manual_resolution_is_not_hidden = false ) {
3489 if ( ! is_object( $this->_site ) ) {
3490 return false;
3491 }
3492
3493 $blog_id = null;
3494
3495 if (
3496 fs_is_network_admin() &&
3497 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
3498 ) {
3499 // Ensure that we're comparing the network install's URL with the relevant subsite's URL.
3500 $blog_id = $this->_storage->network_install_blog_id;
3501 }
3502
3503 $site_url = Freemius::get_unfiltered_site_url( $blog_id, true, true );
3504
3505 if ( ! $this->_site->is_clone( $site_url ) ) {
3506 return false;
3507 }
3508
3509 return (
3510 ! $only_if_manual_resolution_is_not_hidden ||
3511 ! FS_Clone_Manager::instance()->should_hide_manual_resolution()
3512 );
3513 }
3514
3515 /**
3516 * @author Leo Fajardo (@leorw)
3517 * @since 2.5.0
3518 *
3519 * @param int|null $blog_id
3520 * @param bool $strip_protocol
3521 * @param bool $add_trailing_slash
3522 *
3523 * @return string
3524 */
3525 static function get_unfiltered_site_url( $blog_id = null, $strip_protocol = false, $add_trailing_slash = false ) {
3526 $url = ( ! is_multisite() && defined( 'WP_SITEURL' ) ) ? WP_SITEURL : self::get_site_url_from_wp_option( $blog_id );
3527
3528 if ( $strip_protocol ) {
3529 $url = fs_strip_url_protocol( $url );
3530 }
3531
3532 if ( $add_trailing_slash ) {
3533 $url = trailingslashit( $url );
3534 }
3535
3536 return $url;
3537 }
3538
3539 /**
3540 * @author Leo Fajardo (@leorw)
3541 * @since 2.6.0
3542 *
3543 * @param int|null $blog_id
3544 *
3545 * @return string
3546 */
3547 private static function get_site_url_from_wp_option( $blog_id = null ) {
3548 global $wp_filter;
3549
3550 $site_url_filters = array(
3551 'site_url' => null,
3552 'pre_option_siteurl' => null,
3553 'default_option_siteurl' => null,
3554 'option_siteurl' => null,
3555 );
3556
3557 // Detach all URL-related filters to get the actual site's URL (stripped of potential manipulations by multilingual plugins).
3558 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3559 if ( ! empty( $wp_filter[ $hook_name ] ) ) {
3560 $site_url_filters[ $hook_name ] = $wp_filter[ $hook_name ];
3561 unset( $wp_filter[ $hook_name ] );
3562 }
3563 }
3564
3565 $url = get_site_url( $blog_id );
3566
3567 // Re-attach the filters back.
3568 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3569 if ( ! empty( $site_url_filter ) ) {
3570 $wp_filter[ $hook_name ] = $site_url_filter;
3571 }
3572 }
3573
3574 return $url;
3575 }
3576
3577 /**
3578 * @author Leo Fajardo (@leorw)
3579 * @since 2.5.0
3580 *
3581 * @param number $site_id
3582 */
3583 function fetch_install_by_id( $site_id ) {
3584 return $this->get_current_or_network_user_api_scope()->get( "/installs/{$site_id}.json" );
3585 }
3586
3587 /**
3588 * @author Leo Fajardo (@leorw)
3589 * @since 2.5.0
3590 *
3591 * @return string|object|bool
3592 */
3593 function _handle_long_term_duplicate() {
3594 $this->_logger->entrance();
3595
3596 $this->delete_current_install( false );
3597
3598 $license_key = false;
3599
3600 if (
3601 is_object( $this->_license ) &&
3602 ! $this->_license->is_utilized(
3603 ( WP_FS__IS_LOCALHOST_FOR_SERVER || FS_Site::is_localhost_by_address( self::get_unfiltered_site_url() ) )
3604 )
3605 ) {
3606 $license_key = $this->_license->secret_key;
3607 }
3608
3609 return $this->opt_in(
3610 false,
3611 false,
3612 false,
3613 $license_key,
3614 false,
3615 false,
3616 false,
3617 null,
3618 array(),
3619 false
3620 );
3621 }
3622
3623 #endregion
3624
3625 /**
3626 * @author Leo Fajardo (@leorw)
3627 *
3628 * @since 2.1.3
3629 */
3630 private static function migrate_options_to_network() {
3631 self::migrate_accounts_to_network();
3632
3633 // Migrate API options from site level to network level.
3634 $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
3635 $api_network_options->migrate_to_network();
3636
3637 // Migrate API cache to network level storage.
3638 FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
3639
3640 self::$_accounts->set_option( 'ms_migration_complete', true, true );
3641 }
3642
3643 #----------------------------------------------------------------------------------
3644 #region Localization
3645 #----------------------------------------------------------------------------------
3646
3647 /**
3648 * Load framework's text domain.
3649 *
3650 * @author Vova Feldman (@svovaf)
3651 * @since 1.2.1
3652 */
3653 static function _load_textdomain() {
3654 if ( ! is_admin() ) {
3655 return;
3656 }
3657
3658 global $fs_active_plugins;
3659
3660 // Works both for plugins and themes.
3661 load_plugin_textdomain(
3662 'freemius',
3663 false,
3664 $fs_active_plugins->newest->sdk_path . '/languages/'
3665 );
3666 }
3667
3668 #endregion
3669
3670 #----------------------------------------------------------------------------------
3671 #region Debugging
3672 #----------------------------------------------------------------------------------
3673
3674 /**
3675 * @author Vova Feldman (@svovaf)
3676 * @since 1.0.8
3677 */
3678 static function _add_debug_section() {
3679 if ( ! is_super_admin() ) {
3680 // Add debug page only for super-admins.
3681 return;
3682 }
3683
3684 self::$_static_logger->entrance();
3685
3686 $title = sprintf( '%s [v.%s]', fs_text_inline( 'Freemius Debug' ), WP_FS__SDK_VERSION );
3687
3688 if ( WP_FS__DEV_MODE ) {
3689 // Add top-level debug menu item.
3690 $hook = FS_Admin_Menu_Manager::add_page(
3691 $title,
3692 $title,
3693 'manage_options',
3694 'freemius',
3695 array( 'Freemius', '_debug_page_render' )
3696 );
3697 } else {
3698 // Add hidden debug page.
3699 $hook = FS_Admin_Menu_Manager::add_subpage(
3700 '',
3701 $title,
3702 $title,
3703 'manage_options',
3704 'freemius',
3705 array( 'Freemius', '_debug_page_render' )
3706 );
3707 }
3708
3709 if ( ! empty( $hook ) ) {
3710 add_action( "load-$hook", array( 'Freemius', '_debug_page_actions' ) );
3711 }
3712 }
3713
3714 /**
3715 * @author Vova Feldman (@svovaf)
3716 * @since 1.1.7.3
3717 */
3718 static function _toggle_debug_mode() {
3719 check_admin_referer( 'fs_toggle_debug_mode' );
3720
3721 if ( ! is_super_admin() ) {
3722 return;
3723 }
3724
3725 $is_on = fs_request_get( 'is_on', false, 'post' );
3726
3727 if ( fs_request_is_post() && in_array( $is_on, array( 0, 1 ) ) ) {
3728 update_option( 'fs_debug_mode', $is_on );
3729
3730 // Turn on/off storage logging.
3731 FS_Logger::_set_storage_logging( ( 1 == $is_on ) );
3732 }
3733
3734 exit;
3735 }
3736
3737 /**
3738 * @author Vova Feldman (@svovaf)
3739 * @since 1.2.1.6
3740 */
3741 static function _get_debug_log() {
3742 check_admin_referer( 'fs_get_debug_log' );
3743
3744 if ( ! is_super_admin() ) {
3745 return;
3746 }
3747
3748 $limit = min( ! empty( $_POST['limit'] ) ? absint( $_POST['limit'] ) : 200, 200 );
3749 $offset = min( ! empty( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 200, 200 );
3750
3751 $logs = FS_Logger::load_db_logs(
3752 fs_request_get( 'filters', false, 'post' ),
3753 $limit,
3754 $offset
3755 );
3756
3757 self::shoot_ajax_success( $logs );
3758 }
3759
3760 /**
3761 * @author Vova Feldman (@svovaf)
3762 * @since 1.2.1.7
3763 */
3764 static function _get_db_option() {
3765 check_admin_referer( 'fs_get_db_option' );
3766
3767 $option_name = fs_request_get( 'option_name' );
3768
3769 if ( ! is_super_admin() ||
3770 ! fs_starts_with( $option_name, 'fs_' )
3771 ) {
3772 self::shoot_ajax_failure();
3773 }
3774
3775 $value = get_option( $option_name );
3776
3777 $result = array(
3778 'name' => $option_name,
3779 );
3780
3781 if ( false !== $value ) {
3782 if ( ! is_string( $value ) ) {
3783 $value = json_encode( $value );
3784 }
3785
3786 $result['value'] = $value;
3787 }
3788
3789 self::shoot_ajax_success( $result );
3790 }
3791
3792 /**
3793 * @author Vova Feldman (@svovaf)
3794 * @since 1.2.1.7
3795 */
3796 static function _set_db_option() {
3797 check_admin_referer( 'fs_set_db_option' );
3798
3799 $option_name = fs_request_get( 'option_name' );
3800
3801 if ( ! is_super_admin() ||
3802 ! fs_starts_with( $option_name, 'fs_' )
3803 ) {
3804 self::shoot_ajax_failure();
3805 }
3806
3807 $option_value = fs_request_get_raw( 'option_value' );
3808
3809 if ( ! empty( $option_value ) ) {
3810 update_option( $option_name, $option_value );
3811 }
3812
3813 self::shoot_ajax_success();
3814 }
3815
3816 /**
3817 * @author Vova Feldman (@svovaf)
3818 * @since 1.0.8
3819 */
3820 static function _debug_page_actions() {
3821 self::_clean_admin_content_section();
3822
3823 if ( fs_request_is_action( 'restart_freemius' ) ) {
3824 check_admin_referer( 'restart_freemius' );
3825
3826 if ( ! is_multisite() ) {
3827 // Clear accounts data.
3828 self::$_accounts->clear( null, true );
3829 } else {
3830 $sites = self::get_sites();
3831 foreach ( $sites as $site ) {
3832 $blog_id = self::get_site_blog_id( $site );
3833 self::$_accounts->clear( $blog_id, true );
3834 }
3835
3836 // Clear network level storage.
3837 self::$_accounts->clear( true, true );
3838 }
3839
3840 // Clear SDK reference cache.
3841 delete_option( 'fs_active_plugins' );
3842 } else if ( fs_request_is_action( 'clear_updates_data' ) ) {
3843 check_admin_referer( 'clear_updates_data' );
3844
3845 if ( ! is_multisite() ) {
3846 set_site_transient( 'update_plugins', null );
3847 set_site_transient( 'update_themes', null );
3848 } else {
3849 $current_blog_id = get_current_blog_id();
3850
3851 $sites = self::get_sites();
3852 foreach ( $sites as $site ) {
3853 switch_to_blog( self::get_site_blog_id( $site ) );
3854
3855 set_site_transient( 'update_plugins', null );
3856 set_site_transient( 'update_themes', null );
3857 }
3858
3859 switch_to_blog( $current_blog_id );
3860 }
3861 } else if ( fs_request_is_action( 'reset_deactivation_snoozing' ) ) {
3862 check_admin_referer( 'reset_deactivation_snoozing' );
3863
3864 self::reset_deactivation_snoozing();
3865 } else if ( fs_request_is_action( 'simulate_trial' ) ) {
3866 check_admin_referer( 'simulate_trial' );
3867
3868 $fs = freemius( fs_request_get( 'module_id' ) );
3869
3870 // Update SDK install to at least 24 hours before.
3871 $fs->_storage->install_timestamp = ( time() - WP_FS__TIME_24_HOURS_IN_SEC );
3872 // Unset the trial shown timestamp.
3873 unset( $fs->_storage->trial_promotion_shown );
3874 } else if ( fs_request_is_action( 'simulate_network_upgrade' ) ) {
3875 check_admin_referer( 'simulate_network_upgrade' );
3876
3877 $fs = freemius( fs_request_get( 'module_id' ) );
3878
3879 self::set_network_upgrade_mode( $fs->_storage );
3880 } else if ( fs_request_is_action( 'delete_install' ) ) {
3881 check_admin_referer( 'delete_install' );
3882
3883 self::_delete_site_by_slug(
3884 fs_request_get( 'slug' ),
3885 fs_request_get( 'module_type' ),
3886 true,
3887 fs_request_get( 'blog_id', null )
3888 );
3889 } else if ( fs_request_is_action( 'delete_user' ) ) {
3890 check_admin_referer( 'delete_user' );
3891
3892 self::delete_user( fs_request_get( 'user_id' ) );
3893 } else if ( fs_request_is_action( 'download_logs' ) ) {
3894 check_admin_referer( 'download_logs' );
3895
3896 $download_url = FS_Logger::download_db_logs(
3897 fs_request_get( 'filters', false, 'post' )
3898 );
3899
3900 if ( false === $download_url ) {
3901 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.' );
3902 }
3903
3904 fs_redirect( $download_url );
3905 } else if ( fs_request_is_action( 'migrate_options_to_network' ) ) {
3906 check_admin_referer( 'migrate_options_to_network' );
3907
3908 self::migrate_options_to_network();
3909 }
3910 }
3911
3912 /**
3913 * @author Leo Fajardo (@leorw)
3914 * @since 2.5.0
3915 *
3916 * @return array
3917 */
3918 static function get_all_modules_sites() {
3919 self::$_static_logger->entrance();
3920
3921 $sites_by_type = array(
3922 WP_FS__MODULE_TYPE_PLUGIN => array(),
3923 WP_FS__MODULE_TYPE_THEME => array(),
3924 );
3925
3926 $module_types = array_keys( $sites_by_type );
3927
3928 if ( ! is_multisite() ) {
3929 foreach ( $module_types as $type ) {
3930 $sites_by_type[ $type ] = self::get_all_sites( $type );
3931
3932 foreach ( $sites_by_type[ $type ] as $slug => $install ) {
3933 $sites_by_type[ $type ][ $slug ] = array( $install );
3934 }
3935 }
3936 } else {
3937 $sites = self::get_sites();
3938
3939 foreach ( $sites as $site ) {
3940 $blog_id = self::get_site_blog_id( $site );
3941
3942 foreach ( $module_types as $type ) {
3943 $installs = self::get_all_sites( $type, $blog_id );
3944
3945 foreach ( $installs as $slug => $install ) {
3946 if ( ! isset( $sites_by_type[ $type ][ $slug ] ) ) {
3947 $sites_by_type[ $type ][ $slug ] = array();
3948 }
3949
3950 $install->blog_id = $blog_id;
3951
3952 $sites_by_type[ $type ][ $slug ][] = $install;
3953 }
3954
3955 }
3956 }
3957 }
3958
3959 return $sites_by_type;
3960 }
3961
3962 /**
3963 * @author Vova Feldman (@svovaf)
3964 * @since 1.0.8
3965 */
3966 static function _debug_page_render() {
3967 self::$_static_logger->entrance();
3968
3969 $all_modules_sites = self::get_all_modules_sites();
3970
3971 $licenses_by_module_type = self::get_all_licenses_by_module_type();
3972
3973 $vars = array(
3974 'plugin_sites' => $all_modules_sites[ WP_FS__MODULE_TYPE_PLUGIN ],
3975 'theme_sites' => $all_modules_sites[ WP_FS__MODULE_TYPE_THEME ],
3976 'users' => self::get_all_users(),
3977 'addons' => self::get_all_addons(),
3978 'account_addons' => self::get_all_account_addons(),
3979 'plugin_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_PLUGIN ],
3980 'theme_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_THEME ]
3981 );
3982
3983 fs_enqueue_local_style( 'fs_debug', '/admin/debug.css' );
3984 fs_require_once_template( 'debug.php', $vars );
3985 }
3986
3987 #endregion
3988
3989 #----------------------------------------------------------------------------------
3990 #region Connectivity Issues
3991 #----------------------------------------------------------------------------------
3992
3993 /**
3994 * Check if Freemius should be turned on for the current plugin install.
3995 *
3996 * Note:
3997 * $this->_is_on is updated in has_api_connectivity()
3998 *
3999 * @author Vova Feldman (@svovaf)
4000 * @since 1.0.9
4001 *
4002 * @return bool
4003 */
4004 function is_on() {
4005 self::$_static_logger->entrance();
4006
4007 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
4008 return false;
4009 }
4010
4011 if ( isset( $this->_is_on ) ) {
4012 return $this->_is_on;
4013 }
4014
4015 // If already installed or pending then sure it's on :)
4016 if ( $this->is_registered() || $this->is_pending_activation() ) {
4017 $this->_is_on = true;
4018
4019 return true;
4020 }
4021
4022 return false;
4023 }
4024
4025 /**
4026 * @author Vova Feldman (@svovaf)
4027 * @since 1.1.7.3
4028 *
4029 * @param bool $flush_if_no_connectivity
4030 *
4031 * @return bool
4032 */
4033 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
4034 if ( ! isset( $this->_storage->connectivity_test ) ) {
4035 // Connectivity test was never executed, or cache was cleared.
4036 return true;
4037 }
4038
4039 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
4040 if ( WP_FS__IS_HTTP_REQUEST ) {
4041 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
4042 // Domain changed.
4043 return true;
4044 }
4045
4046 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
4047 // Server IP changed.
4048 return true;
4049 }
4050 }
4051 }
4052
4053 if ( $this->_storage->connectivity_test['is_connected'] &&
4054 $this->_storage->connectivity_test['is_active']
4055 ) {
4056 // API connected and Freemius is active - no need to run connectivity check.
4057 return false;
4058 }
4059
4060 if ( $flush_if_no_connectivity ) {
4061 /**
4062 * If explicitly asked to flush when no connectivity - do it only
4063 * if at least 10 sec passed from the last API connectivity test.
4064 */
4065 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
4066 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
4067 }
4068
4069 /**
4070 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
4071 */
4072 $version = $this->get_plugin_version();
4073 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
4074 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
4075 return true;
4076 }
4077
4078 return false;
4079 }
4080
4081 /**
4082 * @author Leo Fajardo (@leorw)
4083 * @since 2.5.4
4084 *
4085 * @param bool $is_update
4086 *
4087 * @return bool
4088 */
4089 private function should_turn_fs_on( $is_update = true ) {
4090 if (
4091 empty( $this->_plugin->opt_in_moderation ) ||
4092 ! is_array( $this->_plugin->opt_in_moderation )
4093 ) {
4094 return true;
4095 }
4096
4097 $optin_config = $this->_plugin->opt_in_moderation;
4098
4099 if (
4100 WP_FS__IS_LOCALHOST &&
4101 ( ! isset( $optin_config['localhost'] ) || false !== $optin_config['localhost'] )
4102 ) {
4103 return true;
4104 }
4105
4106 $optin_config_key = $is_update ?
4107 'updates' :
4108 'new';
4109
4110 if ( ! isset( $optin_config[ $optin_config_key ] ) ) {
4111 return true;
4112 }
4113
4114 $visibility_percentage = $optin_config[ $optin_config_key ];
4115
4116 if ( 0 == $visibility_percentage ) {
4117 return false;
4118 }
4119
4120 if ( ! is_numeric( $visibility_percentage ) ) {
4121 return true;
4122 }
4123
4124 $min = 1;
4125 $max = 100;
4126
4127 if ( function_exists( 'random_int' ) ) {
4128 $random = random_int( $min, $max ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.random_intFound
4129 } else {
4130 $random = rand( $min, $max );
4131 }
4132
4133 return ( $random <= $visibility_percentage );
4134 }
4135
4136 /**
4137 * Check if there's any connectivity issue to Freemius API.
4138 *
4139 * @author Vova Feldman (@svovaf)
4140 * @since 1.0.9
4141 *
4142 * @param bool $flush_if_no_connectivity
4143 *
4144 * @return bool|null
4145 */
4146 function has_api_connectivity( $flush_if_no_connectivity = false ) {
4147 $this->_logger->entrance();
4148
4149 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
4150 return $this->_has_api_connection;
4151 }
4152
4153 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
4154 isset( $this->_storage->connectivity_test ) &&
4155 true === $this->_storage->connectivity_test['is_connected']
4156 ) {
4157 $this->clear_connectivity_info();
4158 }
4159
4160 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
4161 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
4162 /**
4163 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
4164 *
4165 * @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.
4166 */
4167 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
4168 $this->is_premium() ||
4169 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
4170
4171 return $this->_has_api_connection;
4172 }
4173
4174 if (
4175 ! empty( $this->_storage->connectivity_test ) &&
4176 isset( $this->_storage->connectivity_test['is_active'] )
4177 ) {
4178 $is_active = $this->_storage->connectivity_test['is_active'];
4179 } else {
4180 $is_active = $this->should_turn_fs_on( $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() ) );
4181
4182 $this->store_connectivity_info( (object) array( 'is_active' => $is_active ), null );
4183 }
4184
4185 if ( $is_active ) {
4186 $this->_is_on = true;
4187 }
4188
4189 return $this->_has_api_connection;
4190 }
4191
4192 /**
4193 * @author Leo Fajardo (@leorw)
4194 * @since 2.5.4
4195 */
4196 private function clear_connectivity_info() {
4197 unset( $this->_storage->connectivity_test );
4198
4199 FS_Api::clear_force_http_flag();
4200 }
4201
4202 /**
4203 * @author Vova Feldman (@svovaf)
4204 * @since 1.1.7.4
4205 *
4206 * @param object $pong
4207 * @param bool|null $is_connected
4208 */
4209 private function store_connectivity_info( $pong, $is_connected ) {
4210 $this->_logger->entrance();
4211
4212 $version = $this->get_plugin_version();
4213
4214 if ( false === $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
4215 $is_active = false;
4216 } else {
4217 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
4218 }
4219
4220 $is_active = $this->apply_filters(
4221 'is_on',
4222 $is_active,
4223 $this->is_plugin_update(),
4224 $version
4225 );
4226
4227 $this->_storage->connectivity_test = array(
4228 'is_connected' => $is_connected,
4229 'host' => $_SERVER['HTTP_HOST'],
4230 'server_ip' => WP_FS__REMOTE_ADDR,
4231 'is_active' => $is_active,
4232 'timestamp' => WP_FS__SCRIPT_START_TIME,
4233 // Last version with connectivity attempt.
4234 'version' => $version,
4235 );
4236
4237 $this->_has_api_connection = $is_connected;
4238 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
4239 }
4240
4241 /**
4242 * @author Leo Fajardo (@leorw)
4243 * @since 2.5.4
4244 *
4245 * @param bool $is_connected
4246 */
4247 private function update_connectivity_info( $is_connected ) {
4248 $this->store_connectivity_info(
4249 // 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.
4250 (object) array( 'is_active' => true ),
4251 $is_connected
4252 );
4253 }
4254
4255 /**
4256 * Force turning Freemius on.
4257 *
4258 * @author Vova Feldman (@svovaf)
4259 * @since 1.1.8.1
4260 *
4261 * @return bool TRUE if successfully turned on.
4262 */
4263 private function turn_on() {
4264 $this->_logger->entrance();
4265
4266 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
4267 return false;
4268 }
4269
4270 $updated_connectivity = $this->_storage->connectivity_test;
4271 $updated_connectivity['is_active'] = true;
4272 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
4273 $this->_storage->connectivity_test = $updated_connectivity;
4274
4275 $this->_is_on = true;
4276
4277 return true;
4278 }
4279
4280 /**
4281 * Anonymous and unique site identifier (Hash).
4282 *
4283 * @author Vova Feldman (@svovaf)
4284 * @since 1.1.0
4285 *
4286 * @param null|int $blog_id Since 2.0.0
4287 *
4288 * @return string
4289 */
4290 function get_anonymous_id( $blog_id = null ) {
4291 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
4292
4293 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
4294 $key = self::get_unfiltered_site_url( $blog_id, true );
4295
4296 $secure_auth = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
4297 if ( empty( $secure_auth ) ||
4298 false !== strpos( $secure_auth, ' ' ) ||
4299 'put your unique phrase here' === $secure_auth
4300 ) {
4301 // Protect against default auth key.
4302 $secure_auth = md5( microtime() );
4303 }
4304
4305 /**
4306 * Base the unique identifier on the WP secure authentication key. Which
4307 * turns the key into a secret anonymous identifier. This will help us
4308 * to avoid duplicate installs generation on the backend upon opt-in.
4309 *
4310 * @author Vova Feldman (@svovaf)
4311 * @since 1.2.3
4312 */
4313 $unique_id = md5( $key . $secure_auth );
4314
4315 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
4316 }
4317
4318 $this->_logger->departure( $unique_id );
4319
4320 return $unique_id;
4321 }
4322
4323 /**
4324 * Returns anonymous network ID.
4325 *
4326 * @since 2.4.3
4327 *
4328 * @return string
4329 */
4330 function get_anonymous_network_id() {
4331 return $this->get_anonymous_id( get_network()->site_id );
4332 }
4333
4334 /**
4335 * @author Vova Feldman (@svovaf)
4336 * @since 1.1.7.4
4337 *
4338 * @return \WP_User
4339 */
4340 static function _get_current_wp_user() {
4341 self::require_pluggable_essentials();
4342 self::wp_cookie_constants();
4343
4344 return wp_get_current_user();
4345 }
4346
4347 /**
4348 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
4349 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
4350 * is network activated the cookie constants are only configured after the network
4351 * plugins activation, therefore, if we don't define those constants WP will throw
4352 * PHP warnings/notices.
4353 *
4354 * @author Vova Feldman (@svovaf)
4355 * @since 2.1.1
4356 */
4357 private static function wp_cookie_constants() {
4358 if ( defined( 'LOGGED_IN_COOKIE' ) &&
4359 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
4360 ) {
4361 return;
4362 }
4363
4364 /**
4365 * Used to guarantee unique hash cookies
4366 *
4367 * @since 1.5.0
4368 */
4369 if ( ! defined( 'COOKIEHASH' ) ) {
4370 $siteurl = get_site_option( 'siteurl' );
4371 if ( $siteurl ) {
4372 define( 'COOKIEHASH', md5( $siteurl ) );
4373 } else {
4374 define( 'COOKIEHASH', '' );
4375 }
4376 }
4377
4378 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
4379 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
4380 }
4381
4382 /**
4383 * @since 2.5.0
4384 */
4385 if ( ! defined( 'AUTH_COOKIE' ) ) {
4386 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
4387 }
4388
4389 /**
4390 * @since 2.6.0
4391 */
4392 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
4393 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
4394 }
4395 }
4396
4397 /**
4398 * @author Vova Feldman (@svovaf)
4399 * @since 2.1.0
4400 *
4401 * @return int
4402 */
4403 static function get_current_wp_user_id() {
4404 $wp_user = self::_get_current_wp_user();
4405
4406 return $wp_user->ID;
4407 }
4408
4409 /**
4410 * @author Vova Feldman (@svovaf)
4411 * @since 1.2.1.7
4412 *
4413 * @param string $email
4414 *
4415 * @return bool
4416 */
4417 static function is_valid_email( $email ) {
4418 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4419 return false;
4420 }
4421
4422 $parts = explode( '@', $email );
4423
4424 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4425 return false;
4426 }
4427
4428 $blacklist = array(
4429 'admin.',
4430 'webmaster.',
4431 'localhost.',
4432 'dev.',
4433 'development.',
4434 'test.',
4435 'stage.',
4436 'staging.',
4437 );
4438
4439 // Make sure domain is not one of the blacklisted.
4440 foreach ( $blacklist as $invalid ) {
4441 if ( 0 === strpos( $parts[1], $invalid ) ) {
4442 return false;
4443 }
4444 }
4445
4446 // Get the UTF encoded domain name.
4447 /**
4448 * @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.
4449 * @author - @swashata
4450 */
4451 $is_new_idn_available = (
4452 version_compare( PHP_VERSION, '5.6.40') > 0 &&
4453 defined( 'IDNA_DEFAULT' ) &&
4454 defined( 'INTL_IDNA_VARIANT_UTS46' )
4455 );
4456 if ( $is_new_idn_available ) {
4457 $domain = idn_to_ascii( $parts[1], IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 );
4458 } else {
4459 $domain = idn_to_ascii( $parts[1] ); // phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet
4460 }
4461
4462 $domain = $domain . '.';
4463
4464 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4465 }
4466
4467 #endregion
4468
4469 #----------------------------------------------------------------------------------
4470 #region Email
4471 #----------------------------------------------------------------------------------
4472
4473 /**
4474 * Generates and sends an HTML email with customizable sections.
4475 *
4476 * @author Leo Fajardo (@leorw)
4477 * @since 1.1.2
4478 *
4479 * @param string $to_address
4480 * @param string $subject
4481 * @param array $sections
4482 * @param array $headers
4483 *
4484 * @return bool Whether the email contents were sent successfully.
4485 */
4486 private function send_email(
4487 $to_address,
4488 $subject,
4489 $sections = array(),
4490 $headers = array()
4491 ) {
4492 $default_sections = $this->get_email_sections();
4493
4494 // Insert new sections or replace the default email sections.
4495 if ( is_array( $sections ) && ! empty( $sections ) ) {
4496 foreach ( $sections as $section_id => $custom_section ) {
4497 if ( ! isset( $default_sections[ $section_id ] ) ) {
4498 // If the section does not exist, add it.
4499 $default_sections[ $section_id ] = $custom_section;
4500 } else {
4501 // If the section already exists, override it.
4502 $current_section = $default_sections[ $section_id ];
4503
4504 // Replace the current section's title if a custom section title exists.
4505 if ( isset( $custom_section['title'] ) ) {
4506 $current_section['title'] = $custom_section['title'];
4507 }
4508
4509 // Insert new rows under the current section or replace the default rows.
4510 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4511 foreach ( $custom_section['rows'] as $row_id => $row ) {
4512 $current_section['rows'][ $row_id ] = $row;
4513 }
4514 }
4515
4516 $default_sections[ $section_id ] = $current_section;
4517 }
4518 }
4519 }
4520
4521 $vars = array( 'sections' => $default_sections );
4522 $message = fs_get_template( 'email.php', $vars );
4523
4524 // Set the type of email to HTML.
4525 $headers[] = 'Content-type: text/html; charset=UTF-8';
4526
4527 $header_string = implode( "\r\n", $headers );
4528
4529 return wp_mail(
4530 $to_address,
4531 $subject,
4532 $message,
4533 $header_string
4534 );
4535 }
4536
4537 /**
4538 * Generates the data for the sections of the email content.
4539 *
4540 * @author Leo Fajardo (@leorw)
4541 * @since 1.1.2
4542 *
4543 * @return array
4544 */
4545 private function get_email_sections() {
4546 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4547 $current_user = self::_get_current_wp_user();
4548
4549 // Retrieve the cURL version information so that we can get the version number below.
4550 $curl_version_information = curl_version();
4551
4552 $active_plugin = self::get_active_plugins();
4553
4554 // Generate the list of active plugins separated by new line.
4555 $active_plugin_string = '';
4556 foreach ( $active_plugin as $plugin ) {
4557 $active_plugin_string .= sprintf(
4558 '<a href="%s">%s</a> [v%s]<br>',
4559 $plugin['PluginURI'],
4560 $plugin['Name'],
4561 $plugin['Version']
4562 );
4563 }
4564
4565 $server_ip = WP_FS__REMOTE_ADDR;
4566
4567 // Add PHP info for deeper investigation.
4568 ob_start();
4569 phpinfo();
4570 $php_info = ob_get_clean();
4571
4572 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4573
4574 // Generate the default email sections.
4575 $sections = array(
4576 'sdk' => array(
4577 'title' => 'SDK',
4578 'rows' => array(
4579 'fs_version' => array( 'FS Version', $this->version ),
4580 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4581 )
4582 ),
4583 'plugin' => array(
4584 'title' => ucfirst( $this->get_module_type() ),
4585 'rows' => array(
4586 'name' => array( 'Name', $this->get_plugin_name() ),
4587 'version' => array( 'Version', $this->get_plugin_version() )
4588 )
4589 ),
4590 'api' => array(
4591 'title' => 'API Subdomain',
4592 'rows' => array(
4593 'dns' => array(
4594 'DNS_CNAME',
4595 function_exists( 'dns_get_record' ) ?
4596 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4597 'dns_get_record() disabled/blocked'
4598 ),
4599 'ip' => array(
4600 'IP',
4601 function_exists( 'gethostbyname' ) ?
4602 gethostbyname( $api_domain ) :
4603 'gethostbyname() disabled/blocked'
4604 ),
4605 ),
4606 ),
4607 'site' => array(
4608 'title' => 'Site',
4609 'rows' => array(
4610 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4611 'address' => array( 'Address', site_url() ),
4612 'host' => array(
4613 'HTTP_HOST',
4614 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4615 ),
4616 'hosting' => array(
4617 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4618 fs_request_get( 'hosting_company' ) :
4619 'Unknown',
4620 ),
4621 'server_addr' => array(
4622 'SERVER_ADDR',
4623 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4624 )
4625 )
4626 ),
4627 'user' => array(
4628 'title' => 'User',
4629 'rows' => array(
4630 'email' => array( 'Email', $current_user->user_email ),
4631 'first' => array( 'First', $current_user->user_firstname ),
4632 'last' => array( 'Last', $current_user->user_lastname )
4633 )
4634 ),
4635 'plugins' => array(
4636 'title' => 'Plugins',
4637 'rows' => array(
4638 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4639 )
4640 ),
4641 'php_info' => array(
4642 'title' => 'PHP Info',
4643 'rows' => array(
4644 'info' => array( $php_info )
4645 ),
4646 )
4647 );
4648
4649 // Allow the sections to be modified by other code.
4650 $sections = $this->apply_filters( 'email_template_sections', $sections );
4651
4652 return $sections;
4653 }
4654
4655 #endregion
4656
4657 #----------------------------------------------------------------------------------
4658 #region Initialization
4659 #----------------------------------------------------------------------------------
4660
4661 /**
4662 * Init plugin's Freemius instance.
4663 *
4664 * @author Vova Feldman (@svovaf)
4665 * @since 1.0.1
4666 *
4667 * @param number $id
4668 * @param string $public_key
4669 * @param bool $is_live
4670 * @param bool $is_premium
4671 */
4672 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4673 $this->_logger->entrance();
4674
4675 $this->dynamic_init( array(
4676 'id' => $id,
4677 'public_key' => $public_key,
4678 'is_live' => $is_live,
4679 'is_premium' => $is_premium,
4680 ) );
4681 }
4682
4683 /**
4684 * Dynamic initiator, originally created to support initiation
4685 * with parent_id for add-ons.
4686 *
4687 * @author Vova Feldman (@svovaf)
4688 * @since 1.0.6
4689 *
4690 * @param array $plugin_info
4691 *
4692 * @throws Freemius_Exception
4693 */
4694 function dynamic_init( array $plugin_info ) {
4695 $this->_logger->entrance();
4696
4697 $this->parse_settings( $plugin_info );
4698
4699 $this->register_after_settings_parse_hooks();
4700
4701 /**
4702 * 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.
4703 *
4704 * @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.
4705 */
4706 if ( $this->is_anonymous() ) {
4707 $is_network_level = ( $this->_is_network_active && fs_is_network_admin() );
4708
4709 if (
4710 ! $is_network_level ||
4711 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
4712 ) {
4713 if ( $this->is_paying_or_trial() ) {
4714 $this->reset_anonymous_mode( $is_network_level );
4715 }
4716 } else {
4717 $network = get_network();
4718
4719 if ( is_object( $network ) ) {
4720 $main_blog_id = $network->site_id;
4721 $first_install = $this->get_install_by_blog_id( $main_blog_id );
4722
4723 if ( is_object( $first_install ) ) {
4724 $this->_storage->network_install_blog_id = $main_blog_id;
4725 $this->_storage->network_user_id = $first_install->user_id;
4726 }
4727 }
4728 }
4729 }
4730
4731 if ( $this->should_stop_execution() ) {
4732 return;
4733 }
4734
4735 if ( ! $this->is_registered() ) {
4736 if ( $this->is_anonymous() ) {
4737 // If user skipped, no need to test connectivity.
4738 $this->_has_api_connection = true;
4739 $this->_is_on = true;
4740 } else {
4741 if ( false === $this->has_api_connectivity() ) {
4742 return;
4743 } else {
4744 if ( $this->_anonymous_mode ) {
4745 // Simulate anonymous mode.
4746 $this->_is_anonymous = true;
4747 }
4748 }
4749 }
4750 }
4751
4752 /**
4753 * This should be executed even if Freemius is off for the core module,
4754 * otherwise, the add-ons dialog box won't work properly. This is especially
4755 * relevant when the developer decided to turn FS off for existing users.
4756 *
4757 * @author Vova Feldman (@svovaf)
4758 */
4759 if ( $this->is_user_in_admin() &&
4760 'plugin-information' === fs_request_get( 'tab', false ) &&
4761 $this->should_use_freemius_updater_and_dialog() &&
4762 (
4763 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4764 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4765 )
4766 ) {
4767 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4768
4769 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4770 }
4771
4772 // Check if Freemius is on for the current plugin.
4773 // This MUST be executed after all the plugin variables has been loaded.
4774 if ( ! $this->is_registered() && ! $this->is_on() ) {
4775 return;
4776 }
4777
4778 if ( $this->has_api_connectivity() ) {
4779 if ( self::is_cron() ) {
4780 $this->hook_callback_to_sync_cron();
4781 } else if ( $this->is_user_in_admin() ) {
4782 /**
4783 * Schedule daily data sync cron if:
4784 *
4785 * 1. User opted-in (for tracking).
4786 * 2. If skipped, but later upgraded (opted-in via upgrade).
4787 *
4788 * @author Vova Feldman (@svovaf)
4789 * @since 1.1.7.3
4790 *
4791 */
4792 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
4793 $this->maybe_schedule_sync_cron();
4794 }
4795
4796 /**
4797 * Check if requested for manual blocking background sync.
4798 */
4799 if ( fs_request_has( 'background_sync' ) ) {
4800 self::require_pluggable_essentials();
4801 self::wp_cookie_constants();
4802
4803 $this->run_manual_sync();
4804 }
4805 }
4806 }
4807
4808 if ( $this->is_registered() ) {
4809 FS_Clone_Manager::instance()->maybe_resolve_new_subsite_install_automatically( $this );
4810
4811 $this->hook_callback_to_install_sync();
4812 }
4813
4814 if ( $this->is_addon() ) {
4815 if ( $this->is_parent_plugin_installed() ) {
4816 // Link to parent FS.
4817 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4818
4819 // Get parent plugin reference.
4820 $this->_parent_plugin = $this->_parent->get_plugin();
4821 }
4822 }
4823
4824 if ( $this->is_user_in_admin() ) {
4825 if ( $this->is_registered() && fs_request_has( 'purchase_completed' ) ) {
4826 $this->_admin_notices->add_sticky(
4827 sprintf(
4828 /* translators: %s: License type (e.g. you have a professional license) */
4829 $this->get_text_inline( 'You have purchased a %s license.', 'you-have-x-license' ),
4830 fs_request_get( 'purchased_plan' )
4831 ) .
4832 sprintf(
4833 $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' ),
4834 $this->get_module_label( true ),
4835 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? "products' " : '' ),
4836 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? 's' : '' ),
4837 sprintf(
4838 '<strong>%s</strong>',
4839 fs_request_get( 'purchase_email' )
4840 )
4841 ),
4842 'plan_purchased',
4843 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
4844 );
4845 }
4846
4847 if ( $this->is_addon() ) {
4848 if ( ! $this->is_parent_plugin_installed() ) {
4849 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4850
4851 if ( isset( $plugin_info['parent'] ) ) {
4852 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4853 }
4854
4855 $this->_admin_notices->add(
4856 ( ! empty( $parent_name ) ?
4857 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 ) :
4858 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() )
4859 ),
4860 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4861 'error'
4862 );
4863
4864 return;
4865 } else {
4866 $is_network_admin = fs_is_network_admin();
4867
4868 if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4869 // If add-on activated and parent not, automatically install parent for the user.
4870 $this->activate_parent_account( $this->_parent );
4871 } else if (
4872 $this->_parent->is_registered() &&
4873 ! $this->is_registered() &&
4874 /**
4875 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
4876 * * Network active and in network admin - network activate add-on account.
4877 * * Network active and not in network admin - activate add-on account for the current blog.
4878 * * Not network active and not in network admin - activate add-on account for the current blog.
4879 *
4880 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
4881 *
4882 * @author Leo Fajardo (@leorw)
4883 * @since 2.3.0
4884 */
4885 ( $this->is_network_active() || ! $is_network_admin )
4886 ) {
4887 $premium_license = null;
4888
4889 if (
4890 ! $this->has_free_plan() &&
4891 $this->is_bundle_license_auto_activation_enabled() &&
4892 $this->_parent->is_activated_with_bundle_license()
4893 ) {
4894 /**
4895 * If the add-on has no free plan, try to activate the account only when there's a bundle license.
4896 *
4897 * @author Leo Fajardo (@leorw)
4898 * @since 2.4.0
4899 */
4900 $bundle_license = $this->get_active_parent_license( $this->_parent->_get_license()->secret_key, false );
4901
4902 if (
4903 is_object( $bundle_license ) &&
4904 ! empty( $bundle_license->products ) &&
4905 in_array( $this->get_id(), $bundle_license->products )
4906 ) {
4907 $premium_license = $bundle_license;
4908 }
4909 }
4910
4911 if ( $this->has_free_plan() || is_object( $premium_license) ) {
4912 // If parent plugin activated, automatically install add-on for the user.
4913 $this->_activate_addon_account(
4914 $this->_parent,
4915 ( $this->is_network_active() && $is_network_admin ) ?
4916 true :
4917 get_current_blog_id(),
4918 $premium_license
4919 );
4920 }
4921 }
4922
4923 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4924 if ( $this->is_premium() ) {
4925 // Remove add-on download admin-notice.
4926 $this->_parent->_admin_notices->remove_sticky( array(
4927 'addon_plan_upgraded_' . $this->_slug,
4928 'no_addon_license_' . $this->_slug,
4929 ) );
4930 }
4931
4932 // $this->deactivate_premium_only_addon_without_license();
4933 }
4934 }
4935
4936 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
4937
4938 // if ( $this->is_registered() ||
4939 // $this->is_anonymous() ||
4940 // $this->is_pending_activation()
4941 // ) {
4942 // $this->_init_admin();
4943 // }
4944 }
4945
4946 /**
4947 * Should be called outside `$this->is_user_in_admin()` scope
4948 * because the updater has some logic that needs to be executed
4949 * during AJAX calls.
4950 *
4951 * Currently, we need to hook to the `http_request_host_is_external` filter.
4952 * In the future, there might be additional logic added.
4953 *
4954 * @author Vova Feldman
4955 * @since 1.2.1.6
4956 */
4957 if (
4958 $this->should_use_freemius_updater_and_dialog() &&
4959 (
4960 $this->is_premium() ||
4961 /**
4962 * If not premium but the premium version is installed, also instantiate the updater so that the
4963 * plugin information dialog of the premium version will have the information from the server.
4964 *
4965 * @author Leo Fajardo (@leorw)
4966 * @since 2.2.3
4967 */
4968 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
4969 ) &&
4970 $this->has_release_on_freemius() &&
4971 ( ! $this->is_unresolved_clone( true ) )
4972 ) {
4973 FS_Plugin_Updater::instance( $this );
4974 }
4975
4976 $this->do_action( 'initiated' );
4977
4978 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
4979 if ( isset( $this->_storage->prev_is_premium ) ) {
4980 $this->apply_filters(
4981 'after_code_type_change',
4982 // New code type.
4983 $this->_plugin->is_premium
4984 );
4985 } else {
4986 // Set for code type for the first time.
4987 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
4988 }
4989 }
4990
4991 if ( ! $this->is_addon() ) {
4992 if ( $this->is_registered() ) {
4993 // Fix for upgrade from versions < 1.0.9.
4994 if ( ! isset( $this->_storage->activation_timestamp ) ) {
4995 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
4996 }
4997
4998 $this->do_action( 'after_init_plugin_registered' );
4999 } else if ( $this->is_anonymous() ) {
5000 $this->do_action( 'after_init_plugin_anonymous' );
5001 } else if ( $this->is_pending_activation() ) {
5002 $this->do_action( 'after_init_plugin_pending_activations' );
5003 }
5004 } else {
5005 if ( $this->is_registered() ) {
5006 $this->do_action( 'after_init_addon_registered' );
5007 } else if ( $this->is_anonymous() ) {
5008 $this->do_action( 'after_init_addon_anonymous' );
5009 } else if ( $this->is_pending_activation() ) {
5010 $this->do_action( 'after_init_addon_pending_activations' );
5011 }
5012 }
5013 }
5014
5015 /**
5016 * @author Leo Fajardo (@leorw)
5017 * @since 2.2.3
5018 *
5019 * @return bool
5020 */
5021 private function should_use_freemius_updater_and_dialog() {
5022 return (
5023 /**
5024 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
5025 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
5026 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
5027 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
5028 * plugin details from .org).
5029 */
5030 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
5031 (
5032 ! self::is_plugin_install_page() &&
5033 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
5034 ( 'install-plugin' !== fs_request_get( 'action' ) )
5035 )
5036 );
5037 }
5038
5039 /**
5040 * @param string[] $permissions
5041 * @param bool $is_enabled
5042 * @param int|null $blog_id
5043 *
5044 * @return true|object `true` on success, API error object on failure.
5045 */
5046 private function update_site_permissions( array $permissions, $is_enabled, $blog_id = null ) {
5047 $this->_logger->entrance();
5048
5049 $params = array(
5050 'permissions' => implode( ',', $permissions ),
5051 'is_enabled' => $is_enabled,
5052 );
5053
5054 $current_blog_id = get_current_blog_id();
5055 $is_blog_switched = false;
5056 if ( is_numeric( $blog_id ) && $current_blog_id != $blog_id ) {
5057 $is_blog_switched = $this->switch_to_blog( $blog_id );
5058 }
5059
5060 $result = $this->api_site_call( '/permissions.json', 'put', $params );
5061
5062 if ( $is_blog_switched ) {
5063 $this->switch_to_blog( $current_blog_id );
5064 }
5065
5066 if (
5067 ! $this->is_api_result_object( $result ) ||
5068 ! isset( $result->install_id )
5069 ) {
5070 $this->_logger->api_error( $result );
5071
5072 return $result;
5073 }
5074
5075 return true;
5076 }
5077
5078 /**
5079 * @param string[] $permissions
5080 * @param bool $is_enabled
5081 * @param bool $has_site_delegated_connection
5082 *
5083 * @return true|object `true` on success, API error object on failure.
5084 */
5085 private function update_network_permissions(
5086 array $permissions,
5087 $is_enabled,
5088 &$has_site_delegated_connection
5089 ) {
5090 $this->_logger->entrance();
5091
5092 $install_id_2_blog_id = array();
5093 $install_by_blog_id = $this->get_blog_install_map();
5094
5095 $has_site_delegated_connection = false;
5096
5097 foreach ( $install_by_blog_id as $blog_id => $install ) {
5098 if ( $this->is_site_delegated_connection( $blog_id ) ) {
5099 // Only update permissions of non-delegated installs.
5100 $has_site_delegated_connection = true;
5101 continue;
5102 }
5103
5104 $install_id_2_blog_id[ $install->id ] = $blog_id;
5105 }
5106
5107 if ( empty( $install_id_2_blog_id ) ) {
5108 return true;
5109 }
5110
5111 $params = array(
5112 'permissions' => implode( ',', $permissions ),
5113 'is_enabled' => $is_enabled,
5114 'install_ids' => implode( ',', array_keys( $install_id_2_blog_id ) ),
5115 );
5116
5117 // Send update to FS.
5118 $result = $this->get_current_or_network_user_api_scope()->call(
5119 "/plugins/{$this->_module_id}/installs/permissions.json",
5120 'put',
5121 $params
5122 );
5123
5124 if ( ! $this->is_api_result_object( $result, 'installs_metadata' ) ) {
5125 $this->_logger->api_error( $result );
5126
5127 return $result;
5128 }
5129
5130 return true;
5131 }
5132
5133 /**
5134 * @param mixed $result
5135 *
5136 * @return string
5137 */
5138 private function get_api_error_message( $result ) {
5139 $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:',
5140 'unexpected-api-error' ), $this->_module_type ) . ' ';
5141
5142 if (
5143 $this->is_api_error( $result ) &&
5144 isset( $result->error )
5145 ) {
5146 $code = empty( $result->error->code ) ? '' : " Code: {$result->error->code}";
5147
5148 $error_message .= "<b>{$result->error->message}{$code}</b>";
5149 } else {
5150 $error_message .= var_export( $result, true );
5151 }
5152
5153 return $error_message;
5154 }
5155
5156 /**
5157 * @author Vova Feldman (@svovaf)
5158 * @since 2.5.1
5159 */
5160 function _toggle_permission_tracking_callback() {
5161 $this->_logger->entrance();
5162
5163 $this->check_ajax_referer( 'toggle_permission_tracking' );
5164
5165 if ( ! $this->is_registered( true ) ) {
5166 self::shoot_ajax_failure( 'User never opted-in.' );
5167 }
5168
5169 $is_enabled = fs_request_get_bool( 'is_enabled' );
5170 $permissions = fs_request_get( 'permissions' );
5171
5172 if ( ! is_string( $permissions ) ) {
5173 self::shoot_ajax_failure( 'The permissions param must be a string.' );
5174 }
5175
5176 $permissions = explode( ',', $permissions );
5177
5178 $result = $this->toggle_permission_tracking( $permissions, $is_enabled );
5179
5180 if ( true !== $result ) {
5181 self::shoot_ajax_failure( $this->get_api_error_message( $result ) );
5182 }
5183
5184 self::shoot_ajax_success();
5185 }
5186
5187 /**
5188 * @param string[] $permissions
5189 * @param bool $is_enabled
5190 * @param int|null $blog_id
5191 *
5192 * @return bool|mixed `true` if updated successfully or no update is needed.
5193 */
5194 private function toggle_permission_tracking( $permissions, $is_enabled, $blog_id = null ) {
5195 if ( ! $this->is_registered( true ) ) {
5196 // User never opted-in.
5197 return false;
5198 }
5199
5200 // Check if permissions are already set as needed.
5201 if ( FS_Permission_Manager::instance( $this )->are_permissions( $permissions, $is_enabled, $blog_id ) ) {
5202 /**
5203 * Note:
5204 * 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.
5205 */
5206 return true;
5207 }
5208
5209 $api_managed_permissions = array_intersect(
5210 $permissions,
5211 FS_Permission_Manager::get_api_managed_permission_ids()
5212 );
5213
5214 if (
5215 in_array( FS_Permission_Manager::PERMISSION_ESSENTIALS, $permissions ) &&
5216 ! in_array( FS_Permission_Manager::PERMISSION_SITE, $permissions )
5217 ) {
5218 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_SITE;
5219 }
5220
5221 if ( ! empty( $api_managed_permissions ) ) {
5222 $has_site_delegated_connection = false;
5223
5224 if (
5225 ! $is_enabled &&
5226 ! in_array( FS_Permission_Manager::PERMISSION_EXTENSIONS, $api_managed_permissions ) &&
5227 false === FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed( $blog_id )
5228 ) {
5229 /**
5230 * 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.
5231 *
5232 * @todo Remove this entire `if` after implementing granular opt-in that also sends the permissions to the API when opting in.
5233 */
5234 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_EXTENSIONS;
5235 }
5236
5237 if ( is_null( $blog_id ) && fs_is_network_admin() ) {
5238 $result = $this->update_network_permissions(
5239 $api_managed_permissions,
5240 $is_enabled,
5241 $has_site_delegated_connection
5242 );
5243 } else {
5244 $result = $this->update_site_permissions(
5245 $api_managed_permissions,
5246 $is_enabled,
5247 $blog_id
5248 );
5249 }
5250
5251 if ( true !== $result ) {
5252 return $result;
5253 }
5254
5255 if ( in_array( FS_Permission_Manager::PERMISSION_SITE, $api_managed_permissions ) ) {
5256 if ( $is_enabled ) {
5257 $this->schedule_sync_cron();
5258 } else {
5259 $this->clear_sync_cron( ! $has_site_delegated_connection );
5260 }
5261 }
5262
5263 if ( in_array( FS_Permission_Manager::PERMISSION_USER, $api_managed_permissions ) ) {
5264 $this->toggle_user_permission( $is_enabled, $blog_id );
5265 }
5266 }
5267
5268 $this->update_tracking_permissions(
5269 $permissions,
5270 $is_enabled,
5271 $blog_id
5272 );
5273
5274 return true;
5275 }
5276
5277 /**
5278 * @param bool $is_enabled
5279 * @param int|null $blog_id
5280 */
5281 private function toggle_user_permission( $is_enabled, $blog_id = null ) {
5282 $network_or_blog_ids = is_numeric( $blog_id ) ?
5283 $blog_id :
5284 fs_is_network_admin();
5285
5286 if ( $is_enabled ) {
5287 $this->reset_anonymous_mode( $network_or_blog_ids );
5288 } else {
5289 $this->skip_connection( $network_or_blog_ids );
5290 }
5291 }
5292
5293 /**
5294 * Opt-in back into usage tracking.
5295 *
5296 * Note: This will only work if the user opted-in previously.
5297 *
5298 * Returns:
5299 * 1. FALSE - If the user never opted-in.
5300 * 2. TRUE - If successfully opted-in back to usage tracking.
5301 * 3. object - API result on failure.
5302 *
5303 * @author Leo Fajardo (@leorw)
5304 * @since 1.2.1.5
5305 *
5306 * @bool $is_enabled
5307 *
5308 * @return bool|object
5309 */
5310 private function toggle_site_tracking( $is_enabled, $blog_id = null ) {
5311 $this->_logger->entrance();
5312
5313 return $this->toggle_permission_tracking(
5314 FS_Permission_Manager::instance( $this )->get_site_tracking_permission_names(),
5315 $is_enabled,
5316 $blog_id
5317 );
5318 }
5319
5320 /**
5321 * If user opted-in and later disabled usage-tracking,
5322 * re-allow tracking for licensing and updates.
5323 *
5324 * @author Leo Fajardo (@leorw)
5325 * @since 1.2.1.5
5326 *
5327 * @param bool $is_context_single_site
5328 */
5329 private function reconnect_locally( $is_context_single_site = false ) {
5330 $this->_logger->entrance();
5331
5332 if ( ! $this->is_registered() ) {
5333 return;
5334 }
5335
5336 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5337 if ( $this->is_tracking_prohibited() ) {
5338 FS_Permission_Manager::instance( $this )->update_site_tracking( true );
5339 }
5340 } else {
5341 $installs_map = $this->get_blog_install_map();
5342 foreach ( $installs_map as $blog_id => $install ) {
5343 /**
5344 * @var FS_Site $install
5345 */
5346 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
5347 FS_Permission_Manager::instance( $this )->update_site_tracking( true, $blog_id );
5348 }
5349 }
5350 }
5351 }
5352
5353 /**
5354 * 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.
5355 *
5356 * @param string[] $permissions
5357 * @param bool $is_enabled
5358 * @param int|null $blog_id
5359 *
5360 * @return array
5361 */
5362 private function update_tracking_permissions( $permissions, $is_enabled, $blog_id = null ) {
5363 // Alias.
5364 $permission_manager = FS_Permission_Manager::instance( $this );
5365
5366 $network_or_blog_ids = is_numeric( $blog_id ) ?
5367 $blog_id :
5368 fs_is_network_admin();
5369
5370 if ( true === $network_or_blog_ids ) {
5371 // Update the permission for all non-delegated sub-sites.
5372 $blog_ids = $this->get_non_delegated_blog_ids();
5373
5374 // Add the network-level to the array, to update the permission on the network-level storage.
5375 array_unshift( $blog_ids, null );
5376 }
5377 else
5378 {
5379 if ( false === $network_or_blog_ids ) {
5380 $network_or_blog_ids = null;
5381 }
5382
5383 $blog_ids = is_array( $network_or_blog_ids ) ?
5384 $network_or_blog_ids :
5385 array( $network_or_blog_ids );
5386 }
5387
5388 $result = array();
5389 foreach ( $permissions as $permission ) {
5390 $permission = trim( $permission );
5391 $is_permission_supported = true;
5392
5393 foreach ( $blog_ids as $id ) {
5394 $is_permission_supported = $permission_manager->update_permission_tracking_flag(
5395 $permission,
5396 $is_enabled,
5397 $id
5398 );
5399 }
5400
5401 if ( ! $is_permission_supported ) {
5402 $permission = 'no_match';
5403 }
5404
5405 $result[ $permission ] = $is_enabled;
5406 }
5407
5408 return $result;
5409 }
5410
5411 /**
5412 * Parse plugin's settings (as defined by the plugin dev).
5413 *
5414 * @author Vova Feldman (@svovaf)
5415 * @since 1.1.7.3
5416 *
5417 * @param array $plugin_info
5418 *
5419 * @throws \Freemius_Exception
5420 */
5421 private function parse_settings( &$plugin_info ) {
5422 $this->_logger->entrance();
5423
5424 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5425 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5426 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5427 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5428 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5429
5430 /**
5431 * @author Vova Feldman (@svovaf)
5432 * @since 1.1.9 Try to pull secret key from external config.
5433 */
5434 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5435 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5436 }
5437
5438 if ( isset( $plugin_info['parent'] ) ) {
5439 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5440 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5441 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5442 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5443 }
5444
5445 if ( false === $id ) {
5446 throw new Freemius_Exception( array(
5447 'error' => array(
5448 'type' => 'ParameterNotSet',
5449 'message' => 'Plugin id parameter is not set.',
5450 'code' => 'plugin_id_not_set',
5451 'http' => 500,
5452 )
5453 ) );
5454 }
5455 if ( false === $public_key ) {
5456 throw new Freemius_Exception( array(
5457 'error' => array(
5458 'type' => 'ParameterNotSet',
5459 'message' => 'Plugin public_key parameter is not set.',
5460 'code' => 'plugin_public_key_not_set',
5461 'http' => 500,
5462 )
5463 ) );
5464 }
5465
5466 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5467 $this->_plugin :
5468 new FS_Plugin();
5469
5470 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5471
5472 $plugin->update( array(
5473 'id' => $id,
5474 'type' => $this->get_option( $plugin_info, 'type', $this->_module_type ),
5475 'public_key' => $public_key,
5476 'slug' => $this->_slug,
5477 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5478 'parent_plugin_id' => $parent_id,
5479 'version' => $this->get_plugin_version(),
5480 'title' => $this->get_plugin_name( $premium_suffix ),
5481 'file' => $this->_plugin_basename,
5482 'is_premium' => $this->get_bool_option( $plugin_info, 'is_premium', true ),
5483 'premium_suffix' => $premium_suffix,
5484 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5485 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5486 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5487 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ),
5488 'opt_in_moderation' => $this->get_option( $plugin_info, 'opt_in', null ),
5489 ) );
5490
5491 if ( $plugin->is_updated() ) {
5492 // Update plugin details.
5493 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5494 }
5495 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5496 $this->_plugin->secret_key = $secret_key;
5497
5498 /**
5499 * 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).
5500 *
5501 * @author Vova Feldman
5502 * @since 2.4.5
5503 */
5504 if ( $this->is_network_active() && fs_is_network_admin() ) {
5505 if ( isset( $plugin_info['menu_network'] ) &&
5506 is_array( $plugin_info['menu_network'] ) &&
5507 ! empty( $plugin_info['menu_network'] )
5508 ) {
5509 $plugin_info['menu'] = $plugin_info['menu_network'];
5510 }
5511 }
5512
5513 if ( ! isset( $plugin_info['menu'] ) ) {
5514 $plugin_info['menu'] = array();
5515
5516 if ( ! empty( $this->_storage->sdk_last_version ) &&
5517 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5518 ) {
5519 // Backward compatibility to 1.1.2
5520 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5521 $plugin_info['menu_slug'] :
5522 $this->_slug;
5523 }
5524 }
5525
5526 $this->_menu = FS_Admin_Menu_Manager::instance(
5527 $this->_module_id,
5528 $this->_module_type,
5529 $this->get_unique_affix()
5530 );
5531
5532 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5533
5534 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5535 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5536 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5537 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5538 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5539 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5540 if ( $this->_is_premium_only ) {
5541 // If premium only plugin, disable anonymous mode.
5542 $this->_enable_anonymous = false;
5543 $this->_anonymous_mode = false;
5544 } else {
5545 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5546 $this->_anonymous_mode = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
5547 }
5548 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5549 $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
5550
5551 if ( ! empty( $plugin_info['trial'] ) ) {
5552 $this->_trial_days = $this->get_numeric_option(
5553 $plugin_info['trial'],
5554 'days',
5555 // Default to 0 - trial without days specification.
5556 0
5557 );
5558
5559 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5560 }
5561
5562 $this->_navigation = $this->get_option(
5563 $plugin_info,
5564 'navigation',
5565 $this->is_free_wp_org_theme() ?
5566 self::NAVIGATION_TABS :
5567 self::NAVIGATION_MENU
5568 );
5569 }
5570
5571 /**
5572 * @param string[] $options
5573 * @param string $key
5574 * @param mixed $default
5575 *
5576 * @return bool
5577 */
5578 private function get_option( &$options, $key, $default = false ) {
5579 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5580 }
5581
5582 private function get_bool_option( &$options, $key, $default = false ) {
5583 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5584 }
5585
5586 private function get_numeric_option( &$options, $key, $default = false ) {
5587 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5588 }
5589
5590 /**
5591 * Gate keeper.
5592 *
5593 * @author Vova Feldman (@svovaf)
5594 * @since 1.1.7.3
5595 *
5596 * @return bool
5597 */
5598 private function should_stop_execution() {
5599 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5600 /**
5601 * Don't execute Freemius until plugin was fully loaded at least once,
5602 * to give the opportunity for the activation hook to run before pinging
5603 * the API for connectivity test. This logic is relevant for the
5604 * identification of new plugin install vs. plugin update.
5605 *
5606 * @author Vova Feldman (@svovaf)
5607 * @since 1.1.9
5608 */
5609 return true;
5610 }
5611
5612 if ( $this->is_activation_mode() ) {
5613 if ( ! is_admin() ) {
5614 /**
5615 * If in activation mode, don't execute Freemius outside the admin dashboard.
5616 *
5617 * @author Vova Feldman (@svovaf)
5618 * @since 1.1.7.3
5619 */
5620 return true;
5621 }
5622
5623 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5624 /**
5625 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5626 * then don't start Freemius.
5627 *
5628 * @author Vova Feldman (@svovaf)
5629 * @since 1.1.6.3
5630 *
5631 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5632 */
5633 return true;
5634 }
5635
5636 if ( self::is_cron() ) {
5637 /**
5638 * If in activation mode, don't execute Freemius during wp crons
5639 * (wp crons have HTTP context - called as HTTP request).
5640 *
5641 * @author Vova Feldman (@svovaf)
5642 * @since 1.1.7.3
5643 */
5644 return true;
5645 }
5646
5647 if ( self::is_ajax() ) {
5648 /**
5649 * During activation, if running in AJAX mode, unless there's a sticky
5650 * connectivity issue notice, don't run Freemius.
5651 *
5652 * @author Vova Feldman (@svovaf)
5653 * @since 1.1.7.3
5654 */
5655 return true;
5656 }
5657 }
5658
5659 return false;
5660 }
5661
5662 /**
5663 * Triggered after code type has changed.
5664 *
5665 * @author Vova Feldman (@svovaf)
5666 * @since 1.1.9.1
5667 */
5668 function _after_code_type_change() {
5669 $this->_logger->entrance();
5670
5671 if ( $this->is_theme() ) {
5672 // Expire the cache of the previous tabs since the theme may
5673 // have setting updates after code type has changed.
5674 $this->_cache->expire( 'tabs' );
5675 $this->_cache->expire( 'tabs_stylesheets' );
5676 }
5677
5678 if ( ! $this->is_addon() ) {
5679 add_action(
5680 is_admin() ? 'admin_init' : 'init',
5681 array( &$this, '_plugin_code_type_changed' )
5682 );
5683 }
5684
5685 if ( $this->is_registered() && $this->is_premium() ) {
5686 // Purge cached payments after switching to the premium version.
5687 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5688 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5689 }
5690 }
5691
5692 /**
5693 * Handles plugin's code type change (free <--> premium).
5694 *
5695 * @author Vova Feldman (@svovaf)
5696 * @since 1.0.9
5697 */
5698 function _plugin_code_type_changed() {
5699 $this->_logger->entrance();
5700
5701 if ( $this->is_premium() ) {
5702 $this->reconnect_locally();
5703
5704 // Activated premium code.
5705 $this->do_action( 'after_premium_version_activation' );
5706
5707 // Remove all sticky messages related to download of the premium version.
5708 $this->_admin_notices->remove_sticky( array(
5709 'trial_started',
5710 'plan_upgraded',
5711 'plan_changed',
5712 'license_activated',
5713 ) );
5714
5715 $notice = '';
5716 if ( ! $this->is_only_premium() ) {
5717 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5718 }
5719
5720 $license_notice = $this->get_license_network_activation_notice();
5721 if ( ! empty( $license_notice ) ) {
5722 $notice .= ' ' . $license_notice;
5723 }
5724
5725 if ( ! empty( $notice ) ) {
5726 $this->_admin_notices->add_sticky(
5727 trim( $notice ),
5728 'premium_activated',
5729 $this->get_text_x_inline( 'W00t',
5730 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5731 );
5732 }
5733 } else {
5734 // Remove sticky message related to premium code activation.
5735 $this->_admin_notices->remove_sticky( 'premium_activated' );
5736
5737 // Activated free code (after had the premium before).
5738 $this->do_action( 'after_free_version_reactivation' );
5739
5740 if ( $this->is_paying() && ! $this->is_premium() ) {
5741 $this->add_complete_upgrade_instructions_notice(
5742 sprintf(
5743 /* translators: %s: License type (e.g. you have a professional license) */
5744 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5745 $this->get_plan_title()
5746 ),
5747 'plan_upgraded'
5748 );
5749 }
5750 }
5751
5752 if ( $this->is_registered() ) {
5753 // Schedule code type changes event.
5754 $this->schedule_install_sync();
5755 }
5756
5757 /**
5758 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5759 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5760 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5761 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5762 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5763 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5764 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5765 */
5766 $this->unregister_uninstall_hook();
5767
5768 $this->clear_module_main_file_cache();
5769
5770 // Update is_premium of latest version.
5771 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5772 }
5773
5774 #endregion
5775
5776 #----------------------------------------------------------------------------------
5777 #region Add-ons
5778 #----------------------------------------------------------------------------------
5779
5780 /**
5781 * Check if add-on installed and activated on site.
5782 *
5783 * @author Vova Feldman (@svovaf)
5784 * @since 1.0.6
5785 *
5786 * @param string|number $id_or_slug
5787 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5788 *
5789 * @return bool
5790 */
5791 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5792 $this->_logger->entrance();
5793
5794 $addon_id = self::get_module_id( $id_or_slug );
5795 $is_activated = self::has_instance( $addon_id );
5796
5797 if ( ! $is_activated ) {
5798 return false;
5799 }
5800
5801 if ( is_bool( $is_premium ) ) {
5802 // Check if the specified code version is activate.
5803 $addon = $this->get_addon_instance( $addon_id );
5804 $is_activated = ( $is_premium === $addon->is_premium() );
5805 }
5806
5807 return $is_activated;
5808 }
5809
5810 /**
5811 * Check if add-on was connected to install
5812 *
5813 * @author Vova Feldman (@svovaf)
5814 * @since 1.1.7
5815 *
5816 * @param string|number $id_or_slug
5817 *
5818 * @return bool
5819 */
5820 function is_addon_connected( $id_or_slug ) {
5821 $this->_logger->entrance();
5822
5823 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5824
5825 $addon_id = self::get_module_id( $id_or_slug );
5826 $addon = $this->get_addon( $addon_id );
5827 $slug = $addon->slug;
5828 if ( ! isset( $sites[ $slug ] ) ) {
5829 return false;
5830 }
5831
5832 $site = $sites[ $slug ];
5833
5834 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5835
5836 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5837 // The given slug do NOT belong to any of the plugin's add-ons.
5838 return false;
5839 }
5840
5841 return ( is_object( $site ) &&
5842 is_numeric( $site->id ) &&
5843 is_numeric( $site->user_id ) &&
5844 FS_Plugin_Plan::is_valid_id( $site->plan_id )
5845 );
5846 }
5847
5848 /**
5849 * Determines if add-on installed.
5850 *
5851 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
5852 *
5853 * @author Vova Feldman (@svovaf)
5854 * @since 1.0.6
5855 *
5856 * @param string|number $id_or_slug
5857 *
5858 * @return bool
5859 */
5860 function is_addon_installed( $id_or_slug ) {
5861 $this->_logger->entrance();
5862
5863 $addon_id = self::get_module_id( $id_or_slug );
5864
5865 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
5866 }
5867
5868 /**
5869 * Get add-on basename.
5870 *
5871 * @author Vova Feldman (@svovaf)
5872 * @since 1.0.6
5873 *
5874 * @param string|number $id_or_slug
5875 *
5876 * @return string
5877 */
5878 function get_addon_basename( $id_or_slug ) {
5879 $addon_id = self::get_module_id( $id_or_slug );
5880
5881 if ( $this->is_addon_activated( $addon_id ) ) {
5882 return self::instance( $addon_id )->get_plugin_basename();
5883 }
5884
5885 $addon = $this->get_addon( $addon_id );
5886 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
5887
5888 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
5889 return $premium_basename;
5890 }
5891
5892 $all_plugins = $this->get_all_plugins();
5893
5894 foreach ( $all_plugins as $basename => $data ) {
5895 if ( $addon->slug === $data['slug'] ||
5896 $addon->premium_slug === $data['slug']
5897 ) {
5898 return $basename;
5899 }
5900 }
5901
5902 $free_basename = "{$addon->slug}/{$addon->slug}.php";
5903
5904 return $free_basename;
5905 }
5906
5907 /**
5908 * Get installed add-ons instances.
5909 *
5910 * @author Vova Feldman (@svovaf)
5911 * @since 1.0.6
5912 *
5913 * @return Freemius[]
5914 */
5915 function get_installed_addons() {
5916 if ( $this->is_addon() ) {
5917 // Add-on cannot have add-ons.
5918 return array();
5919 }
5920
5921 $installed_addons = array();
5922
5923 foreach ( self::$_instances as $instance ) {
5924 if ( $instance->is_addon_of( $this->_plugin->id ) ) {
5925 $installed_addons[] = $instance;
5926 }
5927 }
5928
5929 return $installed_addons;
5930 }
5931
5932 /**
5933 * Check if any add-ons of the plugin are installed.
5934 *
5935 * @author Leo Fajardo (@leorw)
5936 * @since 1.1.1
5937 *
5938 * @return bool
5939 */
5940 function has_installed_addons() {
5941 if ( ! $this->has_addons() ) {
5942 return false;
5943 }
5944
5945 foreach ( self::$_instances as $instance ) {
5946 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
5947 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
5948 return true;
5949 }
5950 }
5951 }
5952
5953 return false;
5954 }
5955
5956 /**
5957 * Tell Freemius that the current plugin is an add-on.
5958 *
5959 * @author Vova Feldman (@svovaf)
5960 * @since 1.0.6
5961 *
5962 * @param number $parent_plugin_id The parent plugin ID
5963 */
5964 function init_addon( $parent_plugin_id ) {
5965 $this->_plugin->parent_plugin_id = $parent_plugin_id;
5966 }
5967
5968 /**
5969 * @author Vova Feldman (@svovaf)
5970 * @since 1.0.6
5971 *
5972 * @return bool
5973 */
5974 function is_addon() {
5975 return (
5976 isset( $this->_plugin->parent_plugin_id ) &&
5977 is_numeric( $this->_plugin->parent_plugin_id )
5978 );
5979 }
5980
5981 /**
5982 * @author Vova Feldman (@svovaf)
5983 * @since 2.3.2
5984 *
5985 * @param number $parent_product_id
5986 *
5987 * @return bool
5988 */
5989 function is_addon_of( $parent_product_id ) {
5990 return (
5991 $this->is_addon() &&
5992 $parent_product_id == $this->_plugin->parent_plugin_id
5993 );
5994 }
5995
5996 /**
5997 * Deactivate add-on if it's premium only and the user does't have a valid license.
5998 *
5999 * @param bool $is_after_trial_cancel
6000 *
6001 * @return bool If add-on was deactivated.
6002 */
6003 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
6004 if ( ! $this->has_free_plan() &&
6005 ! $this->has_features_enabled_license() &&
6006 ! $this->_has_premium_license()
6007 ) {
6008 if ( $this->is_registered() ) {
6009 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
6010 // if (empty($this->_storage->activation_timestamp) ||
6011 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
6012 // ) {
6013 /**
6014 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
6015 *
6016 * Retry syncing the user add-on licenses.
6017 */
6018 // Sync licenses.
6019 $this->_sync_licenses();
6020 // }
6021
6022 // Try to activate premium license.
6023 $this->_activate_license( true );
6024 }
6025
6026 if ( ! $this->has_free_plan() &&
6027 ! $this->has_features_enabled_license() &&
6028 ! $this->_has_premium_license()
6029 ) {
6030 // @todo Check if deactivate plugins also call the deactivation hook.
6031
6032 $this->_parent->_admin_notices->add_sticky(
6033 sprintf(
6034 ( $is_after_trial_cancel ?
6035 $this->_parent->get_text_inline(
6036 '%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.',
6037 'addon-trial-cancelled-message'
6038 ) :
6039 $this->_parent->get_text_inline(
6040 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
6041 'addon-no-license-message'
6042 )
6043 ),
6044 '<b>' . $this->_plugin->title . '</b>'
6045 ) . ' ' . sprintf(
6046 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
6047 $this->_parent->addon_url( $this->_slug ),
6048 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
6049 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
6050 ),
6051 'no_addon_license_' . $this->_slug,
6052 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
6053 ( $is_after_trial_cancel ? 'success' : 'error' )
6054 );
6055
6056 deactivate_plugins( array( $this->_plugin_basename ), true );
6057
6058 return true;
6059 }
6060 }
6061
6062 return false;
6063 }
6064
6065 #endregion
6066
6067 #----------------------------------------------------------------------------------
6068 #region Sandbox
6069 #----------------------------------------------------------------------------------
6070
6071 /**
6072 * Set Freemius into sandbox mode for debugging.
6073 *
6074 * @author Vova Feldman (@svovaf)
6075 * @since 1.0.4
6076 *
6077 * @param string $secret_key
6078 */
6079 function init_sandbox( $secret_key ) {
6080 $this->_plugin->secret_key = $secret_key;
6081
6082 // Update plugin details.
6083 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
6084 }
6085
6086 /**
6087 * Check if running payments in sandbox mode.
6088 *
6089 * @author Vova Feldman (@svovaf)
6090 * @since 1.0.4
6091 *
6092 * @return bool
6093 */
6094 function is_payments_sandbox() {
6095 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
6096 }
6097
6098 #endregion
6099
6100 /**
6101 * Check if running test vs. live plugin.
6102 *
6103 * @author Vova Feldman (@svovaf)
6104 * @since 1.0.5
6105 *
6106 * @return bool
6107 */
6108 function is_live() {
6109 return $this->_plugin->is_live;
6110 }
6111
6112 /**
6113 * Check if super-admin skipped connection for all sites in the network.
6114 *
6115 * @author Vova Feldman (@svovaf)
6116 * @since 2.0.0
6117 */
6118 function is_network_anonymous() {
6119 if ( ! $this->_is_network_active ) {
6120 return false;
6121 }
6122
6123 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
6124
6125 if ( empty( $is_anonymous_ms ) ) {
6126 return false;
6127 }
6128
6129 return $is_anonymous_ms['is'];
6130 }
6131
6132 /**
6133 * Check if super-admin opted-in for all sites in the network.
6134 *
6135 * @author Vova Feldman (@svovaf)
6136 * @since 2.0.0
6137 */
6138 function is_network_connected() {
6139 if ( ! $this->_is_network_active ) {
6140 return false;
6141 }
6142
6143 return $this->_storage->get( 'is_network_connected' );
6144 }
6145
6146 /**
6147 * Check if the user skipped connecting the account with Freemius.
6148 *
6149 * @author Vova Feldman (@svovaf)
6150 * @since 1.0.7
6151 *
6152 * @return bool
6153 */
6154 function is_anonymous() {
6155 if ( ! isset( $this->_is_anonymous ) ) {
6156 if ( $this->is_network_anonymous() ) {
6157 $this->_is_anonymous = true;
6158 } else if ( fs_is_network_admin() ) {
6159 /**
6160 * When not-network-anonymous, yet, running in the network admin, consider as anonymous only when ALL non-delegated sites are set to anonymous.
6161 */
6162 $non_delegated_sites = $this->get_non_delegated_blog_ids();
6163
6164 foreach ( $non_delegated_sites as $blog_id ) {
6165 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6166
6167 if ( empty( $is_anonymous ) || false === $is_anonymous[ 'is' ] ) {
6168 $this->_is_anonymous = false;
6169 break;
6170 }
6171 }
6172
6173 if ( false !== $this->_is_anonymous ) {
6174 $this->_is_anonymous = true;
6175 }
6176 } else {
6177 if ( ! isset( $this->_storage->is_anonymous ) ) {
6178 // Not skipped.
6179 $this->_is_anonymous = false;
6180 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
6181 // For back compatibility, since the variable was boolean before.
6182 $this->_is_anonymous = $this->_storage->is_anonymous;
6183
6184 // Upgrade stored data format to 1.1.3 format.
6185 $this->set_anonymous_mode( $this->_storage->is_anonymous );
6186 } else {
6187 // Version 1.1.3 and later.
6188 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
6189 }
6190 }
6191 }
6192
6193 return $this->_is_anonymous;
6194 }
6195
6196 /**
6197 * Check if the user skipped the connection of a specified site.
6198 *
6199 * @author Vova Feldman (@svovaf)
6200 * @since 2.0.0
6201 *
6202 * @param int $blog_id
6203 *
6204 * @return bool
6205 */
6206 function is_anonymous_site( $blog_id = 0 ) {
6207 if ( $this->is_network_anonymous() ) {
6208 return true;
6209 }
6210
6211 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6212
6213 if ( empty( $is_anonymous ) ) {
6214 return false;
6215 }
6216
6217 return $is_anonymous['is'];
6218 }
6219
6220 /**
6221 * Check if user connected his account and install pending email activation.
6222 *
6223 * @author Vova Feldman (@svovaf)
6224 * @since 1.0.7
6225 *
6226 * @return bool
6227 */
6228 function is_pending_activation() {
6229 return $this->_storage->get( 'is_pending_activation', false );
6230 }
6231
6232 /**
6233 * @author Leo Fajardo (@leorw)
6234 * @since 2.5.0
6235 */
6236 private function clear_pending_activation_mode() {
6237 // Remove the pending activation sticky notice (if it still exists).
6238 $this->_admin_notices->remove_sticky( 'activation_pending' );
6239
6240 // Clear the plugin's pending activation mode.
6241 unset( $this->_storage->is_pending_activation );
6242 }
6243
6244 /**
6245 * Check if plugin must be WordPress.org compliant.
6246 *
6247 * @since 1.0.7
6248 *
6249 * @return bool
6250 */
6251 function is_org_repo_compliant() {
6252 return $this->_is_org_compliant;
6253 }
6254
6255 #--------------------------------------------------------------------------------
6256 #region WP Cron Common
6257 #--------------------------------------------------------------------------------
6258
6259 /**
6260 * @author Vova Feldman (@svovaf)
6261 * @since 2.0.0
6262 *
6263 * @param string $name Cron name.
6264 *
6265 * @return object
6266 */
6267 private function get_cron_data( $name ) {
6268 $this->_logger->entrance( $name );
6269
6270 /**
6271 * @var object $cron_data
6272 */
6273 return $this->_storage->get( "{$name}_cron", null );
6274 }
6275
6276 /**
6277 * @author Vova Feldman (@svovaf)
6278 * @since 2.0.0
6279 *
6280 * @param string $name Cron name.
6281 */
6282 private function clear_cron_data( $name ) {
6283 $this->_logger->entrance( $name );
6284
6285 $this->_storage->remove( "{$name}_cron" );
6286 }
6287
6288 /**
6289 * @author Vova Feldman (@svovaf)
6290 * @since 2.0.0
6291 *
6292 * @param string $name Cron name.
6293 * @param int $cron_blog_id The cron executing blog ID.
6294 */
6295 private function set_cron_data( $name, $cron_blog_id = 0 ) {
6296 $this->_logger->entrance( $name );
6297
6298 $this->_storage->store( "{$name}_cron", (object) array(
6299 'version' => $this->get_plugin_version(),
6300 'blog_id' => $cron_blog_id,
6301 'sdk_version' => $this->version,
6302 'timestamp' => WP_FS__SCRIPT_START_TIME,
6303 'on' => true,
6304 ) );
6305 }
6306
6307 /**
6308 * Get the cron's executing blog ID.
6309 *
6310 * @author Vova Feldman (@svovaf)
6311 * @since 2.0.0
6312 *
6313 * @param string $name Cron name.
6314 *
6315 * @return int
6316 */
6317 private function get_cron_blog_id( $name ) {
6318 $this->_logger->entrance( $name );
6319
6320 if ( ! is_multisite() ) {
6321 // Not a multisite.
6322 return 0;
6323 }
6324
6325 $cron_data = $this->get_cron_data( $name );
6326
6327 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6328 $cron_data->blog_id :
6329 0;
6330 }
6331
6332 /**
6333 * @author Vova Feldman (@svovaf)
6334 * @since 2.0.0
6335 *
6336 * @param string $name Cron name.
6337 *
6338 * @return bool
6339 */
6340 private function is_cron_on( $name ) {
6341 $this->_logger->entrance( $name );
6342
6343 /**
6344 * @var object $cron_data
6345 */
6346 $cron_data = $this->get_cron_data( $name );
6347
6348 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6349 }
6350
6351 /**
6352 * Unix timestamp for previous cron execution or false if never executed.
6353 *
6354 * @author Vova Feldman (@svovaf)
6355 * @since 2.0.0
6356 *
6357 * @param string $name Cron name.
6358 *
6359 * @return int|false
6360 */
6361 private function cron_last_execution( $name ) {
6362 $this->_logger->entrance( $name );
6363
6364 return $this->_storage->get( "{$name}_timestamp" );
6365 }
6366
6367 /**
6368 * Set cron execution time to now.
6369 *
6370 * @author Vova Feldman (@svovaf)
6371 * @since 2.0.0
6372 *
6373 * @param string $name Cron name.
6374 */
6375 private function set_cron_execution_timestamp( $name ) {
6376 $this->_logger->entrance( $name );
6377
6378 $this->_storage->store( "{$name}_timestamp", time() );
6379 }
6380
6381 /**
6382 * Sets the keepalive time to now.
6383 *
6384 * @author Leo Fajardo (@leorw)
6385 * @since 2.2.3
6386 *
6387 * @param bool|null $use_network_level_storage
6388 */
6389 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6390 $this->_logger->entrance();
6391
6392 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6393 }
6394
6395 /**
6396 * Check if cron was executed in the last $period of seconds.
6397 *
6398 * @author Vova Feldman (@svovaf)
6399 * @since 2.0.0
6400 *
6401 * @param string $name Cron name.
6402 * @param int $period In seconds
6403 *
6404 * @return bool
6405 */
6406 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6407 $this->_logger->entrance( $name );
6408
6409 $last_execution = $this->cron_last_execution( $name );
6410
6411 if ( ! is_numeric( $last_execution ) ) {
6412 return false;
6413 }
6414
6415 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6416 }
6417
6418 /**
6419 * WP Cron is executed on a site level. When running in a multisite network environment
6420 * with the network integration activated, for optimization reasons, we are consolidating
6421 * the installs data sync cron to be executed only from a single site.
6422 *
6423 * @author Vova Feldman (@svovaf)
6424 * @since 2.0.0
6425 *
6426 * @param int $except_blog_id Target any except the excluded blog ID.
6427 *
6428 * @return int
6429 */
6430 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6431 if ( ! is_multisite() ) {
6432 return 0;
6433 }
6434
6435 if ( $this->_is_network_active ) {
6436 $network_install_blog_id = $this->_storage->network_install_blog_id;
6437
6438 if (
6439 is_numeric( $network_install_blog_id ) &&
6440 $except_blog_id != $network_install_blog_id &&
6441 self::is_site_active( $network_install_blog_id )
6442 ) {
6443 // Try to run cron from the main network blog.
6444 $install = $this->get_install_by_blog_id( $network_install_blog_id );
6445
6446 if (
6447 is_object( $install ) &&
6448 $this->is_tracking_allowed( $network_install_blog_id, $install )
6449 ) {
6450 return $network_install_blog_id;
6451 }
6452 }
6453 }
6454
6455 // Get first opted-in blog ID with active tracking.
6456 $installs = $this->get_blog_install_map();
6457 foreach ( $installs as $blog_id => $install ) {
6458 if ( $except_blog_id != $blog_id &&
6459 self::is_site_active( $blog_id ) &&
6460 $this->is_tracking_allowed( $blog_id, $install )
6461 ) {
6462 return $blog_id;
6463 }
6464 }
6465
6466 return 0;
6467 }
6468
6469 /**
6470 * @author Vova Feldman (@svovaf)
6471 * @since 2.0.0
6472 *
6473 * @param string $name Cron name.
6474 * @param string $action_tag Callback action tag.
6475 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6476 */
6477 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6478 $this->_logger->entrance( $name );
6479
6480 if ( ! $this->is_cron_on( $name ) ) {
6481 return;
6482 }
6483
6484 $clear_cron = true;
6485 if ( ! $is_network_clear && $this->_is_network_active ) {
6486 $installs = $this->get_blog_install_map();
6487
6488 foreach ( $installs as $blog_id => $install ) {
6489 /**
6490 * @var FS_Site $install
6491 */
6492 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6493 $clear_cron = false;
6494 break;
6495 }
6496 }
6497 }
6498
6499 if ( ! $clear_cron ) {
6500 return;
6501 }
6502
6503 $cron_blog_id = $this->get_cron_blog_id( $name );
6504
6505 $this->clear_cron_data( $name );
6506
6507 if ( 0 < $cron_blog_id ) {
6508 switch_to_blog( $cron_blog_id );
6509 }
6510
6511 if ( empty( $action_tag ) ) {
6512 $action_tag = $name;
6513 }
6514
6515 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6516
6517 if ( 0 < $cron_blog_id ) {
6518 restore_current_blog();
6519 }
6520 }
6521
6522 /**
6523 * Unix timestamp for next cron execution or false if not scheduled.
6524 *
6525 * @author Vova Feldman (@svovaf)
6526 * @since 2.0.0
6527 *
6528 * @param string $name Cron name.
6529 * @param string $action_tag Callback action tag.
6530 *
6531 * @return int|false
6532 */
6533 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6534 $this->_logger->entrance( $name );
6535
6536 if ( ! $this->is_cron_on( $name ) ) {
6537 return false;
6538 }
6539
6540 $cron_blog_id = $this->get_cron_blog_id( $name );
6541
6542 if ( 0 < $cron_blog_id ) {
6543 switch_to_blog( $cron_blog_id );
6544 }
6545
6546 if ( empty( $action_tag ) ) {
6547 $action_tag = $name;
6548 }
6549
6550 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6551
6552 if ( 0 < $cron_blog_id ) {
6553 restore_current_blog();
6554 }
6555
6556 return $next_scheduled;
6557 }
6558
6559 /**
6560 * @author Vova Feldman (@svovaf)
6561 * @since 2.0.0
6562 *
6563 * @param string $name Cron name.
6564 * @param string $action_tag Callback action tag.
6565 * @param string $recurrence 'single' or 'daily'.
6566 * @param int $start_at Defaults to now.
6567 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6568 * @param int $except_blog_id Target any except the excluded blog ID.
6569 */
6570 private function schedule_cron(
6571 $name,
6572 $action_tag = '',
6573 $recurrence = 'single',
6574 $start_at = WP_FS__SCRIPT_START_TIME,
6575 $randomize_start = true,
6576 $except_blog_id = 0
6577 ) {
6578 $this->_logger->entrance( $name );
6579
6580 $this->clear_cron( $name, $action_tag, true );
6581
6582 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6583
6584 if ( is_multisite() && 0 == $cron_blog_id ) {
6585 // Don't schedule cron since couldn't find a target blog.
6586 return;
6587 }
6588
6589 if ( 0 < $cron_blog_id ) {
6590 switch_to_blog( $cron_blog_id );
6591 }
6592
6593 if ( 'daily' === $recurrence ) {
6594 if ( $randomize_start ) {
6595 // Schedule first sync with a random 12 hour time range from now.
6596 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6597 }
6598
6599 // Schedule daily WP cron.
6600 wp_schedule_event(
6601 $start_at,
6602 'daily',
6603 $this->get_action_tag( $action_tag )
6604 );
6605 } else if ( 'single' === $recurrence ) {
6606 // Schedule single cron.
6607 wp_schedule_single_event(
6608 $start_at,
6609 $this->get_action_tag( $action_tag )
6610 );
6611 }
6612
6613 $this->set_cron_data( $name, $cron_blog_id );
6614
6615 if ( 0 < $cron_blog_id ) {
6616 restore_current_blog();
6617 }
6618 }
6619
6620 /**
6621 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6622 * that doesn't halt page loading.
6623 *
6624 * @author Vova Feldman (@svovaf)
6625 * @since 2.0.0
6626 *
6627 * @param string $name Cron name.
6628 * @param callable $callable The function that should be executed.
6629 */
6630 private function execute_cron( $name, $callable ) {
6631 $this->_logger->entrance( $name );
6632
6633 // Store the last time data sync was executed.
6634 $this->set_cron_execution_timestamp( $name );
6635
6636 // Check if API is temporary down.
6637 if ( FS_Api::is_temporary_down() ) {
6638 return;
6639 }
6640
6641 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6642
6643 $users_2_blog_ids = array();
6644
6645 if ( ! is_multisite() ) {
6646 // Add dummy blog.
6647 $users_2_blog_ids[0] = array( 0 );
6648 } else {
6649 $installs = $this->get_blog_install_map();
6650 foreach ( $installs as $blog_id => $install ) {
6651 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6652 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6653 $users_2_blog_ids[ $install->user_id ] = array();
6654 }
6655
6656 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6657 }
6658 }
6659 }
6660
6661 $current_blog_id = get_current_blog_id();
6662
6663 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6664 if ( 0 < $blog_ids[0] ) {
6665 $this->switch_to_blog( $blog_ids[0] );
6666 }
6667
6668 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6669
6670 foreach ( $blog_ids as $blog_id ) {
6671 $this->do_action( "after_{$name}_cron", $blog_id );
6672 }
6673 }
6674
6675 if ( is_multisite() ) {
6676 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6677
6678 $this->do_action( "after_{$name}_cron_multisite" );
6679 }
6680 }
6681
6682 #endregion
6683
6684 #----------------------------------------------------------------------------------
6685 #region Daily Sync Cron
6686 #----------------------------------------------------------------------------------
6687
6688
6689 /**
6690 * @author Vova Feldman (@svovaf)
6691 * @since 2.0.0
6692 *
6693 * @return bool
6694 */
6695 private function is_sync_cron_scheduled() {
6696 return $this->is_cron_on( 'sync' );
6697 }
6698
6699 /**
6700 * Get the sync cron's executing blog ID.
6701 *
6702 * @author Vova Feldman (@svovaf)
6703 * @since 2.0.0
6704 *
6705 * @return int
6706 */
6707 private function get_sync_cron_blog_id() {
6708 return $this->get_cron_blog_id( 'sync' );
6709 }
6710
6711 /**
6712 * @author Vova Feldman (@svovaf)
6713 * @since 1.1.7.3
6714 */
6715 private function run_manual_sync() {
6716 if ( ! $this->is_user_admin() ) {
6717 return;
6718 }
6719
6720 // Run manual sync.
6721 $this->_sync_cron();
6722
6723 // Reschedule next cron to run 24 hours from now (performance optimization).
6724 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6725 }
6726
6727 /**
6728 * Data sync cron job. Replaces the background sync non blocking HTTP request
6729 * that doesn't halt page loading.
6730 *
6731 * @author Vova Feldman (@svovaf)
6732 * @since 1.1.7.3
6733 * @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.
6734 */
6735 function _sync_cron() {
6736 $this->_logger->entrance();
6737
6738 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6739 }
6740
6741 /**
6742 * The actual data sync cron logic.
6743 *
6744 * @author Vova Feldman (@svovaf)
6745 * @since 2.0.0
6746 *
6747 * @param int[] $blog_ids
6748 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6749 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6750 * updates for a single site in case `execute_cron` has switched to a different blog.
6751 */
6752 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6753 if ( $this->is_registered() ) {
6754 if ( $this->has_paid_plan() ) {
6755 // Initiate background plan sync.
6756 $this->_sync_license( true, false, $current_blog_id );
6757
6758 if ( $this->is_paying() ) {
6759 // Check for premium plugin updates.
6760 $this->check_updates( true );
6761 }
6762 } else {
6763 // Sync install(s) (only if something changed locally).
6764 if ( 1 < count( $blog_ids ) ) {
6765 $this->sync_installs();
6766 } else {
6767 $this->sync_install();
6768 }
6769
6770 $this->maybe_sync_install_user();
6771 }
6772 }
6773 }
6774
6775 /**
6776 * Check if sync was executed in the last $period of seconds.
6777 *
6778 * @author Vova Feldman (@svovaf)
6779 * @since 1.1.7.3
6780 *
6781 * @param int $period In seconds
6782 *
6783 * @return bool
6784 */
6785 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6786 return $this->is_cron_executed( 'sync', $period );
6787 }
6788
6789 /**
6790 * @author Vova Feldman (@svovaf)
6791 * @since 1.1.7.3
6792 *
6793 * @return bool
6794 */
6795 private function is_sync_cron_on() {
6796 return $this->is_cron_on( 'sync' );
6797 }
6798
6799 /**
6800 * @author Leo Fajardo (@leorw)
6801 * @since 2.5.0
6802 */
6803 private function maybe_schedule_sync_cron() {
6804 $next_schedule = $this->next_sync_cron();
6805
6806 // The event is properly scheduled, so no need to reschedule it.
6807 if (
6808 is_numeric( $next_schedule ) &&
6809 $next_schedule > time()
6810 ) {
6811 return;
6812 }
6813
6814 $this->schedule_sync_cron();
6815 }
6816
6817 /**
6818 * @author Vova Feldman (@svovaf)
6819 * @since 1.1.7.3
6820 *
6821 * @param int $start_at Defaults to now.
6822 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6823 * @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.
6824 */
6825 private function schedule_sync_cron(
6826 $start_at = WP_FS__SCRIPT_START_TIME,
6827 $randomize_start = true,
6828 $except_blog_id = 0
6829 ) {
6830 $this->schedule_cron(
6831 'sync',
6832 'data_sync',
6833 'daily',
6834 $start_at,
6835 $randomize_start,
6836 $except_blog_id
6837 );
6838 }
6839
6840 /**
6841 * Add the actual sync function to the cron job hook.
6842 *
6843 * @author Vova Feldman (@svovaf)
6844 * @since 1.1.7.3
6845 */
6846 private function hook_callback_to_sync_cron() {
6847 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6848 }
6849
6850 /**
6851 * @author Vova Feldman (@svovaf)
6852 * @since 1.1.7.3
6853 *
6854 * @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.
6855 */
6856 private function clear_sync_cron( $is_network_clear = false ) {
6857 $this->_logger->entrance();
6858
6859 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6860 }
6861
6862 /**
6863 * Unix timestamp for next sync cron execution or false if not scheduled.
6864 *
6865 * @author Vova Feldman (@svovaf)
6866 * @since 1.1.7.3
6867 *
6868 * @return int|false
6869 */
6870 function next_sync_cron() {
6871 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6872 }
6873
6874 /**
6875 * Unix timestamp for previous sync cron execution or false if never executed.
6876 *
6877 * @author Vova Feldman (@svovaf)
6878 * @since 1.1.7.3
6879 *
6880 * @return int|false
6881 */
6882 function last_sync_cron() {
6883 return $this->cron_last_execution( 'sync' );
6884 }
6885
6886 #endregion Daily Sync Cron ------------------------------------------------------------------
6887
6888 #----------------------------------------------------------------------------------
6889 #region Async Install Sync
6890 #----------------------------------------------------------------------------------
6891
6892 /**
6893 * @author Vova Feldman (@svovaf)
6894 * @since 1.1.7.3
6895 *
6896 * @return bool
6897 */
6898 private function is_install_sync_scheduled() {
6899 return $this->is_cron_on( 'install_sync' );
6900 }
6901
6902 /**
6903 * Get the sync cron's executing blog ID.
6904 *
6905 * @author Vova Feldman (@svovaf)
6906 * @since 2.0.0
6907 *
6908 * @return int
6909 */
6910 private function get_install_sync_cron_blog_id() {
6911 return $this->get_cron_blog_id( 'install_sync' );
6912 }
6913
6914 /**
6915 * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
6916 *
6917 * @author Vova Feldman (@svovaf)
6918 * @since 1.1.7.3
6919 *
6920 * @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.
6921 */
6922 private function schedule_install_sync( $except_blog_id = 0 ) {
6923 if ( $this->is_clone() ) {
6924 return;
6925 }
6926
6927 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
6928 }
6929
6930 /**
6931 * Unix timestamp for previous install sync cron execution or false if never executed.
6932 *
6933 * @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.
6934 *
6935 * @author Vova Feldman (@svovaf)
6936 * @since 1.1.7.3
6937 *
6938 * @return int|false
6939 */
6940 function last_install_sync() {
6941 return $this->cron_last_execution( 'install_sync' );
6942 }
6943
6944 /**
6945 * Unix timestamp for next install sync cron execution or false if not scheduled.
6946 *
6947 * @author Vova Feldman (@svovaf)
6948 * @since 1.1.7.3
6949 *
6950 * @return int|false
6951 */
6952 function next_install_sync() {
6953 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
6954 }
6955
6956 /**
6957 * Add the actual install sync function to the cron job hook.
6958 *
6959 * @author Vova Feldman (@svovaf)
6960 * @since 1.1.7.3
6961 */
6962 private function hook_callback_to_install_sync() {
6963 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
6964 }
6965
6966 /**
6967 * @author Vova Feldman (@svovaf)
6968 * @since 1.1.7.3
6969 *
6970 * @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.
6971 */
6972 private function clear_install_sync_cron( $is_network_clear = false ) {
6973 $this->_logger->entrance();
6974
6975 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
6976 }
6977
6978 /**
6979 * @author Vova Feldman (@svovaf)
6980 * @since 1.1.7.3
6981 * @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.
6982 */
6983 public function _run_sync_install() {
6984 $this->_logger->entrance();
6985
6986 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
6987 }
6988
6989 /**
6990 * The actual install(s) sync cron logic.
6991 *
6992 * @author Vova Feldman (@svovaf)
6993 * @since 2.0.0
6994 *
6995 * @param int[] $blog_ids
6996 * @param int|null $current_blog_id
6997 */
6998 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
6999 if ( $this->is_registered() ) {
7000 if ( 1 < count( $blog_ids ) ) {
7001 $this->sync_installs( array(), true );
7002 } else {
7003 $this->sync_install( array(), true );
7004 }
7005
7006 $this->maybe_sync_install_user();
7007 }
7008 }
7009
7010 #endregion Async Install Sync ------------------------------------------------------------------
7011
7012 /**
7013 * Show a notice that activation is currently pending.
7014 *
7015 * @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.
7016 *
7017 * @author Vova Feldman (@svovaf)
7018 * @since 1.0.7
7019 *
7020 * @param bool|string $email_address
7021 * @param bool $is_pending_trial Since 1.2.1.5
7022 * @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.
7023 * @param bool $has_upgrade_context Since 2.5.3
7024 * @param bool $support_email_address Since 2.5.3
7025 */
7026 function _add_pending_activation_notice(
7027 $email_address = false,
7028 $is_pending_trial = false,
7029 $is_suspicious_email = false,
7030 $has_upgrade_context = false,
7031 $support_email_address = false
7032 ) {
7033 if ( ! is_string( $email_address ) ) {
7034 $current_user = self::_get_current_wp_user();
7035 $email_address = $current_user->user_email;
7036 }
7037
7038 $formatted_message_args = array(
7039 "<b>{$this->get_plugin_name()}</b>",
7040 "<b>{$email_address}</b>",
7041 );
7042
7043 if ( ! $has_upgrade_context || ! fs_is_network_admin() ) {
7044 /* translators: %3$s: action (e.g.: "start the trial" or "complete the opt-in") */
7045 $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' );
7046
7047 $formatted_message_args[] = $is_pending_trial ?
7048 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
7049 $this->get_text_inline( 'complete the opt-in', 'complete-the-opt-in' );
7050
7051 $notice_title = $this->get_text_inline( 'Thanks!', 'thanks' );
7052 } else {
7053 /* translators: %3$s: What the user is expected to receive via email (e.g.: "the installation instructions" or "a license key") */
7054 $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.' );
7055
7056 if ( $this->has_release_on_freemius() ) {
7057 $formatted_message_args[] = $this->get_text_x_inline(
7058 'the installation instructions',
7059 'Part of the message telling the user what they should receive via email.',
7060 'the-installation-instructions-phrase'
7061 );
7062 } else {
7063 $formatted_message_args[] = $this->get_text_x_inline(
7064 'a license key',
7065 'Part of the message telling the user what they should receive via email.',
7066 'a-license-key-phrase'
7067 );
7068
7069 $formatted_message .= ( ' ' . sprintf(
7070 /* translators: %s: activation link (e.g.: <a>Click here</a>) */
7071 $this->get_text_inline( '%s to activate the license once you get it.', 'license-activation-link-message' ),
7072 sprintf(
7073 '<b><a href="%s">%s</a></b>',
7074 $this->get_activation_url( array(
7075 'fs_action' => 'reset_pending_activation_mode',
7076 'require_license' => 'true',
7077 'fs_unique_affix' => $this->get_unique_affix(),
7078 ) ),
7079 $this->get_text_x_inline( 'Click here', 'Part of an activation link message.', 'click-here' )
7080 )
7081 ) );
7082 }
7083
7084 $formatted_message_args[] = ( ! empty( $support_email_address ) ) ?
7085 ( "<b>{$support_email_address}</b>" ) :
7086 $this->get_text_x_inline(
7087 "the product's support email address",
7088 'Part of the message that tells the user to check their spam folder for a specific email.',
7089 'product-support-email-address-phrase'
7090 );
7091
7092 $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' ) );
7093
7094 $notice_title = $this->get_text_inline( 'Thanks for upgrading.', 'after-upgrade-thank-you-message' );
7095 }
7096
7097 $this->_admin_notices->add_sticky(
7098 vsprintf( $formatted_message, $formatted_message_args ),
7099 'activation_pending',
7100 $notice_title
7101 );
7102 }
7103
7104 /**
7105 * Check if currently in plugin activation.
7106 *
7107 * @author Vova Feldman (@svovaf)
7108 * @since 1.1.4
7109 *
7110 * @return bool
7111 */
7112 function is_plugin_activation() {
7113 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7114
7115 return !empty($result);
7116 }
7117
7118 /**
7119 *
7120 * NOTE: admin_menu action executed before admin_init.
7121 *
7122 * @author Vova Feldman (@svovaf)
7123 * @since 1.0.7
7124 */
7125 function _admin_init_action() {
7126 $is_migration = $this->is_migration();
7127
7128 /**
7129 * Automatically redirect to connect/activation page after plugin activation.
7130 *
7131 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
7132 */
7133 if ( $this->is_plugin_activation() ) {
7134 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7135
7136 if ( isset( $_GET['activate-multi'] ) ) {
7137 /**
7138 * Don't redirect if activating multiple plugins at once (bulk activation).
7139 */
7140 } else if (
7141 self::is_deactivation_snoozed() &&
7142 (
7143 // Either running the free code base.
7144 ! $this->is_premium() ||
7145 // Or if has a free version.
7146 ! $this->is_only_premium() ||
7147 // If premium only, don't redirect if license is activated.
7148 ( $this->is_registered() && ! $this->can_use_premium_code() )
7149 )
7150 ) {
7151 /**
7152 * 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.
7153 */
7154 } else if ( ! $is_migration ) {
7155 $this->_redirect_on_activation_hook();
7156 return;
7157 }
7158 }
7159
7160 if ( $is_migration ) {
7161 return;
7162 }
7163
7164 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
7165 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
7166
7167 $this->skip_connection( fs_is_network_admin() );
7168
7169 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
7170 }
7171
7172 if ( $this->is_network_activation_mode() &&
7173 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
7174 ) {
7175 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
7176
7177 $this->delegate_connection();
7178
7179 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
7180 }
7181
7182 $this->_add_upgrade_action_link();
7183
7184 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
7185 (
7186 ( true === $this->_storage->require_license_activation ) ||
7187 // Not registered nor anonymous.
7188 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
7189 // OR, network level and in network upgrade mode.
7190 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
7191 )
7192 ) {
7193 if ( ! $this->is_pending_activation() ) {
7194 if ( ! $this->is_activation_page() ) {
7195 /**
7196 * If a user visits any other admin page before activating the premium-only theme with a valid
7197 * license, reactivate the previous theme.
7198 *
7199 * @author Leo Fajardo (@leorw)
7200 * @since 1.2.2
7201 */
7202 if ( $this->is_theme() &&
7203 ! $this->has_settings_menu() &&
7204 ! isset( $_REQUEST['fs_action'] ) &&
7205 $this->can_activate_previous_theme()
7206 ) {
7207 if ( $this->is_only_premium() ) {
7208 $this->activate_previous_theme();
7209 return;
7210 }
7211
7212 if ( true === $this->_storage->require_license_activation ) {
7213 $this->_storage->require_license_activation = false;
7214 }
7215 }
7216
7217 if ( ! fs_is_network_admin() &&
7218 $this->is_network_activation_mode() &&
7219 ! $this->is_delegated_connection()
7220 ) {
7221 return;
7222 }
7223
7224 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
7225 if ( ! $this->_anonymous_mode &&
7226 ( ! $this->is_addon() || ! $this->_parent->is_anonymous() ) ) {
7227 // Show notice for new plugin installations.
7228 $this->_admin_notices->add(
7229 sprintf(
7230 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
7231 sprintf( '<b><a href="%s">%s</a></b>',
7232 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
7233 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
7234 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
7235 )
7236 ),
7237 '',
7238 'update-nag'
7239 );
7240 }
7241 } else {
7242 if ( $this->should_add_sticky_optin_notice() ) {
7243 $this->add_sticky_optin_admin_notice();
7244 }
7245
7246 if ( $this->has_filter( 'optin_pointer_element' ) ) {
7247 // Don't show admin nag if plugin update.
7248 wp_enqueue_script( 'wp-pointer' );
7249 wp_enqueue_style( 'wp-pointer' );
7250
7251 $this->_enqueue_connect_essentials();
7252
7253 add_action( 'admin_print_footer_scripts', array(
7254 $this,
7255 '_add_connect_pointer_script'
7256 ) );
7257 }
7258 }
7259 }
7260 }
7261
7262 if ( $this->show_opt_in_on_themes_page() &&
7263 $this->is_activation_page()
7264 ) {
7265 $this->_show_theme_activation_optin_dialog();
7266 }
7267 }
7268 }
7269
7270 /**
7271 * @author Vova Feldman (@svovaf)
7272 * @since 2.0.0
7273 *
7274 * @return bool
7275 */
7276 private function should_add_sticky_optin_notice() {
7277 if ( $this->is_addon() && $this->_parent->is_anonymous() ) {
7278 return false;
7279 }
7280
7281 if ( fs_is_network_admin() ) {
7282 if ( ! $this->_is_network_active ) {
7283 return false;
7284 }
7285
7286 if ( ! $this->is_network_activation_mode() ) {
7287 return false;
7288 }
7289
7290 return ! isset( $this->_storage->sticky_optin_added_ms );
7291 }
7292
7293 if ( ! $this->is_activation_mode() ) {
7294 return false;
7295 }
7296
7297 // If running from a blog admin and delegated the connection.
7298 return ! isset( $this->_storage->sticky_optin_added );
7299 }
7300
7301 /**
7302 * @author Leo Fajardo (@leorw)
7303 * @since 2.0.0
7304 */
7305 private function add_sticky_optin_admin_notice() {
7306 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7307 $this->_storage->sticky_optin_added = true;
7308 } else {
7309 $this->_storage->sticky_optin_added_ms = true;
7310 }
7311
7312 // Show notice for new plugin installations.
7313 $this->_admin_notices->add_sticky(
7314 sprintf(
7315 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7316 $this->_module_type,
7317 sprintf( '<b><a href="%s">%s</a></b>',
7318 $this->get_activation_url(),
7319 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7320 )
7321 ),
7322 'connect_account',
7323 '',
7324 'update-nag'
7325 );
7326 }
7327
7328 /**
7329 * Enqueue connect requires scripts and styles.
7330 *
7331 * @author Vova Feldman (@svovaf)
7332 * @since 1.1.4
7333 */
7334 function _enqueue_connect_essentials() {
7335 wp_enqueue_script( 'jquery' );
7336 wp_enqueue_script( 'json2' );
7337
7338 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.min.js' );
7339 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7340 }
7341
7342 /**
7343 * Add connect / opt-in pointer.
7344 *
7345 * @author Vova Feldman (@svovaf)
7346 * @since 1.1.4
7347 */
7348 function _add_connect_pointer_script() {
7349 $vars = array( 'id' => $this->_module_id );
7350 $pointer_content = fs_get_template( 'connect.php', $vars );
7351 ?>
7352 <script type="text/javascript">// <![CDATA[
7353 jQuery(document).ready(function ($) {
7354 if ('undefined' !== typeof(jQuery().pointer)) {
7355
7356 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7357
7358 if (element.length > 0) {
7359 var optin = $(element).pointer($.extend(true, {}, {
7360 content : <?php echo json_encode( $pointer_content ) ?>,
7361 position : {
7362 edge : 'left',
7363 align: 'center'
7364 },
7365 buttons : function () {
7366 // Don't show pointer buttons.
7367 return '';
7368 },
7369 pointerWidth: 482
7370 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7371
7372 <?php
7373 echo $this->apply_filters( 'optin_pointer_execute', "
7374
7375 optin.pointer('open');
7376
7377 // Tag the opt-in pointer with custom class.
7378 $('.wp-pointer #fs_connect')
7379 .parents('.wp-pointer.wp-pointer-top')
7380 .addClass('fs-opt-in-pointer');
7381
7382 ", 'element', 'optin' ) ?>
7383 }
7384 }
7385 });
7386 // ]]></script>
7387 <?php
7388 }
7389
7390 /**
7391 * Return current page's URL.
7392 *
7393 * @author Vova Feldman (@svovaf)
7394 * @since 1.0.7
7395 *
7396 * @return string
7397 */
7398 static function current_page_url() {
7399 $url = 'http';
7400
7401 if ( isset( $_SERVER["HTTPS"] ) ) {
7402 if ( $_SERVER["HTTPS"] == "on" ) {
7403 $url .= "s";
7404 }
7405 }
7406 $url .= "://";
7407 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7408 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7409 } else {
7410 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7411 }
7412
7413 return esc_url( $url );
7414 }
7415
7416 /**
7417 * Check if the current page is the plugin's main admin settings page.
7418 *
7419 * @author Vova Feldman (@svovaf)
7420 * @since 1.0.7
7421 *
7422 * @return bool
7423 */
7424 function _is_plugin_page() {
7425 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7426 fs_is_plugin_page( $this->_slug );
7427 }
7428
7429 /* Events
7430 ------------------------------------------------------------------------------------------------------------------*/
7431 /**
7432 * Delete site install from Database.
7433 *
7434 * @author Vova Feldman (@svovaf)
7435 * @since 1.0.1
7436 *
7437 * @param bool $store
7438 * @param int|null $blog_id Since 2.0.0
7439 *
7440 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7441 */
7442 function _delete_site( $store = true, $blog_id = null ) {
7443 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7444 }
7445
7446 /**
7447 * Delete site install from Database.
7448 *
7449 * @author Vova Feldman (@svovaf)
7450 * @since 1.2.2.7
7451 *
7452 * @param string $slug
7453 * @param string $module_type
7454 * @param bool $store
7455 * @param int|null $blog_id Since 2.0.0
7456 *
7457 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7458 */
7459 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7460 $sites = self::get_all_sites( $module_type, $blog_id );
7461
7462 $install_id = false;
7463
7464 if ( isset( $sites[ $slug ] ) ) {
7465 if ( is_object( $sites[ $slug ] ) ) {
7466 $install_id = $sites[ $slug ]->id;
7467 }
7468
7469 unset( $sites[ $slug ] );
7470
7471 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7472 }
7473
7474 return $install_id;
7475 }
7476
7477 /**
7478 * Delete user.
7479 *
7480 * @author Vova Feldman (@svovaf)
7481 * @since 2.0.0
7482 *
7483 * @param number $user_id
7484 * @param bool $store
7485 *
7486 * @return false|int The user ID if deleted. Otherwise, FALSE (when install not exist).
7487 */
7488 private static function delete_user( $user_id, $store = true ) {
7489 $users = self::get_all_users();
7490
7491 if ( ! is_array( $users ) || ! isset( $users[ $user_id ] ) ) {
7492 return false;
7493 }
7494
7495 unset( $users[ $user_id ] );
7496
7497 self::$_accounts->set_option( 'users', $users, $store );
7498
7499 return $user_id;
7500 }
7501
7502 /**
7503 * Delete plugin's plans information.
7504 *
7505 * @param bool $store Flush to Database if true.
7506 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7507 *
7508 * @author Vova Feldman (@svovaf)
7509 * @since 1.0.9
7510 */
7511 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7512 $this->_logger->entrance();
7513
7514 $plans = self::get_all_plans( $this->_module_type );
7515
7516 $plans_to_keep = array();
7517
7518 if ( $keep_associated_plans ) {
7519 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7520 foreach ( $plans_ids_to_keep as $plan_id ) {
7521 $plan = self::_get_plan_by_id( $plan_id );
7522 if ( is_object( $plan ) ) {
7523 $plans_to_keep[] = self::_encrypt_entity( $plan );
7524 }
7525 }
7526 }
7527
7528 if ( ! empty( $plans_to_keep ) ) {
7529 $plans[ $this->_slug ] = $plans_to_keep;
7530 } else {
7531 unset( $plans[ $this->_slug ] );
7532 }
7533
7534 $this->set_account_option( 'plans', $plans, $store );
7535 }
7536
7537 /**
7538 * Delete all plugin licenses.
7539 *
7540 * @author Vova Feldman (@svovaf)
7541 * @since 1.0.9
7542 *
7543 * @param bool $store
7544 */
7545 private function _delete_licenses( $store = true ) {
7546 $this->_logger->entrance();
7547
7548 $all_licenses = self::get_all_licenses();
7549
7550 unset( $all_licenses[ $this->_module_id ] );
7551
7552 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7553 }
7554
7555 /**
7556 * Check if Freemius was added on new plugin installation.
7557 *
7558 * @author Vova Feldman (@svovaf)
7559 * @since 1.1.5
7560 *
7561 * @return bool
7562 */
7563 function is_plugin_new_install() {
7564 return isset( $this->_storage->is_plugin_new_install ) &&
7565 $this->_storage->is_plugin_new_install;
7566 }
7567
7568 /**
7569 * Check if it's the first plugin release that is running Freemius.
7570 *
7571 * @author Vova Feldman (@svovaf)
7572 * @since 1.2.1.5
7573 *
7574 * @return bool
7575 */
7576 function is_first_freemius_powered_version() {
7577 return empty( $this->_storage->plugin_last_version );
7578 }
7579
7580 /**
7581 * @author Leo Fajardo (@leorw)
7582 * @since 1.2.2
7583 *
7584 * @return bool|string
7585 */
7586 private function get_previous_theme_slug() {
7587 return isset( $this->_storage->previous_theme ) ?
7588 $this->_storage->previous_theme :
7589 false;
7590 }
7591
7592 /**
7593 * @author Leo Fajardo (@leorw)
7594 * @since 1.2.2
7595 *
7596 * @return bool
7597 */
7598 private function can_activate_previous_theme() {
7599 return $this->can_activate_theme( $this->get_previous_theme_slug() );
7600 }
7601
7602 /**
7603 * @author Leo Fajardo (@leorw)
7604 * @since 2.5.0
7605 *
7606 * @return bool
7607 */
7608 private function can_activate_theme( $slug ) {
7609 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7610 $theme_instance = wp_get_theme( $slug );
7611
7612 return $theme_instance->exists();
7613 }
7614
7615 return false;
7616 }
7617
7618 /**
7619 * @author Leo Fajardo (@leorw)
7620 * @since 1.2.2
7621 */
7622 private function activate_previous_theme() {
7623 switch_theme( $this->get_previous_theme_slug() );
7624 unset( $this->_storage->previous_theme );
7625
7626 global $pagenow;
7627 if ( 'themes.php' === $pagenow ) {
7628 /**
7629 * Refresh the active theme information.
7630 *
7631 * @author Leo Fajardo (@leorw)
7632 * @since 1.2.2
7633 */
7634 fs_redirect( $this->admin_url( $pagenow ) );
7635 }
7636 }
7637
7638 /**
7639 * @author Leo Fajardo (@leorw)
7640 * @since 1.2.2
7641 *
7642 * @return string
7643 */
7644 function get_previous_theme_activation_url() {
7645 if ( ! $this->can_activate_previous_theme() ) {
7646 return '';
7647 }
7648
7649 /**
7650 * Activation URL
7651 *
7652 * @author Leo Fajardo (@leorw)
7653 * @since 1.2.2
7654 */
7655 return wp_nonce_url(
7656 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7657 'switch-theme_' . $this->get_previous_theme_slug()
7658 );
7659 }
7660
7661 /**
7662 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7663 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7664 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7665 * theme doesn't exist, then there will be no close button.
7666 *
7667 * @author Leo Fajardo (@leorw)
7668 * @since 1.2.2
7669 *
7670 * @param string $slug_or_name Old theme's slug or name.
7671 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7672 */
7673 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7674 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7675 $old_theme->get_stylesheet() :
7676 $slug_or_name;
7677
7678 $this->_activate_plugin_event_hook();
7679 }
7680
7681 /**
7682 * Plugin activated hook.
7683 *
7684 * @author Vova Feldman (@svovaf)
7685 * @since 1.0.1
7686 *
7687 * @uses FS_Api
7688 */
7689 function _activate_plugin_event_hook() {
7690 $this->_logger->entrance( 'slug = ' . $this->_slug );
7691
7692 if ( ! $this->is_user_admin() ) {
7693 return;
7694 }
7695
7696 $this->unregister_uninstall_hook();
7697
7698 // Clear API cache on activation.
7699 FS_Api::clear_cache();
7700
7701 $is_premium_version_activation = $this->is_plugin() ?
7702 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7703 $this->is_premium();
7704
7705 if ( $is_premium_version_activation && $this->is_pending_activation() ) {
7706 $this->clear_pending_activation_mode();
7707 }
7708
7709 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7710
7711 if ( $this->is_plugin() ) {
7712 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7713 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7714 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7715 $other_version_basename = $is_premium_version_activation ?
7716 $this->_free_plugin_basename :
7717 $this->premium_plugin_basename();
7718
7719 if ( ! $this->_is_network_active ) {
7720 /**
7721 * Themes are always network activated, but the ACTUAL activation is per site.
7722 *
7723 * During the activation, the plugin isn't yet active, therefore,
7724 * _is_network_active will be set to false even if it's a network level
7725 * activation. So we need to fix that by looking at the is_network_admin() value.
7726 *
7727 * @author Vova Feldman
7728 */
7729 $this->_is_network_active = (
7730 $this->_is_multisite_integrated &&
7731 fs_is_network_admin()
7732 );
7733 }
7734
7735 /**
7736 * If the other module version is active, deactivate it.
7737 *
7738 * is_plugin_active() checks if the plugin is active on the site or the network level and
7739 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7740 *
7741 * @author Leo Fajardo (@leorw)
7742 * @since 1.2.2
7743 */
7744 if (
7745 is_plugin_active( $other_version_basename ) &&
7746 $this->apply_filters( 'deactivate_on_activation', true )
7747 ) {
7748 deactivate_plugins( $other_version_basename );
7749 }
7750 }
7751
7752 if ( $this->is_registered() ) {
7753 if ( $is_premium_version_activation ) {
7754 $this->reconnect_locally();
7755 }
7756
7757
7758 // Schedule re-activation event and sync.
7759 // $this->sync_install( array(), true );
7760 $this->schedule_install_sync();
7761
7762 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7763 if ( $is_premium_version_activation ) {
7764 $this->_admin_notices->add(
7765 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7766 $this->get_text_x_inline( 'W00t',
7767 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7768 );
7769 }
7770 } else if ( $this->is_anonymous() ) {
7771 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7772 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7773 $network = true;
7774 } else {
7775 $plugin_version = isset( $this->_storage->is_anonymous ) ?
7776 $this->_storage->is_anonymous['version'] :
7777 null;
7778 $network = false;
7779 }
7780
7781 /**
7782 * Reset "skipped" click cache on the following:
7783 * 1. Freemius DEV mode.
7784 * 2. WordPress DEBUG mode.
7785 * 3. If a plugin and the user skipped the exact same version before.
7786 *
7787 * @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).
7788 *
7789 * @todo 4. If explicitly asked to retry after every activation.
7790 */
7791 if ( WP_FS__DEV_MODE ||
7792 (
7793 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7794 $this->get_plugin_version() == $plugin_version
7795 )
7796 ) {
7797 $this->reset_anonymous_mode( $network );
7798 }
7799 }
7800
7801 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7802
7803 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7804 /**
7805 * When activating an add-on, try to also activate a license.
7806 *
7807 * @author Leo Fajardo (@leorw)
7808 * @since 2.3.0
7809 */
7810 if ( ! $this->_is_network_active ) {
7811 $this->maybe_activate_addon_license();
7812 } else {
7813 $this->maybe_network_activate_addon_license();
7814 }
7815
7816 /**
7817 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7818 *
7819 * @author Leo Fajardo (@leorw)
7820 * @since 2.3.0
7821 */
7822 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7823
7824 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7825 $this->_storage->require_license_activation = false;
7826 }
7827 }
7828
7829 if (
7830 $is_premium_version_activation &&
7831 (
7832 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7833 (
7834 $this->is_registered() &&
7835 ! $is_trial_or_has_features_enabled_license
7836 )
7837 )
7838 ) {
7839 $this->_storage->require_license_activation = true;
7840 }
7841
7842 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7843 /**
7844 * If no previous version of plugin's version exist, it means that it's either
7845 * the first time that the plugin installed on the site, or the plugin was installed
7846 * before but didn't have Freemius integrated.
7847 *
7848 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7849 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7850 * only after immediate activation.
7851 *
7852 * @since 1.1.4
7853 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7854 */
7855 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7856 }
7857
7858 /**
7859 * Also flush when activating the premium version so that even if Freemius was off before, the API
7860 * connectivity test can be run again.
7861 *
7862 * @author Leo Fajardo (@leorw)
7863 * @since 2.2.3.1
7864 */
7865 $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation );
7866
7867 if ( ! $this->_anonymous_mode &&
7868 ( false !== $has_api_connectivity ) &&
7869 ! $this->_isAutoInstall
7870 ) {
7871 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7872 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7873 }
7874
7875 /**
7876 * Activation hook is executed after the plugin's main file is loaded, therefore,
7877 * after the plugin was loaded. The logic is located at activate_plugin()
7878 * ./wp-admin/includes/plugin.php.
7879 *
7880 * @author Vova Feldman (@svovaf)
7881 * @since 1.1.9
7882 */
7883 $this->_storage->was_plugin_loaded = true;
7884 }
7885
7886 /**
7887 * @author Leo Fajardo (@leorw)
7888 * @since 2.3.0
7889 */
7890 private function maybe_activate_addon_license() {
7891 $parent_fs = $this->get_parent_instance();
7892
7893 if (
7894 ! is_object( $parent_fs ) ||
7895 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7896 ) {
7897 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7898 return;
7899 }
7900
7901 $license = $this->get_active_parent_license();
7902 if ( ! is_object( $license ) ) {
7903 return;
7904 }
7905
7906 if (
7907 $this->is_bundle_license_auto_activation_enabled() &&
7908 ! empty( $license->products )
7909 ) {
7910 $this->activate_bundle_license( $license );
7911
7912 return;
7913 }
7914
7915 if ( ! $this->is_registered() ) {
7916 // Opt in with a license key.
7917 $this->opt_in(
7918 $parent_fs->get_current_or_network_user()->email,
7919 false,
7920 false,
7921 $license->secret_key
7922 );
7923 } else {
7924 // Activate the license.
7925 $install = $this->api_site_call(
7926 '/',
7927 'put',
7928 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7929 );
7930
7931 if ( ! FS_Api::is_api_error( $install ) ) {
7932 $this->_sync_addon_license( $this->get_id(), true );
7933 }
7934 }
7935 }
7936
7937 /**
7938 * @author Leo Fajardo (@leorw)
7939 * @since 2.3.0
7940 *
7941 * @param FS_Plugin_License $license
7942 */
7943 private function maybe_network_activate_addon_license( $license = null ) {
7944 $parent_fs = $this->get_parent_instance();
7945 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7946 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7947 return;
7948 }
7949
7950 $license = ( ! is_null( $license ) ) ?
7951 $license :
7952 $this->get_active_parent_license();
7953
7954 if ( ! is_object( $license ) ) {
7955 return;
7956 }
7957
7958 if (
7959 $this->is_bundle_license_auto_activation_enabled() &&
7960 ! empty( $license->products )
7961 ) {
7962 $this->activate_bundle_license( $license );
7963
7964 return;
7965 }
7966
7967 if ( ! $this->is_network_registered() ) {
7968 $sites = $this->get_sites_for_network_level_optin();
7969
7970 if ( count( $sites ) > $license->left() ) {
7971 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
7972 return;
7973 }
7974
7975 // Opt in with a license key.
7976 $this->opt_in(
7977 $parent_fs->get_user()->email,
7978 false,
7979 false,
7980 $license->secret_key,
7981 false,
7982 false,
7983 false,
7984 null,
7985 $sites
7986 );
7987 } else {
7988 $blog_2_install_map = array();
7989 $site_ids = array();
7990
7991 $all_sites = Freemius::get_sites();
7992
7993 foreach ( $all_sites as $site ) {
7994 $blog_id = Freemius::get_site_blog_id( $site );
7995 $install = $this->get_install_by_blog_id( $blog_id );
7996
7997 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
7998 // Skip license activation for installs that are already associated with a license.
7999 continue;
8000 }
8001
8002 if ( is_object( $install ) ) {
8003 $blog_2_install_map[ $blog_id ] = $install;
8004 } else {
8005 $site_ids[] = $blog_id;
8006 }
8007 }
8008
8009 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
8010 return;
8011 }
8012
8013 $user = $this->get_current_or_network_user();
8014
8015 if ( ! empty( $blog_2_install_map ) ) {
8016 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
8017
8018 if ( true !== $result ) {
8019 return;
8020 }
8021 }
8022
8023 if ( ! empty( $site_ids ) ) {
8024 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
8025 }
8026 }
8027 }
8028
8029 /**
8030 * 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.
8031 *
8032 * @author Leo Fajardo (@leorw)
8033 * @since 2.4.0
8034 *
8035 * @param FS_Plugin_License $license
8036 * @param array $sites
8037 * @param int $blog_id
8038 */
8039 private function maybe_activate_bundle_license( FS_Plugin_License $license = null, $sites = array(), $blog_id = 0 ) {
8040 if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
8041 $license = $this->_license;
8042 }
8043
8044 if ( ! is_object( $license ) ) {
8045 return;
8046 }
8047
8048 $parent_license = ( ! empty( $license->products ) ) ?
8049 $license :
8050 $this->get_active_parent_license( $license->secret_key );
8051
8052 if ( is_object( $parent_license ) ) {
8053 $this->activate_bundle_license( $parent_license, $sites, $blog_id );
8054 }
8055 }
8056
8057 /**
8058 * Try to activate a bundle license for all the bundle products installed on the site.
8059 * (1) If a child product install already has a license, the bundle license won't be activated.
8060 * (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.
8061 * (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.
8062 *
8063 * @author Leo Fajardo (@leorw)
8064 * @since 2.4.0
8065 *
8066 * @param FS_Plugin_License $license
8067 * @param array $sites
8068 * @param int $current_blog_id
8069 */
8070 private function activate_bundle_license( $license, $sites = array(), $current_blog_id = 0 ) {
8071 $is_network_admin = fs_is_network_admin();
8072
8073 $installs_by_blog_map = array();
8074 $site_info_by_blog_map = array();
8075
8076 /**
8077 * Try to activate the license for all supported products.
8078 *
8079 * @author Leo Fajardo
8080 */
8081 foreach ( $license->products as $product_id ) {
8082 $fs = self::get_instance_by_id( $product_id );
8083
8084 if ( ! is_object( $fs ) ) {
8085 continue;
8086 }
8087
8088 if ( ! $fs->has_paid_plan() ) {
8089 continue;
8090 }
8091
8092 if (
8093 ! $fs->is_addon() &&
8094 ! FS_Plan_Manager::instance()->has_paid_plan( $fs->_plans )
8095 ) {
8096 /**
8097 * The parent product can be free-only but can have its `has_paid_plan` flag set to `true` when
8098 * there is a context bundle.
8099 */
8100 continue;
8101 }
8102
8103 if ( $current_blog_id > 0 ) {
8104 $fs->switch_to_blog( $current_blog_id );
8105 }
8106
8107 if ( $fs->has_active_valid_license() ) {
8108 continue;
8109 }
8110
8111 if ( ! $is_network_admin || $current_blog_id > 0 ) {
8112 if ( $fs->is_network_active() && ! $fs->is_delegated_connection( $current_blog_id ) ) {
8113 // Do not try to activate the license in the site level if the product is network active and the connection was not delegated.
8114 continue;
8115 }
8116 } else {
8117 if ( ! $fs->is_network_active() ) {
8118 // Do not try to activate the license in the network level if the product is not network active.
8119 continue;
8120 }
8121
8122 if ( $fs->is_network_delegated_connection() ) {
8123 // Do not try to activate the license in the network level if the activation has been delegated to site admins.
8124 continue;
8125 }
8126
8127 $has_install_with_license = false;
8128
8129 // 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.
8130 $filtered_sites = array();
8131
8132 if ( empty( $sites ) ) {
8133 $all_sites = self::get_sites();
8134
8135 foreach ( $all_sites as $site ) {
8136 $sites[] = array( 'blog_id' => self::get_site_blog_id( $site ) );
8137 }
8138 } else {
8139 // Populate the map here to avoid calling `$fs->get_site_info( $site );` in the other `for` loop below.
8140 foreach ( $sites as $site ) {
8141 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8142 continue;
8143 }
8144
8145 $site_info_by_blog_map[ $site['blog_id'] ] = $site;
8146 }
8147 }
8148
8149 foreach ( $sites as $site ) {
8150 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8151 continue;
8152 }
8153
8154 $blog_id = $site['blog_id'];
8155
8156 if ( ! isset( $installs_by_blog_map[ $blog_id ] ) ) {
8157 $installs_by_blog_map[ $blog_id ] = self::get_all_sites( $fs->get_module_type(), $blog_id );
8158 }
8159
8160 $installs = $installs_by_blog_map[ $blog_id ];
8161 $install = null;
8162
8163 if ( isset( $installs[ $fs->get_slug() ] ) ) {
8164 $install = $installs[ $fs->get_slug() ];
8165
8166 if (
8167 is_object( $install ) &&
8168 (
8169 ! FS_Site::is_valid_id( $install->id ) ||
8170 ! FS_User::is_valid_id( $install->user_id ) ||
8171 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
8172 )
8173 ) {
8174 $install = null;
8175 }
8176 }
8177
8178 if (
8179 is_object( $install ) &&
8180 FS_Plugin_License::is_valid_id( $install->license_id )
8181 ) {
8182 $has_install_with_license = true;
8183 break;
8184 }
8185
8186 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
8187 // Site activation delegated, don't activate bundle license on the site in the network admin.
8188 continue;
8189 }
8190
8191 if ( ! isset( $site_info_by_blog_map[ $blog_id ] ) ) {
8192 $site_info_by_blog_map[ $blog_id ] = $fs->get_site_info( $site );
8193 }
8194
8195 $filtered_sites[] = $site_info_by_blog_map[ $blog_id ];
8196 }
8197
8198 if ( $has_install_with_license || empty( $filtered_sites ) ) {
8199 // 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.
8200 continue;
8201 }
8202
8203 $sites = $filtered_sites;
8204 }
8205
8206 $fs->activate_migrated_license(
8207 $license->secret_key,
8208 null,
8209 null,
8210 $sites,
8211 ( $current_blog_id > 0 ? $current_blog_id : null )
8212 );
8213 }
8214 }
8215
8216 /**
8217 * Returns a parent license that can be activated for the context product.
8218 *
8219 * @author Leo Fajardo (@leorw)
8220 * @since 2.3.0
8221 *
8222 * @param string|null $license_key
8223 * @param bool $flush
8224 *
8225 * @return FS_Plugin_License
8226 */
8227 function get_active_parent_license( $license_key = null, $flush = true ) {
8228 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
8229
8230 $fs = $this;
8231
8232 if ( $this->is_addon() ) {
8233 $parent_instance = $this->get_parent_instance();
8234
8235 if ( is_object( $parent_instance ) && $parent_instance->is_registered() ) {
8236 $fs = $parent_instance;
8237 }
8238 }
8239
8240 $foreign_licenses = $fs->get_foreign_licenses_info(
8241 self::get_all_licenses( $this->get_parent_id() )
8242 );
8243
8244 if ( ! empty ( $foreign_licenses ) ) {
8245 $foreign_licenses = array(
8246 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
8247 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
8248 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
8249 );
8250
8251 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
8252 }
8253
8254 $result = $fs->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, $flush );
8255
8256 if (
8257 ! $this->is_api_result_object( $result, 'licenses' ) ||
8258 ! is_array( $result->licenses ) ||
8259 empty( $result->licenses )
8260 ) {
8261 return null;
8262 }
8263
8264 $parent_license = null;
8265
8266 if ( empty( $license_key ) ) {
8267 $parent_license = $result->licenses[0];
8268 } else {
8269 foreach ( $result->licenses as $license ) {
8270 if ( $license_key === $license->secret_key ) {
8271 $parent_license = $license;
8272 break;
8273 }
8274 }
8275 }
8276
8277 if ( ! is_null( $parent_license ) ) {
8278 $parent_license = new FS_Plugin_License( $parent_license );
8279 }
8280
8281 return $parent_license;
8282 }
8283
8284 /**
8285 * @author Leo Fajardo (@leorw)
8286 * @since 2.3.0
8287 *
8288 * @return array
8289 */
8290 function get_sites_for_network_level_optin() {
8291 $sites = array();
8292 $all_sites = self::get_sites();
8293
8294 foreach ( $all_sites as $site ) {
8295 $blog_id = self::get_site_blog_id( $site );
8296
8297 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
8298 ! $this->is_installed_on_site( $blog_id )
8299 ) {
8300 $sites[] = $this->get_site_info( $site );
8301 }
8302 }
8303
8304 return $sites;
8305 }
8306
8307 /**
8308 * Delete account.
8309 *
8310 * @author Vova Feldman (@svovaf)
8311 * @since 1.0.3
8312 *
8313 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8314 */
8315 function delete_account_event( $check_user = true ) {
8316 $this->_logger->entrance( 'slug = ' . $this->_slug );
8317
8318 if ( $check_user && ! $this->is_user_admin() ) {
8319 return;
8320 }
8321
8322 $this->do_action( 'before_account_delete' );
8323
8324 // Clear all admin notices.
8325 $this->_admin_notices->clear_all_sticky( false );
8326
8327 $this->_delete_site( false );
8328
8329 $delete_network_common_data = true;
8330
8331 if ( $this->_is_network_active ) {
8332 $installs = $this->get_blog_install_map();
8333
8334 // Don't delete common network data unless no other installs left.
8335 $delete_network_common_data = empty( $installs );
8336 }
8337
8338 if ( $delete_network_common_data ) {
8339 $this->_delete_plans( false );
8340
8341 $this->_delete_licenses( false );
8342
8343 // Delete add-ons related to plugin's account.
8344 $this->_delete_account_addons( false );
8345 }
8346
8347 // @todo Delete plans and licenses of add-ons.
8348
8349 self::$_accounts->store();
8350
8351 /**
8352 * IMPORTANT:
8353 * Clear crons must be executed before clearing all storage.
8354 * Otherwise, the cron will not be cleared.
8355 */
8356 if ( $delete_network_common_data ) {
8357 $this->clear_sync_cron();
8358 }
8359
8360 $this->clear_install_sync_cron();
8361
8362 // Clear all storage data.
8363 $this->_storage->clear_all( true, array(
8364 'is_delegated_connection',
8365 'connectivity_test',
8366 'is_on',
8367 ), false );
8368
8369 // Send delete event.
8370 $this->get_api_site_scope()->call( '/', 'delete' );
8371
8372 $this->do_action( 'after_account_delete' );
8373 }
8374
8375 /**
8376 * Delete network level account.
8377 *
8378 * @author Vova Feldman (@svovaf)
8379 * @since 2.0.0
8380 *
8381 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8382 */
8383 function delete_network_account_event( $check_user = true ) {
8384 $this->_logger->entrance( 'slug = ' . $this->_slug );
8385
8386 if ( $check_user && ! $this->is_user_admin() ) {
8387 return;
8388 }
8389
8390 $this->do_action( 'before_network_account_delete' );
8391
8392 // Clear all admin notices.
8393 $this->_admin_notices->clear_all_sticky();
8394
8395 $this->_delete_plans( false, false );
8396
8397 $this->_delete_licenses( false );
8398
8399 // Delete add-ons related to plugin's account.
8400 $this->_delete_account_addons( false );
8401
8402 // @todo Delete plans and licenses of add-ons.
8403
8404 self::$_accounts->store( true );
8405
8406 /**
8407 * IMPORTANT:
8408 * Clear crons must be executed before clearing all storage.
8409 * Otherwise, the cron will not be cleared.
8410 */
8411 $this->clear_sync_cron( true );
8412 $this->clear_install_sync_cron( true );
8413
8414 $sites = self::get_sites();
8415
8416 $install_ids = array();
8417 foreach ( $sites as $site ) {
8418 $blog_id = self::get_site_blog_id( $site );
8419
8420 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8421 continue;
8422 }
8423
8424 $install_id = $this->_delete_site( true, $blog_id );
8425
8426 // Clear all storage data.
8427 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8428
8429 if ( FS_Site::is_valid_id( $install_id ) ) {
8430 $install_ids[] = $install_id;
8431 }
8432
8433 switch_to_blog( $blog_id );
8434
8435 $this->do_action( 'after_account_delete' );
8436
8437 restore_current_blog();
8438 }
8439
8440 $this->_storage->clear_all( true, array(
8441 'connectivity_test',
8442 'is_on',
8443 ), true );
8444
8445 // Send delete event.
8446 if ( ! empty( $install_ids ) ) {
8447 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8448 }
8449
8450 $this->do_action( 'after_network_account_delete' );
8451 }
8452
8453 /**
8454 * Plugin deactivation hook.
8455 *
8456 * @author Vova Feldman (@svovaf)
8457 * @since 1.0.1
8458 */
8459 function _deactivate_plugin_hook() {
8460 $this->_logger->entrance( 'slug = ' . $this->_slug );
8461
8462 if ( ! $this->is_user_admin() ) {
8463 return;
8464 }
8465
8466 $is_network_deactivation = fs_is_network_admin();
8467 $storage_keys_for_removal = array();
8468
8469 $this->_admin_notices->clear_all_sticky();
8470
8471 $storage_keys_for_removal[] = 'sticky_optin_added';
8472 if ( isset( $this->_storage->sticky_optin_added ) ) {
8473 unset( $this->_storage->sticky_optin_added );
8474 }
8475
8476 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8477 // Remember that plugin was already installed.
8478 $this->_storage->is_plugin_new_install = false;
8479 }
8480
8481 // Hook to plugin uninstall.
8482 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8483
8484 $this->clear_module_main_file_cache();
8485 $this->clear_sync_cron( $this->_is_network_active );
8486 $this->clear_install_sync_cron();
8487
8488 if ( $this->is_registered() ) {
8489 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8490 FS_Plugin_Updater::instance( $this )->delete_update_data();
8491 }
8492
8493 if ( $is_network_deactivation ) {
8494 // Send deactivation event.
8495 $this->sync_installs( array(
8496 'is_active' => false,
8497 ) );
8498 } else {
8499 // Send deactivation event.
8500 $this->sync_install( array(
8501 'is_active' => false,
8502 ) );
8503 }
8504 } else {
8505 if ( false === $this->has_api_connectivity() && ! $this->is_premium() ) {
8506 // Reset connectivity test cache.
8507 $this->clear_connectivity_info();
8508
8509 $storage_keys_for_removal[] = 'connectivity_test';
8510 }
8511 }
8512
8513 if ( $is_network_deactivation ) {
8514 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8515 unset( $this->_storage->sticky_optin_added_ms );
8516 }
8517
8518 if ( ! empty( $storage_keys_for_removal ) ) {
8519 $sites = self::get_sites();
8520
8521 foreach ( $sites as $site ) {
8522 $blog_id = self::get_site_blog_id( $site );
8523
8524 foreach ( $storage_keys_for_removal as $key ) {
8525 $this->_storage->remove( $key, false, $blog_id );
8526 }
8527
8528 $this->_storage->save( $blog_id );
8529 }
8530 }
8531 }
8532
8533 // Clear API cache on deactivation.
8534 FS_Api::clear_cache();
8535
8536 $this->remove_sdk_reference();
8537 }
8538
8539 /**
8540 * @author Vova Feldman (@svovaf)
8541 * @since 1.1.6
8542 */
8543 private function remove_sdk_reference() {
8544 global $fs_active_plugins;
8545
8546 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8547 if ( $this->_plugin_basename == $data->plugin_path ) {
8548 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8549 break;
8550 }
8551 }
8552
8553 fs_fallback_to_newest_active_sdk();
8554 }
8555
8556 /**
8557 * @author Vova Feldman (@svovaf)
8558 * @since 1.1.3
8559 *
8560 * @param bool $is_anonymous
8561 * @param bool|int $network_or_blog_id Since 2.0.0
8562 */
8563 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8564 // Store information regarding skip to try and opt-in the user
8565 // again in the future.
8566 $skip_info = array(
8567 'is' => $is_anonymous,
8568 'timestamp' => WP_FS__SCRIPT_START_TIME,
8569 'version' => $this->get_plugin_version(),
8570 );
8571
8572 if ( true === $network_or_blog_id ) {
8573 $this->_storage->is_anonymous_ms = $skip_info;
8574 } else {
8575 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8576 }
8577
8578 $this->network_upgrade_mode_completed();
8579
8580 // Update anonymous mode cache.
8581 $this->_is_anonymous = $is_anonymous;
8582 }
8583
8584 /**
8585 * @author Vova Feldman (@svovaf)
8586 * @since 2.5.1
8587 *
8588 * @param bool|int $network_or_blog_id
8589 */
8590 private function unset_anonymous_mode( $network_or_blog_id = 0 ) {
8591 if ( true === $network_or_blog_id ) {
8592 unset( $this->_storage->is_anonymous_ms );
8593 } else {
8594 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8595 }
8596 }
8597
8598 /**
8599 * @author Vova Feldman (@svovaf)
8600 * @since 2.0.0
8601 *
8602 * @param int $blog_id Site ID.
8603 * @param int $user_id User ID.
8604 * @param string $domain Site domain.
8605 * @param string $path Site path.
8606 * @param int $network_id Network ID. Only relevant on multi-network installations.
8607 * @param array $meta Metadata. Used to set initial site options.
8608 *
8609 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8610 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8611 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8612 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8613 */
8614 public function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8615 $this->_logger->entrance();
8616
8617 if ( ! $this->_is_network_active ) {
8618 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
8619 return;
8620 }
8621
8622 $site = null;
8623 $new_blog_id = $blog_id;
8624
8625 if ( $this->is_premium() &&
8626 $this->is_network_connected() &&
8627 is_object( $this->_license ) &&
8628 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8629 $this->is_license_network_active( $blog_id )
8630 ) {
8631 /**
8632 * 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.
8633 */
8634 $current_blog_id = get_current_blog_id();
8635 $license = clone $this->_license;
8636
8637 $this->switch_to_blog( $blog_id );
8638
8639 // Opt-in with network user.
8640 $this->install_with_user(
8641 $this->get_network_user(),
8642 $license->secret_key,
8643 false,
8644 false,
8645 false
8646 );
8647
8648 if ( is_object( $this->_site ) ) {
8649 if ( $this->_site->license_id == $license->id ) {
8650 /**
8651 * If the license was activated successfully, sync the license data from the remote server.
8652 */
8653 $this->_license = $license;
8654 $this->sync_site_license();
8655 }
8656 }
8657
8658 $site = $this->_site;
8659
8660 $this->switch_to_blog( $current_blog_id );
8661
8662 if ( is_object( $site ) ) {
8663 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id, $site );
8664
8665 // Already connected (with or without a license), so no need to continue.
8666 return;
8667 }
8668 }
8669
8670 if ( $this->is_network_anonymous() ) {
8671 /**
8672 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8673 */
8674 $this->skip_site_connection( $blog_id );
8675 } else if ( $this->is_network_delegated_connection() ) {
8676 /**
8677 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8678 */
8679 $this->delegate_site_connection( $blog_id );
8680 } else if ( $this->is_network_connected() ) {
8681 /**
8682 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8683 */
8684 $current_blog_id = get_current_blog_id();
8685
8686 $this->switch_to_blog( $blog_id );
8687
8688 // Opt-in with network user.
8689 $this->install_with_user(
8690 $this->get_network_user(),
8691 false,
8692 false,
8693 false,
8694 false
8695 );
8696
8697 $site = $this->_site;
8698
8699 $this->switch_to_blog( $current_blog_id );
8700 } else {
8701 /**
8702 * If the super-admin mixed different options (connect, skip, delegated):
8703 * a) If at least one site connection was delegated, then automatically delegate connection.
8704 * 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.
8705 */
8706 $has_delegated_site = false;
8707
8708 $sites = self::get_sites();
8709 foreach ( $sites as $wp_site ) {
8710 $blog_id = self::get_site_blog_id( $wp_site );
8711
8712 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8713 $has_delegated_site = true;
8714 break;
8715 }
8716 }
8717
8718 if ( $has_delegated_site ) {
8719 $this->delegate_site_connection( $blog_id );
8720 } else {
8721 $this->skip_site_connection( $blog_id );
8722 }
8723 }
8724
8725 /**
8726 * 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.
8727 *
8728 * @author Leo Fajardo (@leorw)
8729 * @since 2.5.0
8730 */
8731 FS_Clone_Manager::instance()->store_blog_install_info( $new_blog_id, $site );
8732 }
8733
8734 /**
8735 * @author Vova Feldman (@svovaf)
8736 * @since 2.5.0
8737 *
8738 * @param \WP_Site $new_site
8739 * @param array $args
8740 */
8741 public function _after_wp_initialize_site_callback( WP_Site $new_site, $args ) {
8742 $this->_logger->entrance();
8743
8744 $this->_after_new_blog_callback(
8745 $new_site->id,
8746 // Dummy user ID (not in use).
8747 0,
8748 $new_site->domain,
8749 $new_site->path,
8750 $new_site->network_id,
8751 // Dummy meta, not in use.
8752 array()
8753 );
8754 }
8755
8756 /**
8757 * @author Vova Feldman (@svovaf)
8758 * @since 1.1.3
8759 *
8760 * @param bool|int|int[] $network_or_blog_ids Since 2.0.0.
8761 */
8762 private function reset_anonymous_mode( $network_or_blog_ids = false ) {
8763 if ( true === $network_or_blog_ids ) {
8764 $this->unset_anonymous_mode( true );
8765
8766 if ( fs_is_network_admin() ) {
8767 $this->_is_anonymous = null;
8768 }
8769
8770 // Rest anonymous mode for all non-delegated sub-sites.
8771 $blog_ids = $this->get_non_delegated_blog_ids();
8772 }
8773 else
8774 {
8775 if ( false === $network_or_blog_ids ) {
8776 $network_or_blog_ids = 0;
8777 }
8778
8779 $blog_ids = is_array( $network_or_blog_ids ) ?
8780 $network_or_blog_ids :
8781 array( $network_or_blog_ids );
8782
8783 foreach ( $blog_ids as $blog_id ) {
8784 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8785 $this->_is_anonymous = null;
8786 }
8787 }
8788 }
8789
8790 foreach ( $blog_ids as $blog_id ) {
8791 $this->unset_anonymous_mode( $blog_id );
8792 }
8793
8794 /**
8795 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8796 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8797 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8798 *
8799 * @author Leo Fajardo (@leorw)
8800 * @since 1.2.2
8801 */
8802 if ( ! $this->_is_network_active ) {
8803 $this->_is_anonymous = null;
8804 }
8805 }
8806
8807 /**
8808 * @author Leo Fajardo (@leorw)
8809 * @since 2.5.3
8810 */
8811 private function update_license_required_permissions_if_anonymous() {
8812 if ( ! $this->is_anonymous() ) {
8813 return;
8814 }
8815
8816 $this->reset_anonymous_mode( fs_is_network_admin() );
8817
8818 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
8819 'essentials' => true,
8820 'events' => true,
8821 'diagnostic' => false,
8822 'extensions' => false,
8823 'site' => false,
8824 ) );
8825 }
8826
8827 /**
8828 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8829 * deleting the account in the network level, the URL of the page to redirect to is correct.
8830 *
8831 * @author Leo Fajardo (@leorw)
8832 *
8833 * @since 2.1.3
8834 */
8835 private function maybe_set_slug_and_network_menu_exists_flag() {
8836 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8837 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8838 $this->_menu->get_slug() :
8839 $this->_slug
8840 );
8841 }
8842 }
8843
8844 /**
8845 * Clears the anonymous mode and redirects to the opt-in screen.
8846 *
8847 * @author Vova Feldman (@svovaf)
8848 * @since 1.1.7
8849 */
8850 function connect_again() {
8851 if ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) {
8852 return;
8853 }
8854
8855 if ( $this->is_anonymous() ) {
8856 $this->reset_anonymous_mode( fs_is_network_admin() );
8857 }
8858
8859 $activation_url_params = array();
8860
8861 if ( $this->is_pending_activation() ) {
8862 $this->clear_pending_activation_mode();
8863
8864 if ( fs_request_get_bool( 'require_license' ) ) {
8865 $activation_url_params['require_license'] = true;
8866 }
8867 }
8868
8869 $this->maybe_set_slug_and_network_menu_exists_flag();
8870
8871 fs_redirect( $this->get_activation_url( $activation_url_params ) );
8872 }
8873
8874 /**
8875 * Skip account connect, and set anonymous mode.
8876 *
8877 * @author Vova Feldman (@svovaf)
8878 * @since 1.1.1
8879 *
8880 * @param bool|int|int[] $network_or_blog_ids Since 2.5.1
8881 */
8882 function skip_connection( $network_or_blog_ids = false ) {
8883 $this->_logger->entrance();
8884
8885 $this->_admin_notices->remove_sticky( 'connect_account' );
8886
8887 if ( true === $network_or_blog_ids ) {
8888 $this->set_anonymous_mode( true, true );
8889
8890 if ( fs_is_network_admin() ) {
8891 $this->_is_anonymous = null;
8892 }
8893
8894 // Rest anonymous mode for all non-delegated sub-sites.
8895 $blog_ids = $this->get_non_delegated_blog_ids();
8896 }
8897 else
8898 {
8899 if ( false === $network_or_blog_ids ) {
8900 $network_or_blog_ids = 0;
8901 }
8902
8903 $blog_ids = is_array( $network_or_blog_ids ) ?
8904 $network_or_blog_ids :
8905 array( $network_or_blog_ids );
8906
8907 foreach ( $blog_ids as $blog_id ) {
8908 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8909 $this->_is_anonymous = null;
8910 }
8911 }
8912 }
8913
8914 foreach ( $blog_ids as $blog_id ) {
8915 $this->skip_site_connection( $blog_id );
8916 }
8917
8918 $this->network_upgrade_mode_completed();
8919 }
8920
8921 /**
8922 * Skip connection for specific site in the network.
8923 *
8924 * @author Vova Feldman (@svovaf)
8925 * @since 2.0.0
8926 *
8927 * @param int|null $blog_id
8928 * @param bool $send_skip
8929 */
8930 private function skip_site_connection( $blog_id = null ) {
8931 $this->_logger->entrance();
8932
8933 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8934
8935 $this->set_anonymous_mode( true, $blog_id );
8936 }
8937
8938 /**
8939 * Plugin version update hook.
8940 *
8941 * @author Vova Feldman (@svovaf)
8942 * @since 1.0.4
8943 */
8944 private function update_plugin_version_event() {
8945 $this->_logger->entrance();
8946
8947 if ( ! $this->is_registered() ) {
8948 return;
8949 }
8950
8951 $this->schedule_install_sync();
8952 // $this->sync_install( array(), true );
8953 }
8954
8955 /**
8956 * Generate an MD5 signature of a plugins collection.
8957 * This helper methods used to identify changes in a plugins collection.
8958 *
8959 * @author Vova Feldman (@svovaf)
8960 * @since 2.0.0
8961 *
8962 * @param array [string]array $plugins
8963 *
8964 * @return string
8965 */
8966 private function get_plugins_thumbprint( $plugins ) {
8967 ksort( $plugins );
8968
8969 $thumbprint = '';
8970 foreach ( $plugins as $basename => $data ) {
8971 $thumbprint .= $data['slug'] . ',' .
8972 $data['Version'] . ',' .
8973 ( $data['is_active'] ? '1' : '0' ) . ';';
8974 }
8975
8976 return md5( $thumbprint );
8977 }
8978
8979 /**
8980 * Return a list of modified plugins since the last sync.
8981 *
8982 * Note:
8983 * There's no point to store a plugins counter since even if the number of
8984 * plugins didn't change, we still need to check if the versions are all the
8985 * same and the activity state is similar.
8986 *
8987 * @author Vova Feldman (@svovaf)
8988 * @since 1.1.8
8989 *
8990 * @return array|false
8991 */
8992 private function get_plugins_data_for_api() {
8993 // Alias.
8994 $site_active_plugins_option_name = 'active_plugins';
8995 $network_plugins_option_name = 'all_plugins';
8996
8997 /**
8998 * Collection of all site level active plugins.
8999 */
9000 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
9001
9002 if ( ! is_object( $site_active_plugins_cache ) ) {
9003 $site_active_plugins_cache = (object) array(
9004 'timestamp' => '',
9005 'md5' => '',
9006 'plugins' => array(),
9007 );
9008 }
9009
9010 $time = time();
9011
9012 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
9013 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
9014 ) {
9015 // Don't send plugin updates if last update was in the past 5 min.
9016 return false;
9017 }
9018
9019 // Write timestamp to lock the logic.
9020 $site_active_plugins_cache->timestamp = $time;
9021 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
9022
9023 // Reload options from DB.
9024 self::$_accounts->load( true );
9025 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
9026
9027 if ( $time != $site_active_plugins_cache->timestamp ) {
9028 // If timestamp is different, then another thread captured the lock.
9029 return false;
9030 }
9031
9032 /**
9033 * Collection of all plugins (network level).
9034 */
9035 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
9036
9037 if ( ! is_object( $network_plugins_cache ) ) {
9038 $network_plugins_cache = (object) array(
9039 'timestamp' => '',
9040 'md5' => '',
9041 'plugins' => array(),
9042 );
9043 }
9044
9045 // Check if there's a change in plugins.
9046 $network_plugins = self::get_network_plugins();
9047 $site_active_plugins = self::get_site_active_plugins();
9048
9049 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
9050 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
9051
9052 // Check if plugins status changed (version or active/inactive).
9053 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
9054 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
9055
9056 if ( ! $network_plugins_changed &&
9057 ! $site_active_plugins_changed
9058 ) {
9059 // No changes.
9060 return array();
9061 }
9062
9063 $plugins_update_data = array();
9064
9065 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
9066 if ( ! isset( $network_plugins[ $basename ] ) ) {
9067 // Plugin uninstalled.
9068 $uninstalled_plugin_data = $data;
9069 $uninstalled_plugin_data['is_active'] = false;
9070 $uninstalled_plugin_data['is_uninstalled'] = true;
9071 $plugins_update_data[] = $uninstalled_plugin_data;
9072
9073 unset( $network_plugins[ $basename ] );
9074
9075 unset( $network_plugins_cache->plugins[ $basename ] );
9076 unset( $site_active_plugins_cache->plugins[ $basename ] );
9077
9078 continue;
9079 }
9080
9081 $was_active = $data['is_active'] ||
9082 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9083 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
9084 $is_active = $network_plugins[ $basename ]['is_active'] ||
9085 ( isset( $site_active_plugins[ $basename ] ) &&
9086 $site_active_plugins[ $basename ]['is_active'] );
9087
9088 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9089 isset( $site_active_plugins[ $basename ] )
9090 ) {
9091 // Plugin was site level activated.
9092 $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
9093 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
9094 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9095 ! isset( $site_active_plugins[ $basename ] )
9096 ) {
9097 // Plugin was site level deactivated.
9098 unset( $site_active_plugins_cache->plugins[ $basename ] );
9099 }
9100
9101 $prev_version = $data['version'];
9102 $current_version = $network_plugins[ $basename ]['Version'];
9103
9104 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
9105 // Plugin activated or deactivated, or version changed.
9106
9107 if ( $was_active !== $is_active ) {
9108 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
9109 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
9110 }
9111 }
9112
9113 if ( $prev_version !== $current_version ) {
9114 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
9115 }
9116
9117 $updated_plugin_data = $data;
9118 $updated_plugin_data['is_active'] = $is_active;
9119 $updated_plugin_data['version'] = $current_version;
9120 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
9121 $plugins_update_data[] = $updated_plugin_data;
9122 }
9123 }
9124
9125 // Find new plugins that weren't yet seen before.
9126 foreach ( $network_plugins as $basename => $data ) {
9127 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
9128 // New plugin.
9129 $new_plugin = array(
9130 'slug' => $data['slug'],
9131 'version' => $data['Version'],
9132 'title' => $data['Name'],
9133 'is_active' => $data['is_active'],
9134 'is_uninstalled' => false,
9135 );
9136
9137 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
9138
9139 $is_site_level_active = (
9140 isset( $site_active_plugins[ $basename ] ) &&
9141 $site_active_plugins[ $basename ]['is_active']
9142 );
9143
9144 /**
9145 * If not network active, set the activity status based on the site-level plugin status.
9146 */
9147 if ( ! $new_plugin['is_active'] ) {
9148 $new_plugin['is_active'] = $is_site_level_active;
9149 }
9150
9151 $plugins_update_data[] = $new_plugin;
9152
9153 if ( isset( $site_active_plugins[ $basename ] ) ) {
9154 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
9155 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
9156 }
9157 }
9158 }
9159
9160 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
9161 $site_active_plugins_cache->timestamp = $time;
9162 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
9163
9164 $network_plugins_cache->md5 = $network_plugins_thumbprint;
9165 $network_plugins_cache->timestamp = $time;
9166 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
9167
9168 return $plugins_update_data;
9169 }
9170
9171 /**
9172 * Return a list of modified themes since the last sync.
9173 *
9174 * Note:
9175 * There's no point to store a themes counter since even if the number of
9176 * themes didn't change, we still need to check if the versions are all the
9177 * same and the activity state is similar.
9178 *
9179 * @author Vova Feldman (@svovaf)
9180 * @since 1.1.8
9181 *
9182 * @return array|false
9183 */
9184 private function get_themes_data_for_api() {
9185 // Alias.
9186 $option_name = 'all_themes';
9187
9188 $all_cached_themes = self::$_accounts->get_option( $option_name );
9189
9190 if ( ! is_object( $all_cached_themes ) ) {
9191 $all_cached_themes = (object) array(
9192 'timestamp' => '',
9193 'md5' => '',
9194 'themes' => array(),
9195 );
9196 }
9197
9198 $time = time();
9199
9200 if ( ! empty( $all_cached_themes->timestamp ) &&
9201 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
9202 ) {
9203 // Don't send theme updates if last update was in the past 5 min.
9204 return false;
9205 }
9206
9207 // Write timestamp to lock the logic.
9208 $all_cached_themes->timestamp = $time;
9209 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9210
9211 // Reload options from DB.
9212 self::$_accounts->load( true );
9213 $all_cached_themes = self::$_accounts->get_option( $option_name );
9214
9215 if ( $time != $all_cached_themes->timestamp ) {
9216 // If timestamp is different, then another thread captured the lock.
9217 return false;
9218 }
9219
9220 // Get active theme.
9221 $active_theme = wp_get_theme();
9222 $active_theme_stylesheet = $active_theme->get_stylesheet();
9223
9224 // Check if there's a change in themes.
9225 $all_themes = wp_get_themes();
9226
9227 // Check if themes changed.
9228 ksort( $all_themes );
9229
9230 $themes_signature = '';
9231 foreach ( $all_themes as $slug => $data ) {
9232 $is_active = ( $slug === $active_theme_stylesheet );
9233 $themes_signature .= $slug . ',' .
9234 $data->version . ',' .
9235 ( $is_active ? '1' : '0' ) . ';';
9236 }
9237
9238 // Check if themes status changed (version or active/inactive).
9239 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
9240
9241 $themes_update_data = array();
9242
9243 if ( $themes_changed ) {
9244 // Change in themes, report changes.
9245
9246 // Update existing themes info.
9247 foreach ( $all_cached_themes->themes as $slug => $data ) {
9248 $is_active = ( $slug === $active_theme_stylesheet );
9249
9250 if ( ! isset( $all_themes[ $slug ] ) ) {
9251 // Plugin uninstalled.
9252 $uninstalled_theme_data = $data;
9253 $uninstalled_theme_data['is_active'] = false;
9254 $uninstalled_theme_data['is_uninstalled'] = true;
9255 $themes_update_data[] = $uninstalled_theme_data;
9256
9257 unset( $all_themes[ $slug ] );
9258 unset( $all_cached_themes->themes[ $slug ] );
9259 } else if ( $data['is_active'] !== $is_active ||
9260 $data['version'] !== $all_themes[ $slug ]->version
9261 ) {
9262 // Plugin activated or deactivated, or version changed.
9263
9264 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
9265 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
9266
9267 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
9268 }
9269 }
9270
9271 // Find new themes that weren't yet seen before.
9272 foreach ( $all_themes as $slug => $data ) {
9273 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
9274 $is_active = ( $slug === $active_theme_stylesheet );
9275
9276 // New plugin.
9277 $new_plugin = array(
9278 'slug' => $slug,
9279 'version' => $data->version,
9280 'title' => $data->name,
9281 'is_active' => $is_active,
9282 'is_uninstalled' => false,
9283 );
9284
9285 $themes_update_data[] = $new_plugin;
9286 $all_cached_themes->themes[ $slug ] = $new_plugin;
9287 }
9288 }
9289
9290 $all_cached_themes->md5 = md5( $themes_signature );
9291 $all_cached_themes->timestamp = time();
9292 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9293 }
9294
9295 return $themes_update_data;
9296 }
9297
9298 /**
9299 * Get site data for API install request.
9300 *
9301 * @author Vova Feldman (@svovaf)
9302 * @since 1.1.2
9303 *
9304 * @param string[] $override
9305 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9306 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9307 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, title, and URL).
9308 *
9309 * @return array
9310 */
9311 private function get_install_data_for_api(
9312 array $override,
9313 $include_plugins = true,
9314 $include_themes = true,
9315 $include_blog_data = true
9316 ) {
9317 // Alias.
9318 $permissions = FS_Permission_Manager::instance( $this );
9319
9320 if ( $permissions->is_extensions_tracking_allowed() ) {
9321 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
9322 /**
9323 * @since 1.1.8 Also send plugin updates.
9324 */
9325 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9326 $plugins = $this->get_plugins_data_for_api();
9327 if ( ! empty( $plugins ) ) {
9328 $override['plugins'] = $plugins;
9329 }
9330 }
9331 }
9332
9333 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
9334 /**
9335 * @since 1.1.8 Also send themes updates.
9336 */
9337 if ( $include_themes && ! isset( $override['themes'] ) ) {
9338 $themes = $this->get_themes_data_for_api();
9339 if ( ! empty( $themes ) ) {
9340 $override['themes'] = $themes;
9341 }
9342 }
9343 }
9344 }
9345
9346 $versions = $this->get_versions();
9347
9348 $blog_data = array();
9349 if ( $include_blog_data ) {
9350 $blog_data['url'] = self::get_unfiltered_site_url();
9351
9352 if ( $permissions->is_diagnostic_tracking_allowed() ) {
9353 $blog_data = array_merge( $blog_data, array(
9354 'language' => self::get_sanitized_language(),
9355 'title' => get_bloginfo( 'name' ),
9356 ) );
9357 }
9358 }
9359
9360 return array_merge( $versions, $blog_data, array(
9361 'version' => $this->get_plugin_version(),
9362 'is_premium' => $this->is_premium(),
9363 // Special params.
9364 'is_active' => true,
9365 'is_uninstalled' => false,
9366 ), $override );
9367 }
9368
9369 /**
9370 * Update installs details.
9371 *
9372 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
9373 *
9374 * @author Vova Feldman (@svovaf)
9375 * @since 2.0.0
9376 *
9377 * @param string[] string $override
9378 * @param bool $only_diff
9379 * @param bool $is_keepalive
9380 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9381 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9382 *
9383 * @return array
9384 */
9385 private function get_installs_data_for_api(
9386 array $override,
9387 $only_diff = false,
9388 $is_keepalive = false,
9389 $include_plugins = true,
9390 $include_themes = true
9391 ) {
9392 /**
9393 * @since 1.1.8 Also send plugin updates.
9394 */
9395 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9396 // $plugins = $this->get_plugins_data_for_api();
9397 // if ( ! empty( $plugins ) ) {
9398 // $override['plugins'] = $plugins;
9399 // }
9400 // }
9401 /**
9402 * @since 1.1.8 Also send themes updates.
9403 */
9404 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9405 // $themes = $this->get_themes_data_for_api();
9406 // if ( ! empty( $themes ) ) {
9407 // $override['themes'] = $themes;
9408 // }
9409 // }
9410
9411 // Common properties.
9412 $versions = $this->get_versions();
9413 $common = array_merge( $versions, array(
9414 'version' => $this->get_plugin_version(),
9415 'is_premium' => $this->is_premium(),
9416 ), $override );
9417
9418
9419 $is_common_diff_for_any_site = false;
9420 $common_diff_union = array();
9421
9422 $installs_data = array();
9423
9424 $sites = self::get_sites();
9425
9426 $subsite_data_for_api_by_install_id = array();
9427 $install_url_by_install_id = array();
9428 $subsite_registration_date_by_install_id = array();
9429
9430 foreach ( $sites as $site ) {
9431 $blog_id = self::get_site_blog_id( $site );
9432
9433 $install = $this->get_install_by_blog_id( $blog_id );
9434
9435 if ( is_object( $install ) ) {
9436 if ( $install->user_id != $this->_user->id ) {
9437 // Install belongs to a different owner.
9438 continue;
9439 }
9440
9441 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
9442 // Don't send updates regarding opted-out installs.
9443 continue;
9444 }
9445
9446 $install_data = $this->get_site_info( $site, true );
9447
9448 if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $install_data['blog_id'] ) ) {
9449 continue;
9450 }
9451
9452 $uid = $install_data['uid'];
9453 $url = $install_data['url'];
9454 $registration_date = $install_data['registration_date'];
9455
9456 if ( isset( $subsite_data_for_api_by_install_id[ $install->id ] ) ) {
9457 $clone_subsite_data = $subsite_data_for_api_by_install_id[ $install->id ];
9458 $clone_install_url = $install_url_by_install_id[ $install->id ];
9459 $clone_subsite_registration_date = $subsite_registration_date_by_install_id[ $install->id ];
9460
9461 $skip = false;
9462
9463 if (
9464 ! empty( $install_data['registration_date'] ) &&
9465 ! empty( $clone_subsite_registration_date )
9466 ) {
9467 /**
9468 * 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.
9469 *
9470 * @author Leo Fajardo (@leorw)
9471 * @since 2.5.1
9472 */
9473 $skip = ( strtotime( $install_data['registration_date'] ) > strtotime( $clone_subsite_registration_date ) );
9474 } else if (
9475 /**
9476 * 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.
9477 *
9478 * @author Leo Fajardo (@leorw)
9479 * @since 2.5.0
9480 */
9481 fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) ||
9482 fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $url ) )
9483 ) {
9484 $skip = true;
9485 }
9486
9487 if ( $skip ) {
9488 // 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.
9489 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
9490 continue;
9491 }
9492 }
9493
9494 unset( $install_data['blog_id'] );
9495 unset( $install_data['uid'] );
9496 unset( $install_data['url'] );
9497 unset( $install_data['registration_date'] );
9498
9499 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9500 $install_data['is_uninstalled'] = $install->is_uninstalled;
9501
9502 $common_diff = null;
9503 $is_common_diff = false;
9504 if ( $only_diff ) {
9505 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9506 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9507
9508 $is_common_diff = ! empty( $common_diff );
9509
9510 if ( $is_common_diff ) {
9511 foreach ( $common_diff as $k => $v ) {
9512 if ( ! isset( $common_diff_union[ $k ] ) ) {
9513 $common_diff_union[ $k ] = $v;
9514 }
9515 }
9516 }
9517
9518 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9519 }
9520
9521 if ( ! empty( $install_data ) || $is_common_diff || $is_keepalive ) {
9522 // Add install ID and site unique ID.
9523 $install_data['id'] = $install->id;
9524 $install_data['uid'] = $uid;
9525 $install_data['url'] = $url;
9526
9527 $subsite_data_for_api_by_install_id[ $install->id ] = $install_data;
9528 $install_url_by_install_id[ $install->id ] = $install->url;
9529 $subsite_registration_date_by_install_id[ $install->id ] = $registration_date;
9530 }
9531 }
9532 }
9533
9534 restore_current_blog();
9535
9536 $installs_data = array_merge(
9537 $installs_data,
9538 array_values( $subsite_data_for_api_by_install_id )
9539 );
9540
9541 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9542 if ( ! $only_diff ) {
9543 $installs_data[] = $common;
9544 } else if ( ! empty( $common_diff_union ) ) {
9545 $installs_data[] = $common_diff_union;
9546 }
9547 }
9548
9549 foreach ( $installs_data as &$data ) {
9550 $data = (object) $data;
9551 }
9552
9553 return $installs_data;
9554 }
9555
9556 /**
9557 * Compare site actual data to the stored install data and return the differences for an API data sync.
9558 *
9559 * @author Vova Feldman (@svovaf)
9560 * @since 2.0.0
9561 *
9562 * @param array $site
9563 * @param FS_Site $install
9564 * @param string[] string $override
9565 *
9566 * @return array
9567 */
9568 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9569 $diff = array();
9570 $special = array();
9571 $special_override = false;
9572
9573 foreach ( $site as $p => $v ) {
9574 if ( property_exists( $install, $p ) ) {
9575 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9576 $install->{$p} != $v
9577 ) {
9578 $val = self::get_api_sanitized_property( $p, $v );
9579
9580 if ( $install->{$p} != $val ) {
9581 $install->{$p} = $val;
9582 $diff[ $p ] = $val;
9583 }
9584 }
9585 } else {
9586 $special[ $p ] = $v;
9587
9588 if ( isset( $override[ $p ] ) ||
9589 'plugins' === $p ||
9590 'themes' === $p
9591 ) {
9592 $special_override = true;
9593 }
9594 }
9595 }
9596
9597 if ( $special_override || 0 < count( $diff ) ) {
9598 // Add special params only if has at least one
9599 // standard param, or if explicitly requested to
9600 // override a special param or a param which is not exist
9601 // in the install object.
9602 $diff = array_merge( $diff, $special );
9603 }
9604
9605 return $diff;
9606 }
9607
9608 /**
9609 * @author Leo Fajardo (@leorw)
9610 * @since 2.5.1
9611 */
9612 private function send_pending_clone_update_once() {
9613 $this->_logger->entrance();
9614
9615 if ( ! empty( $this->_storage->clone_id ) ) {
9616 return;
9617 }
9618
9619 $install_clone = $this->get_api_site_scope()->call(
9620 '/clones',
9621 'post',
9622 array( 'site_url' => self::get_unfiltered_site_url() )
9623 );
9624
9625 if ( $this->is_api_result_entity( $install_clone ) ) {
9626 $this->_storage->clone_id = $install_clone->id;
9627 }
9628 }
9629
9630 /**
9631 * @author Leo Fajardo (@leorw)
9632 * @since 2.5.1
9633 *
9634 * @param string $resolution_type
9635 * @param FS_Site $clone_context_install
9636 */
9637 function send_clone_resolution_update( $resolution_type, $clone_context_install ) {
9638 $this->_logger->entrance();
9639
9640 if ( empty( $this->_storage->clone_id ) ) {
9641 return;
9642 }
9643
9644 $new_install_id = null;
9645 $current_site = null;
9646
9647 $flush = false;
9648
9649 /**
9650 * 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.
9651 */
9652 if ( $clone_context_install->id != $this->_site->id ) {
9653 $new_install_id = $this->_site->id;
9654 $current_site = $this->_site;
9655 $this->_site = $clone_context_install;
9656
9657 $flush = true;
9658 }
9659
9660 $this->get_api_site_scope( $flush )->call(
9661 "/clones/{$this->_storage->clone_id}",
9662 'put',
9663 array(
9664 'resolution' => $resolution_type,
9665 'new_install_id' => $new_install_id,
9666 )
9667 );
9668
9669 if ( is_object( $current_site ) ) {
9670 /**
9671 * Ensure that the install scope entity is updated back to the previous install entity.
9672 */
9673 $this->_site = $current_site;
9674
9675 // Restore the previous install scope entity of the API.
9676 $this->get_api_site_scope( true );
9677 }
9678 }
9679
9680 /**
9681 * Update install only if changed.
9682 *
9683 * @author Vova Feldman (@svovaf)
9684 * @since 1.0.9
9685 *
9686 * @param string[] string $override
9687 * @param bool $flush
9688 * @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.
9689 *
9690 * @return false|object|string
9691 */
9692 private function send_install_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9693 $this->_logger->entrance();
9694
9695 $check_properties = $this->get_install_data_for_api( $override );
9696
9697 if ( $flush ) {
9698 $params = $check_properties;
9699 } else {
9700 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9701 }
9702
9703 if ( empty( $params ) ) {
9704 $keepalive_only_update = $this->should_send_keepalive_update();
9705
9706 if ( ! $keepalive_only_update ) {
9707 /**
9708 * There are no updates to send including keepalive.
9709 *
9710 * @author Leo Fajardo (@leorw)
9711 * @since 2.2.3
9712 */
9713 return false;
9714 }
9715 }
9716
9717 if ( $is_two_way_sync ) {
9718 /**
9719 * Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9720 *
9721 * @author Leo Fajardo (@leorw)
9722 * @since 2.2.3
9723 */
9724 if ( ! is_multisite() ) {
9725 // Update last install sync timestamp.
9726 $this->set_cron_execution_timestamp( 'install_sync' );
9727 }
9728
9729 $params['uid'] = $this->get_anonymous_id();
9730 }
9731
9732 $this->set_keepalive_timestamp();
9733
9734 // Send updated values to FS.
9735 $site = $this->api_site_call( '/', 'put', $params, true );
9736
9737 if ( $is_two_way_sync && $this->is_api_result_entity( $site ) ) {
9738 /**
9739 * Clear scheduled install sync after a two-way sync call.
9740 *
9741 * @author Leo Fajardo (@leorw)
9742 * @since 2.2.3
9743 */
9744 if ( ! is_multisite() ) {
9745 // I successfully sent install update, clear scheduled sync if exist.
9746 $this->clear_install_sync_cron();
9747 }
9748 }
9749
9750 return $site;
9751 }
9752
9753 /**
9754 * Update installs only if changed.
9755 *
9756 * @author Vova Feldman (@svovaf)
9757 * @since 2.0.0
9758 *
9759 * @param string[] string $override
9760 * @param bool $flush
9761 * @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.
9762 *
9763 * @return false|object|string
9764 */
9765 private function send_installs_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9766 $this->_logger->entrance();
9767
9768 /**
9769 * Pass `true` to use the network level storage since the update is for many installs.
9770 *
9771 * @author Leo Fajardo (@leorw)
9772 * @since 2.2.3
9773 */
9774 $should_send_keepalive = $this->should_send_keepalive_update( true );
9775
9776 $installs_data = $this->get_installs_data_for_api( $override, ! $flush, $should_send_keepalive );
9777
9778 if ( empty( $installs_data ) ) {
9779 return false;
9780 }
9781
9782 if ( $is_two_way_sync ) {
9783 // Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9784 $this->set_cron_execution_timestamp( 'install_sync' );
9785 }
9786
9787 /**
9788 * Pass `true` to use the network level storage since the update is for many installs.
9789 *
9790 * @author Leo Fajardo (@leorw)
9791 * @since 2.2.3
9792 */
9793 $this->set_keepalive_timestamp( true );
9794
9795 // Send updated values to FS.
9796 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9797
9798 if ( $is_two_way_sync && $this->is_api_result_object( $result, 'installs' ) ) {
9799 // I successfully sent a two-way installs update, clear the scheduled install sync if it exists.
9800 $this->clear_install_sync_cron();
9801 }
9802
9803 return $result;
9804 }
9805
9806 /**
9807 * @author Leo Fajardo (@leorw)
9808 *
9809 * @param bool|null $use_network_level_storage
9810 *
9811 * @return bool
9812 */
9813 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9814 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9815
9816 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9817 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9818 return true;
9819 } else {
9820 // 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.
9821 return ( 7 == rand( 1, 7 ) );
9822 }
9823 }
9824
9825 /**
9826 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9827 *
9828 * @author Leo Fajardo (@leorw)
9829 * @since 2.3.2
9830 */
9831 private function maybe_sync_install_user() {
9832 if ( $this->_user->id == $this->_site->user_id ) {
9833 return;
9834 }
9835
9836 // Fetch user data and store if found.
9837 $this->sync_user_by_current_install();
9838 }
9839
9840 /**
9841 * Update install only if changed.
9842 *
9843 * @author Vova Feldman (@svovaf)
9844 * @since 1.0.9
9845 *
9846 * @param string[] string $override
9847 * @param bool $flush
9848 */
9849 function sync_install( $override = array(), $flush = false ) {
9850 $this->_logger->entrance();
9851
9852 $site = $this->send_install_update( $override, $flush, true );
9853
9854 if ( false === $site ) {
9855 // No sync required.
9856 return;
9857 }
9858
9859 if ( ! $this->is_api_result_entity( $site ) ) {
9860 // Failed to sync, don't update locally.
9861 return;
9862 }
9863
9864 $this->_site = new FS_Site( $site );
9865
9866 $this->_store_site( true );
9867 }
9868
9869 /**
9870 * Update install only if changed.
9871 *
9872 * @author Vova Feldman (@svovaf)
9873 * @since 1.0.9
9874 *
9875 * @param string[] string $override
9876 * @param bool $flush
9877 */
9878 private function sync_installs( $override = array(), $flush = false ) {
9879 $this->_logger->entrance();
9880
9881 $result = $this->send_installs_update( $override, $flush, true );
9882
9883 if ( false === $result ) {
9884 // No sync required.
9885 return;
9886 }
9887
9888 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9889 // Failed to sync, don't update locally.
9890 return;
9891 }
9892
9893 $address_to_blog_map = $this->get_address_to_blog_map();
9894
9895 foreach ( $result->installs as $install ) {
9896 $this->_site = new FS_Site( $install );
9897
9898 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9899 $blog_id = $address_to_blog_map[ $address ];
9900
9901 $this->_store_site( true, $blog_id );
9902 }
9903 }
9904
9905 /**
9906 * Track install's custom event.
9907 *
9908 * IMPORTANT:
9909 * Custom event tracking is currently only supported for specific clients.
9910 * If you are not one of them, please don't use this method. If you will,
9911 * the API will simply ignore your request based on the plugin ID.
9912 *
9913 * Need custom tracking for your plugin or theme?
9914 * If you are interested in custom event tracking please contact yo@freemius.com
9915 * for further details.
9916 *
9917 * @author Vova Feldman (@svovaf)
9918 * @since 1.2.1
9919 *
9920 * @param string $name Event name.
9921 * @param array $properties Associative key/value array with primitive values only
9922 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9923 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9924 *
9925 * @return object|false Event data or FALSE on failure.
9926 *
9927 * @throws \Freemius_InvalidArgumentException
9928 */
9929 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9930 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9931
9932 if ( ! $this->is_registered() ) {
9933 return false;
9934 }
9935
9936 $event = array( 'type' => $name );
9937
9938 if ( is_numeric( $process_at ) && $process_at > time() ) {
9939 $event['process_at'] = $process_at;
9940 }
9941
9942 if ( $once ) {
9943 $event['once'] = true;
9944 }
9945
9946 if ( ! empty( $properties ) ) {
9947 // Verify associative array values are primitive.
9948 foreach ( $properties as $k => $v ) {
9949 if ( ! is_scalar( $v ) ) {
9950 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9951 }
9952 }
9953
9954 $event['properties'] = $properties;
9955 }
9956
9957 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9958
9959 return $this->is_api_error( $result ) ?
9960 false :
9961 $result;
9962 }
9963
9964 /**
9965 * Track install's custom event only once, but it still triggers the API call.
9966 *
9967 * IMPORTANT:
9968 * Custom event tracking is currently only supported for specific clients.
9969 * If you are not one of them, please don't use this method. If you will,
9970 * the API will simply ignore your request based on the plugin ID.
9971 *
9972 * Need custom tracking for your plugin or theme?
9973 * If you are interested in custom event tracking please contact yo@freemius.com
9974 * for further details.
9975 *
9976 * @author Vova Feldman (@svovaf)
9977 * @since 1.2.1
9978 *
9979 * @param string $name Event name.
9980 * @param array $properties Associative key/value array with primitive values only
9981 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9982 *
9983 * @return object|false Event data or FALSE on failure.
9984 *
9985 * @throws \Freemius_InvalidArgumentException
9986 *
9987 * @user Freemius::track_event()
9988 */
9989 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9990 return $this->track_event( $name, $properties, $process_at, true );
9991 }
9992
9993 /**
9994 * Plugin uninstall hook.
9995 *
9996 * @author Vova Feldman (@svovaf)
9997 * @since 1.0.1
9998 *
9999 * @param bool $check_user Enforce checking if user have plugins activation privileges.
10000 */
10001 function _uninstall_plugin_event( $check_user = true ) {
10002 $this->_logger->entrance( 'slug = ' . $this->_slug );
10003
10004 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
10005 return;
10006 }
10007
10008 $params = array();
10009 $uninstall_reason = null;
10010 if ( isset( $this->_storage->uninstall_reason ) ) {
10011 $uninstall_reason = $this->_storage->uninstall_reason;
10012 $params['reason_id'] = $uninstall_reason->id;
10013 $params['reason_info'] = $uninstall_reason->info;
10014 }
10015
10016 if ( ! $this->is_registered() ) {
10017 // Send anonymous uninstall event only if user submitted a feedback.
10018 if ( isset( $uninstall_reason ) ) {
10019 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
10020 $this->opt_in( false, false, false, false, true );
10021 } else {
10022 $params['uid'] = $this->get_anonymous_id();
10023 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
10024 }
10025 }
10026 } else {
10027 $params = array_merge( $params, array(
10028 'is_active' => false,
10029 'is_uninstalled' => true,
10030 ) );
10031
10032 if ( $this->_is_network_active ) {
10033 // Send uninstall event.
10034 $this->send_installs_update( $params );
10035 } else {
10036 // Send uninstall event and handle the result.
10037 $this->sync_install( $params );
10038 }
10039 }
10040
10041 // @todo Decide if we want to delete plugin information from db.
10042 }
10043
10044 /**
10045 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
10046 *
10047 * @author Vova Feldman (@svovaf)
10048 * @since 2.2.1
10049 *
10050 * @param string $is_premium
10051 * @param string $caller
10052 *
10053 * @return void
10054 */
10055 function set_basename( $is_premium, $caller ) {
10056 $basename = plugin_basename( $caller );
10057
10058 $current_basename = $is_premium ?
10059 $this->_premium_plugin_basename :
10060 $this->_free_plugin_basename;
10061
10062 if ( $current_basename == $basename ) {
10063 // Basename value set correctly.
10064 return;
10065 }
10066
10067 if ( $is_premium ) {
10068 $this->_premium_plugin_basename = $basename;
10069 } else {
10070 $this->_free_plugin_basename = $basename;
10071 }
10072
10073 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
10074
10075 register_activation_hook(
10076 $plugin_dir . $basename,
10077 array( &$this, '_activate_plugin_event_hook' )
10078 );
10079 }
10080
10081 /**
10082 * @author Vova Feldman (@svovaf)
10083 * @since 1.1.1
10084 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
10085 *
10086 * @return string
10087 */
10088 function premium_plugin_basename() {
10089 if ( ! isset( $this->_premium_plugin_basename ) ) {
10090 $this->_premium_plugin_basename = $this->is_premium() ?
10091 // The product is premium, so use the current basename.
10092 $this->_plugin_basename :
10093 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
10094 }
10095
10096 return $this->_premium_plugin_basename;
10097 }
10098
10099 /**
10100 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
10101 *
10102 * @author Vova Feldman (@svovaf)
10103 * @since 1.0.2
10104 */
10105 public static function _uninstall_plugin_hook() {
10106 self::_load_required_static();
10107
10108 self::$_static_logger->entrance();
10109
10110 if ( ! current_user_can( 'activate_plugins' ) ) {
10111 return;
10112 }
10113
10114 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
10115
10116 self::$_static_logger->info( 'plugin = ' . $plugin_file );
10117
10118 define( 'WP_FS__UNINSTALL_MODE', true );
10119
10120 $fs = self::get_instance_by_file( $plugin_file );
10121
10122 if ( is_object( $fs ) ) {
10123 $fs->remove_sdk_reference();
10124
10125 self::require_plugin_essentials();
10126
10127 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
10128 is_plugin_active( $fs->premium_plugin_basename() )
10129 ) {
10130 // Deleting Free or Premium plugin version while the other version still installed.
10131 return;
10132 }
10133
10134 if (
10135 ! $fs->is_clone() &&
10136 /**
10137 * 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).
10138 *
10139 * @author Leo Fajardo
10140 */
10141 ( ! is_object( $fs->_site ) || $fs->is_registered() )
10142 ) {
10143 $fs->_uninstall_plugin_event();
10144 }
10145
10146 $fs->do_action( 'after_uninstall' );
10147 }
10148 }
10149
10150 #----------------------------------------------------------------------------------
10151 #region Plugin Information
10152 #----------------------------------------------------------------------------------
10153
10154 /**
10155 * Load WordPress core plugin.php essential module.
10156 *
10157 * @author Vova Feldman (@svovaf)
10158 * @since 1.1.1
10159 */
10160 private static function require_plugin_essentials() {
10161 if ( ! function_exists( 'get_plugins' ) ) {
10162 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
10163
10164 require_once ABSPATH . 'wp-admin/includes/plugin.php';
10165 }
10166 }
10167
10168 /**
10169 * Load WordPress core pluggable.php module.
10170 *
10171 * @author Vova Feldman (@svovaf)
10172 * @since 1.1.2
10173 */
10174 private static function require_pluggable_essentials() {
10175 if ( ! function_exists( 'wp_get_current_user' ) ) {
10176 require_once ABSPATH . 'wp-includes/pluggable.php';
10177 }
10178 }
10179
10180 /**
10181 * Return plugin data.
10182 *
10183 * @author Vova Feldman (@svovaf)
10184 * @since 1.0.1
10185 *
10186 * @param bool $reparse_plugin_metadata
10187 *
10188 * @return array
10189 */
10190 function get_plugin_data( $reparse_plugin_metadata = false ) {
10191 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
10192 self::require_plugin_essentials();
10193
10194 if ( $this->is_plugin() ) {
10195 /**
10196 * @author Vova Feldman (@svovaf)
10197 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
10198 *
10199 * @link https://github.com/Freemius/wordpress-sdk/issues/77
10200 */
10201 $plugin_data = get_plugin_data(
10202 $this->_plugin_main_file_path,
10203 false,
10204 false
10205 );
10206 } else {
10207 $theme_data = wp_get_theme();
10208
10209 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
10210 $parent_theme = $theme_data->parent();
10211
10212 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
10213 $theme_data = $parent_theme;
10214 }
10215 }
10216
10217 $plugin_data = array(
10218 'Name' => $theme_data->get( 'Name' ),
10219 'Version' => $theme_data->get( 'Version' ),
10220 'Author' => $theme_data->get( 'Author' ),
10221 'Description' => $theme_data->get( 'Description' ),
10222 'PluginURI' => $theme_data->get( 'ThemeURI' ),
10223 );
10224 }
10225
10226 $this->_plugin_data = $plugin_data;
10227 }
10228
10229 return $this->_plugin_data;
10230 }
10231
10232 /**
10233 * @author Vova Feldman (@svovaf)
10234 * @since 1.0.1
10235 * @since 1.2.2.5 If slug not set load slug by module ID.
10236 *
10237 * @return string Plugin slug.
10238 */
10239 function get_slug() {
10240 if ( ! isset( $this->_slug ) ) {
10241 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
10242 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
10243 }
10244
10245 return $this->_slug;
10246 }
10247
10248 /**
10249 * @author Leo Fajardo (@leorw)
10250 * @since 2.2.1
10251 *
10252 * @return string
10253 */
10254 function get_premium_slug() {
10255 return ( is_object( $this->_plugin ) && ! empty( $this->_plugin->premium_slug ) ) ?
10256 $this->_plugin->premium_slug :
10257 "{$this->_slug}-premium";
10258 }
10259
10260 /**
10261 * Retrieve the desired folder name for the product.
10262 *
10263 * @author Vova Feldman (@svovaf)
10264 * @since 1.2.1.7
10265 *
10266 * @return string Plugin slug.
10267 */
10268 function get_target_folder_name() {
10269 return $this->can_use_premium_code() ?
10270 $this->_plugin->premium_slug :
10271 $this->_slug;
10272 }
10273
10274 /**
10275 * @author Vova Feldman (@svovaf)
10276 * @since 1.0.1
10277 *
10278 * @return number Plugin ID.
10279 */
10280 function get_id() {
10281 return $this->_plugin->id;
10282 }
10283
10284 /**
10285 * @author Leo Fajardo (@leorw)
10286 * @since 2.2.4
10287 *
10288 * @return number|null Bundle ID.
10289 */
10290 function get_bundle_id() {
10291 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
10292 $this->_plugin->bundle_id :
10293 null;
10294 }
10295
10296 /**
10297 * @author Vova Feldman (@svovaf)
10298 * @since 2.3.1
10299 *
10300 * @return string|null Bundle public key.
10301 */
10302 function get_bundle_public_key() {
10303 return isset( $this->_plugin->bundle_public_key ) ?
10304 $this->_plugin->bundle_public_key :
10305 null;
10306 }
10307
10308 /**
10309 * Get whether the SDK has been initiated in the context of a Bundle.
10310 *
10311 * This will return true, if `bundle_id` is present in the SDK init parameters.
10312 *
10313 * ```php
10314 * $my_fs = fs_dynamic_init( array(
10315 * // ...
10316 * 'bundle_id' => 'XXXX', // Will return true since we have bundle id.
10317 * 'bundle_public_key' => 'pk_XXXX',
10318 * ) );
10319 * ```
10320 *
10321 * @author Swashata Ghosh (@swashata)
10322 * @since 2.5.0
10323 *
10324 * @return bool True if we are running in bundle context, false otherwise.
10325 */
10326 private function has_bundle_context() {
10327 return ! is_null( $this->get_bundle_id() );
10328 }
10329
10330 /**
10331 * @author Vova Feldman (@svovaf)
10332 * @since 1.2.1.5
10333 *
10334 * @return string Freemius SDK version
10335 */
10336 function get_sdk_version() {
10337 return $this->version;
10338 }
10339
10340 /**
10341 * @author Vova Feldman (@svovaf)
10342 * @since 1.2.1.5
10343 *
10344 * @return number Parent plugin ID (if parent exist).
10345 */
10346 function get_parent_id() {
10347 return $this->is_addon() ?
10348 $this->get_parent_instance()->get_id() :
10349 $this->_plugin->id;
10350 }
10351
10352 /**
10353 * @author Vova Feldman (@svovaf)
10354 * @since 2.3.1
10355 *
10356 * @return string
10357 */
10358 function get_usage_tracking_terms_url() {
10359 return $this->apply_filters(
10360 'usage_tracking_terms_url',
10361 "https://freemius.com/product/opt-in/{$this->_plugin->id}/{$this->_slug}/"
10362 );
10363 }
10364
10365 /**
10366 * @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.
10367 * @since 2.5.8
10368 *
10369 * @return string
10370 */
10371 function get_license_activation_terms_url() {
10372 return $this->apply_filters(
10373 'license_activation_terms_url',
10374 "https://freemius.com/product/license-activation/{$this->_plugin->id}/{$this->_slug}/"
10375 );
10376 }
10377
10378 /**
10379 * @author Vova Feldman (@svovaf)
10380 * @since 2.3.1
10381 *
10382 * @return string
10383 */
10384 function get_eula_url() {
10385 return $this->apply_filters(
10386 'eula_url',
10387 "https://freemius.com/product/{$this->_plugin->id}/{$this->_slug}/legal/eula/"
10388 );
10389 }
10390
10391 /**
10392 * @author Vova Feldman (@svovaf)
10393 * @since 1.0.1
10394 *
10395 * @return string Plugin public key.
10396 */
10397 function get_public_key() {
10398 return $this->_plugin->public_key;
10399 }
10400
10401 /**
10402 * Will be available only on sandbox mode.
10403 *
10404 * @author Vova Feldman (@svovaf)
10405 * @since 1.0.4
10406 *
10407 * @return mixed Plugin secret key.
10408 */
10409 function get_secret_key() {
10410 return $this->_plugin->secret_key;
10411 }
10412
10413 /**
10414 * @author Vova Feldman (@svovaf)
10415 * @since 1.1.1
10416 *
10417 * @return bool
10418 */
10419 function has_secret_key() {
10420 return ! empty( $this->_plugin->secret_key );
10421 }
10422
10423 /**
10424 * @author Vova Feldman (@svovaf)
10425 * @since 1.0.9
10426 *
10427 * @param string|bool $premium_suffix
10428 *
10429 * @return string
10430 */
10431 function get_plugin_name( $premium_suffix = false ) {
10432 $this->_logger->entrance();
10433
10434 /**
10435 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10436 *
10437 * @author Vova Feldman
10438 */
10439 if ( ! isset( $this->_plugin_name ) ) {
10440 // Name is not yet set.
10441 $this->set_name( $premium_suffix );
10442 } else if (
10443 ! empty( $premium_suffix ) &&
10444 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10445 ) {
10446 // Name is already set, but there's a change in the premium suffix.
10447 $this->set_name( $premium_suffix );
10448 }
10449
10450 return $this->_plugin_name;
10451 }
10452
10453 /**
10454 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10455 *
10456 * @author Vova Feldman (@svovaf)
10457 * @since 2.2.1
10458 *
10459 * @param string $premium_suffix
10460 */
10461 private function set_name( $premium_suffix = '' ) {
10462 $plugin_data = $this->get_plugin_data();
10463
10464 // Get name.
10465 $this->_plugin_name = $plugin_data['Name'];
10466
10467 if ( is_string( $premium_suffix ) ) {
10468 $premium_suffix = trim( $premium_suffix );
10469
10470 if ( ! empty( $premium_suffix ) ) {
10471 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10472 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10473 $suffix_len = strlen( $suffix );
10474
10475 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10476 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10477 ) {
10478 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10479 }
10480 }
10481 }
10482
10483 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10484 }
10485
10486 /**
10487 * @author Vova Feldman (@svovaf)
10488 * @since 1.0.0
10489 *
10490 * @param bool $reparse_plugin_metadata
10491 *
10492 * @return string
10493 */
10494 function get_plugin_version( $reparse_plugin_metadata = false ) {
10495 $this->_logger->entrance();
10496
10497 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10498
10499 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10500
10501 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10502 }
10503
10504 /**
10505 * @author Vova Feldman (@svovaf)
10506 * @since 1.2.1.7
10507 *
10508 * @return string
10509 */
10510 function get_plugin_title() {
10511 $this->_logger->entrance();
10512
10513 $title = $this->_plugin->title;
10514
10515 return $this->apply_filters( 'plugin_title', $title );
10516 }
10517
10518 /**
10519 * @author Vova Feldman (@svovaf)
10520 * @since 1.2.2.7
10521 *
10522 * @param bool $lowercase
10523 *
10524 * @return string
10525 */
10526 function get_module_label( $lowercase = false ) {
10527 $label = $this->is_addon() ?
10528 $this->get_text_inline( 'Add-On', 'addon' ) :
10529 ( $this->is_plugin() ?
10530 $this->get_text_inline( 'Plugin', 'plugin' ) :
10531 $this->get_text_inline( 'Theme', 'theme' ) );
10532
10533 if ( $lowercase ) {
10534 $label = strtolower( $label );
10535 }
10536
10537 return $label;
10538 }
10539
10540 /**
10541 * @author Vova Feldman (@svovaf)
10542 * @since 1.0.4
10543 *
10544 * @return string
10545 */
10546 function get_plugin_basename() {
10547 if ( ! isset( $this->_plugin_basename ) ) {
10548 if ( $this->is_plugin() ) {
10549 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10550 } else {
10551 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10552 }
10553 }
10554
10555 return $this->_plugin_basename;
10556 }
10557
10558 function get_plugin_folder_name() {
10559 $this->_logger->entrance();
10560
10561 $plugin_folder = $this->_plugin_basename;
10562
10563 while ( '.' !== dirname( $plugin_folder ) ) {
10564 $plugin_folder = dirname( $plugin_folder );
10565 }
10566
10567 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10568
10569 return $plugin_folder;
10570 }
10571
10572 #endregion ------------------------------------------------------------------
10573
10574 /* Account
10575 ------------------------------------------------------------------------------------------------------------------*/
10576
10577 /**
10578 * Find plugin's slug by plugin's basename.
10579 *
10580 * @author Vova Feldman (@svovaf)
10581 * @since 1.0.9
10582 *
10583 * @param string $plugin_base_name
10584 *
10585 * @return false|string
10586 */
10587 private static function find_slug_by_basename( $plugin_base_name ) {
10588 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10589
10590 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10591 return false;
10592 }
10593
10594 return $file_slug_map[ $plugin_base_name ];
10595 }
10596
10597 /**
10598 * Store the map between the plugin's basename to the slug.
10599 *
10600 * @author Vova Feldman (@svovaf)
10601 * @since 1.0.9
10602 */
10603 private function store_file_slug_map() {
10604 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10605
10606 if ( ! array( $file_slug_map ) ) {
10607 $file_slug_map = array();
10608 }
10609
10610 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10611 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10612 ) {
10613 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10614 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10615 }
10616 }
10617
10618 /**
10619 * @return array[number]FS_User
10620 */
10621 static function get_all_users() {
10622 $users = self::maybe_get_entities_account_option( 'users', array() );
10623
10624 if ( ! is_array( $users ) ) {
10625 $users = array();
10626 }
10627
10628 return $users;
10629 }
10630
10631 /**
10632 * @param string $module_type
10633 * @param null|int $blog_id Since 2.0.0
10634 *
10635 * @return array[string]FS_Site
10636 */
10637 private static function get_all_sites(
10638 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10639 $blog_id = null,
10640 $is_backup = false
10641 ) {
10642 $sites = self::get_account_option(
10643 ( $is_backup ? 'prev_' : '' ) . 'sites',
10644 $module_type,
10645 $blog_id
10646 );
10647
10648 if ( ! is_array( $sites ) ) {
10649 $sites = array();
10650 }
10651
10652 return $sites;
10653 }
10654
10655 /**
10656 * @author Leo Fajardo (@leorw)
10657 *
10658 * @since 1.2.2
10659 *
10660 * @param string $option_name
10661 * @param string $module_type
10662 * @param null|int $network_level_or_blog_id Since 2.0.0
10663 *
10664 * @return mixed
10665 */
10666 private static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10667 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10668 $option_name = $module_type . '_' . $option_name;
10669 }
10670
10671 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10672 }
10673
10674 /**
10675 * @author Leo Fajardo (@leorw)
10676 *
10677 * @since 1.2.2
10678 *
10679 * @param string $option_name
10680 * @param mixed $option_value
10681 * @param bool $store
10682 * @param null|int $network_level_or_blog_id Since 2.0.0
10683 */
10684 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10685 self::set_account_option_by_module(
10686 $this->_module_type,
10687 $option_name,
10688 $option_value,
10689 $store,
10690 $network_level_or_blog_id
10691 );
10692 }
10693
10694 /**
10695 * @author Vova Feldman (@svovaf)
10696 *
10697 * @since 1.2.2.7
10698 *
10699 * @param string $module_type
10700 * @param string $option_name
10701 * @param mixed $option_value
10702 * @param bool $store
10703 * @param null|int $network_level_or_blog_id Since 2.0.0
10704 */
10705 private static function set_account_option_by_module(
10706 $module_type,
10707 $option_name,
10708 $option_value,
10709 $store,
10710 $network_level_or_blog_id = null
10711 ) {
10712 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10713 $option_name = $module_type . '_' . $option_name;
10714 }
10715
10716 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10717 }
10718
10719 /**
10720 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10721 *
10722 * @author Leo Fajardo (@leorw)
10723 * @since 2.3.1
10724 *
10725 * @param string $option_name
10726 * @param mixed $default
10727 * @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).
10728 *
10729 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10730 */
10731 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10732 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10733
10734 $class_name = '';
10735
10736 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10737 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10738 }
10739
10740 switch ( $option_name ) {
10741 case 'plugins':
10742 case 'themes':
10743 case 'addons':
10744 $class_name = FS_Plugin::get_class_name();
10745 break;
10746 case 'users':
10747 $class_name = FS_User::get_class_name();
10748 break;
10749 case 'sites':
10750 $class_name = FS_Site::get_class_name();
10751 break;
10752 case 'licenses':
10753 case 'all_licenses':
10754 $class_name = FS_Plugin_License::get_class_name();
10755 break;
10756 case 'plans':
10757 $class_name = FS_Plugin_Plan::get_class_name();
10758 break;
10759 case 'updates':
10760 $class_name = FS_Plugin_Tag::get_class_name();
10761 break;
10762 }
10763
10764 if ( empty( $class_name ) ) {
10765 return $option;
10766 }
10767
10768 return fs_get_entities( $option, $class_name );
10769 }
10770
10771 /**
10772 * @author Vova Feldman (@svovaf)
10773 * @since 1.0.6
10774 *
10775 * @param number|null $module_id
10776 *
10777 * @return FS_Plugin_License[]
10778 */
10779 private static function get_all_licenses( $module_id = null ) {
10780 $licenses = self::get_account_option( 'all_licenses' );
10781
10782 if ( ! is_array( $licenses ) ) {
10783 $licenses = array();
10784 }
10785
10786 if ( is_null( $module_id ) ) {
10787 return $licenses;
10788 }
10789
10790 $licenses = isset( $licenses[ $module_id ] ) ?
10791 $licenses[ $module_id ] :
10792 array();
10793
10794 return $licenses;
10795 }
10796
10797 /**
10798 * @author Leo Fajardo (@leorw)
10799 * @since 2.0.0
10800 *
10801 * @return array
10802 */
10803 private static function get_all_licenses_by_module_type() {
10804 $licenses = self::get_account_option( 'all_licenses' );
10805
10806 $licenses_by_module_type = array(
10807 WP_FS__MODULE_TYPE_PLUGIN => array(),
10808 WP_FS__MODULE_TYPE_THEME => array()
10809 );
10810
10811 if ( ! is_array( $licenses ) ) {
10812 return $licenses_by_module_type;
10813 }
10814
10815 foreach ( $licenses as $module_id => $module_licenses ) {
10816 $fs = self::get_instance_by_id( $module_id );
10817 if ( false === $fs ) {
10818 continue;
10819 }
10820
10821 $licenses_by_module_type[ $fs->_module_type ] = array_merge( $licenses_by_module_type[ $fs->_module_type ], $module_licenses );
10822 }
10823
10824 return $licenses_by_module_type;
10825 }
10826
10827 /**
10828 * @author Leo Fajardo (@leorw)
10829 * @since 2.0.0
10830 *
10831 * @param number $module_id
10832 * @param number|null $user_id
10833 *
10834 * @return array
10835 */
10836 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10837 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10838
10839 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10840 $all_modules_user_id_license_ids_map = array();
10841 }
10842
10843 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10844 $all_modules_user_id_license_ids_map[ $module_id ] :
10845 array();
10846
10847 if ( FS_User::is_valid_id( $user_id ) ) {
10848 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10849 $user_id_license_ids_map[ $user_id ] :
10850 array();
10851 }
10852
10853 return $user_id_license_ids_map;
10854 }
10855
10856 /**
10857 * @author Leo Fajardo (@leorw)
10858 * @since 2.0.0
10859 *
10860 * @param array $new_user_id_license_ids_map
10861 * @param number $module_id
10862 * @param number|null $user_id
10863 */
10864 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10865 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10866 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10867 $all_modules_user_id_license_ids_map = array();
10868 }
10869
10870 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10871 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10872 }
10873
10874 if ( FS_User::is_valid_id( $user_id ) ) {
10875 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10876 } else {
10877 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10878 }
10879
10880 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10881 }
10882
10883 /**
10884 * Get a collection of the user's linked license IDs.
10885 *
10886 * @author Vova Feldman (@svovaf)
10887 * @since 2.0.0
10888 *
10889 * @param number $user_id
10890 *
10891 * @return number[]
10892 */
10893 private function get_user_linked_license_ids( $user_id ) {
10894 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10895 }
10896
10897 /**
10898 * Override the user's linked license IDs with a new IDs collection.
10899 *
10900 * @author Vova Feldman (@svovaf)
10901 * @since 2.0.0
10902 *
10903 * @param number $user_id
10904 * @param number[] $license_ids
10905 */
10906 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10907 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10908 }
10909
10910 /**
10911 * Link a specified license ID to a given user.
10912 *
10913 * @author Vova Feldman (@svovaf)
10914 * @since 2.0.0
10915 *
10916 * @param number $license_id
10917 * @param number $user_id
10918 */
10919 private function link_license_2_user( $license_id, $user_id ) {
10920 $license_ids = $this->get_user_linked_license_ids( $user_id );
10921
10922 if ( in_array( $license_id, $license_ids ) ) {
10923 // License already linked.
10924 return;
10925 }
10926
10927 $license_ids[] = $license_id;
10928
10929 $this->set_user_linked_license_ids( $user_id, $license_ids );
10930 }
10931
10932 /**
10933 * @param string|bool $module_type
10934 *
10935 * @return FS_Plugin_Plan[]
10936 */
10937 private static function get_all_plans( $module_type = false ) {
10938 $plans = self::get_account_option( 'plans', $module_type );
10939
10940 if ( ! is_array( $plans ) ) {
10941 $plans = array();
10942 }
10943
10944 return $plans;
10945 }
10946
10947 /**
10948 * @author Vova Feldman (@svovaf)
10949 * @since 1.0.4
10950 *
10951 * @return FS_Plugin_Tag[]
10952 */
10953 private static function get_all_updates() {
10954 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10955
10956 if ( ! is_array( $updates ) ) {
10957 $updates = array();
10958 }
10959
10960 return $updates;
10961 }
10962
10963 /**
10964 * @author Vova Feldman (@svovaf)
10965 * @since 1.0.6
10966 *
10967 * @return array<number,FS_Plugin[]>|false
10968 */
10969 private static function get_all_addons() {
10970 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10971
10972 if ( ! is_array( $addons ) ) {
10973 $addons = array();
10974 }
10975
10976 return $addons;
10977 }
10978
10979 /**
10980 * @author Vova Feldman (@svovaf)
10981 * @since 1.0.6
10982 *
10983 * @return number[]|false
10984 */
10985 private static function get_all_account_addons() {
10986 $addons = self::$_accounts->get_option( 'account_addons', array() );
10987
10988 if ( ! is_array( $addons ) ) {
10989 $addons = array();
10990 }
10991
10992 return $addons;
10993 }
10994
10995 /**
10996 * Check if user has connected his account (opted-in).
10997 *
10998 * Note:
10999 * If the user opted-in and opted-out on a later stage,
11000 * this will still return true. If you want to check if the
11001 * user is currently opted-in, use:
11002 * `$fs->is_registered() && $fs->is_tracking_allowed()`
11003 *
11004 * @author Vova Feldman (@svovaf)
11005 * @since 1.0.1
11006 *
11007 * @param bool $ignore_anonymous_state Since 2.5.1
11008 *
11009 * @return bool
11010 */
11011 function is_registered( $ignore_anonymous_state = false ) {
11012 return (
11013 is_object( $this->_user ) &&
11014 (
11015 $this->is_premium() ||
11016 $ignore_anonymous_state ||
11017 ! $this->is_anonymous()
11018 )
11019 );
11020 }
11021
11022 /**
11023 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
11024 *
11025 * @author Leo Fajardo (@leorw)
11026 * @since 1.2.1.5
11027 *
11028 * @return bool
11029 */
11030 function is_tracking_allowed( $blog_id = null, $install = null ) {
11031 if ( is_null( $install ) ) {
11032 $install = is_null( $blog_id ) ?
11033 $this->_site :
11034 $this->get_install_by_blog_id( $blog_id );
11035 }
11036
11037 return (
11038 is_object( $install ) &&
11039 FS_Permission_Manager::instance( $this )->is_homepage_url_tracking_allowed( $blog_id )
11040 );
11041 }
11042
11043 /**
11044 * Returns TRUE if the user never opted-in or manually opted-out.
11045 *
11046 * @author Vova Feldman (@svovaf)
11047 * @since 1.2.1.5
11048 *
11049 * @param int|null $blog_id
11050 *
11051 * @return bool
11052 */
11053 function is_tracking_prohibited( $blog_id = null ) {
11054 return (
11055 ! $this->is_registered( true ) ||
11056 ! $this->is_tracking_allowed( $blog_id )
11057 );
11058 }
11059
11060 /**
11061 * @author Leo Fajardo (@leorw)
11062 * @since 2.4.0
11063 *
11064 * @return bool
11065 */
11066 function is_bundle_license_auto_activation_enabled() {
11067 return $this->is_addon() ?
11068 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
11069 $this->_is_bundle_license_auto_activation_enabled;
11070 }
11071
11072 /**
11073 * @author Vova Feldman (@svovaf)
11074 * @since 1.0.4
11075 *
11076 * @return FS_Plugin
11077 */
11078 function get_plugin() {
11079 return $this->_plugin;
11080 }
11081
11082 /**
11083 * @author Vova Feldman (@svovaf)
11084 * @since 1.0.3
11085 *
11086 * @return FS_User
11087 */
11088 function get_user() {
11089 return $this->_user;
11090 }
11091
11092 /**
11093 * @author Vova Feldman (@svovaf)
11094 * @since 1.0.3
11095 *
11096 * @return FS_Site
11097 */
11098 function get_site() {
11099 return $this->_site;
11100 }
11101
11102 /**
11103 * @author Leo Fajardo (@leorw)
11104 * @since 2.5.0
11105 */
11106 function store_site( $site ) {
11107 $this->_site = $site;
11108 $this->_store_site( true );
11109 }
11110
11111 /**
11112 * 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).
11113 *
11114 * @author Leo Fajardo (@leorw)
11115 * @since 2.5.0
11116 */
11117 function delete_current_install( $back_up ) {
11118 // Back up and delete the unique ID.
11119 if ( $back_up ) {
11120 self::$_accounts->set_option( 'prev_unique_id', $this->get_anonymous_id() );
11121 }
11122
11123 self::$_accounts->set_option( 'unique_id', null );
11124
11125 if ( $back_up ) {
11126 // 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).
11127 $this->back_up_site();
11128 }
11129
11130 $this->_delete_site();
11131 $this->_site = null;
11132 }
11133
11134 /**
11135 * @author Leo Fajardo (@leorw)
11136 * @since 2.5.0
11137 */
11138 function restore_backup_site() {
11139 self::$_accounts->set_option(
11140 'unique_id',
11141 self::$_accounts->get_option( 'prev_unique_id' )
11142 );
11143
11144 $sites = self::get_all_sites( $this->_module_type, null, true );
11145 $this->store_site( clone $sites[ $this->_slug ] );
11146 }
11147
11148 /**
11149 * Get plugin add-ons.
11150 *
11151 * @author Vova Feldman (@svovaf)
11152 * @since 1.0.6
11153 *
11154 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
11155 *
11156 * @param bool $flush
11157 *
11158 * @return FS_Plugin[]|false
11159 */
11160 function get_addons( $flush = false ) {
11161 $this->_logger->entrance();
11162
11163 if ( ! $this->_has_addons ) {
11164 return false;
11165 }
11166
11167 $addons = $this->sync_addons( $flush );
11168
11169 return ( ! is_array( $addons ) || empty( $addons ) ) ?
11170 false :
11171 $addons;
11172 }
11173
11174 /**
11175 * @author Vova Feldman (@svovaf)
11176 * @since 1.0.6
11177 *
11178 * @return number[]|false
11179 */
11180 function get_account_addons() {
11181 $this->_logger->entrance();
11182
11183 $addons = self::get_all_account_addons();
11184
11185 if ( ! is_array( $addons ) ||
11186 ! isset( $addons[ $this->_plugin->id ] ) ||
11187 ! is_array( $addons[ $this->_plugin->id ] ) ||
11188 0 === count( $addons[ $this->_plugin->id ] )
11189 ) {
11190 return false;
11191 }
11192
11193 return $addons[ $this->_plugin->id ];
11194 }
11195
11196 /**
11197 * Check if user has any
11198 *
11199 * @author Vova Feldman (@svovaf)
11200 * @since 1.1.6
11201 *
11202 * @return bool
11203 */
11204 function has_account_addons() {
11205 $addons = $this->get_account_addons();
11206
11207 return is_array( $addons ) && ( 0 < count( $addons ) );
11208 }
11209
11210
11211 /**
11212 * Get add-on by ID (from local data).
11213 *
11214 * @author Vova Feldman (@svovaf)
11215 * @since 1.0.6
11216 *
11217 * @param number $id
11218 *
11219 * @return FS_Plugin|false
11220 */
11221 function get_addon( $id ) {
11222 $this->_logger->entrance();
11223
11224 $addons = $this->get_addons();
11225
11226 if ( is_array( $addons ) ) {
11227 foreach ( $addons as $addon ) {
11228 if ( $id == $addon->id ) {
11229 return $addon;
11230 }
11231 }
11232 }
11233
11234 return false;
11235 }
11236
11237 /**
11238 * Get add-on by slug (from local data).
11239 *
11240 * @author Vova Feldman (@svovaf)
11241 * @since 1.0.6
11242 *
11243 * @param string $slug
11244 *
11245 * @param bool $flush
11246 *
11247 * @return FS_Plugin|false
11248 */
11249 function get_addon_by_slug( $slug, $flush = false ) {
11250 $this->_logger->entrance();
11251
11252 $addons = $this->get_addons( $flush );
11253
11254 if ( is_array( $addons ) ) {
11255 foreach ( $addons as $addon ) {
11256 if ( $slug === $addon->slug ) {
11257 return $addon;
11258 }
11259 }
11260 }
11261
11262 return false;
11263 }
11264
11265 /**
11266 * @var array<number,object[]> {
11267 * @key number Add-on ID.
11268 * @val object[] The add-on's plans and prices object.
11269 * }
11270 */
11271 private $plans_and_pricing_by_addon_id;
11272
11273 /**
11274 * @author Leo Fajardo (@leorw)
11275 * @since 2.3.0
11276 *
11277 * @return array<number,object[]> {
11278 * @key number Add-on ID.
11279 * @val object[] The add-on's plans and prices object.
11280 * }
11281 */
11282 function _get_addons_plans_and_pricing_map_by_id() {
11283 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
11284 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
11285
11286 $plans_and_pricing_by_addon_id = array();
11287 if ( $this->is_api_result_object( $result, 'addons' ) ) {
11288 foreach ( $result->addons as $addon ) {
11289 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
11290 }
11291 }
11292
11293 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
11294 }
11295
11296 return $this->plans_and_pricing_by_addon_id;
11297 }
11298
11299 /**
11300 * @author Leo Fajardo (@leorw)
11301 * @since 2.3.0
11302 *
11303 * @param number $addon_id
11304 * @param bool $is_installed
11305 *
11306 * @return array
11307 */
11308 function _get_addon_info( $addon_id, $is_installed ) {
11309 $addon = $this->get_addon( $addon_id );
11310
11311 if ( ! is_object( $addon ) ) {
11312 // Unexpected call.
11313 return array();
11314 }
11315
11316 $slug = $addon->slug;
11317
11318 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
11319
11320 if ( ! fs_is_network_admin() ) {
11321 // Get blog-level activated installations.
11322 $sites = self::maybe_get_entities_account_option( 'sites', array() );
11323 } else {
11324 $sites = null;
11325
11326 if ( $this->is_addon_activated( $addon_id ) &&
11327 $this->get_addon_instance( $addon_id )->is_network_active()
11328 ) {
11329 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
11330 // Get network-level activated installations.
11331 $sites = self::maybe_get_entities_account_option(
11332 'sites',
11333 array(),
11334 $addon_storage->network_install_blog_id
11335 );
11336 }
11337 }
11338 }
11339
11340 $addon_info = array(
11341 'is_connected' => false,
11342 'slug' => $slug,
11343 'title' => $addon->title,
11344 'is_whitelabeled' => $addon_storage->is_whitelabeled
11345 );
11346
11347 if ( ! $is_installed ) {
11348 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
11349
11350 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
11351 $has_paid_plan = false;
11352 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
11353
11354 if ( is_array( $plans ) && count( $plans ) > 0 ) {
11355 foreach ( $plans as $plan ) {
11356 if ( isset( $plan->pricing ) &&
11357 is_array( $plan->pricing ) &&
11358 count( $plan->pricing ) > 0
11359 ) {
11360 $has_paid_plan = true;
11361 break;
11362 }
11363 }
11364 }
11365
11366 $addon_info['has_paid_plan'] = $has_paid_plan;
11367 }
11368 }
11369
11370 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
11371 return $addon_info;
11372 }
11373
11374 $site = $sites[ $slug ];
11375
11376 $addon_info['is_connected'] = (
11377 ( $addon->parent_plugin_id == $this->get_id() ) &&
11378 is_object( $site ) &&
11379 FS_Site::is_valid_id( $site->id ) &&
11380 FS_User::is_valid_id( $site->user_id ) &&
11381 FS_Plugin_Plan::is_valid_id( $site->plan_id )
11382 );
11383
11384 if ( $addon_info['is_connected'] && $is_installed ) {
11385 return $addon_info;
11386 }
11387
11388 $addon_info['site'] = $site;
11389
11390 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
11391 if ( isset( $plugins_data[ $slug ] ) ) {
11392 $plugin_data = $plugins_data[ $slug ];
11393
11394 $addon_info['version'] = $plugin_data->version;
11395 }
11396
11397 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
11398 if ( isset( $all_plans[ $slug ] ) ) {
11399 $plans = $all_plans[ $slug ];
11400
11401 foreach ( $plans as $plan ) {
11402 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
11403 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
11404 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
11405 break;
11406 }
11407 }
11408 }
11409
11410 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
11411 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
11412 foreach ( $licenses[ $addon_id ] as $license ) {
11413 if ( $license->id == $site->license_id ) {
11414 $addon_info['license'] = $license;
11415 break;
11416 }
11417 }
11418 }
11419
11420 if ( isset( $addon_info['license'] ) ) {
11421 if ( isset( $addon_storage->subscriptions ) &&
11422 ! empty( $addon_storage->subscriptions )
11423 ) {
11424 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11425
11426 foreach ( $addon_subscriptions as $subscription ) {
11427 if ( $subscription->license_id == $site->license_id ) {
11428 $addon_info['subscription'] = $subscription;
11429 break;
11430 }
11431 }
11432 }
11433 }
11434
11435 return $addon_info;
11436 }
11437
11438 /**
11439 * @author Vova Feldman (@svovaf)
11440 * @since 2.0.0
11441 *
11442 * @param number $user_id
11443 *
11444 * @return FS_User
11445 */
11446 static function _get_user_by_id( $user_id ) {
11447 self::$_static_logger->entrance( "user_id = {$user_id}" );
11448
11449 $users = self::get_all_users();
11450
11451 if ( is_array( $users ) ) {
11452 if ( isset( $users[ $user_id ] ) &&
11453 $users[ $user_id ] instanceof FS_User &&
11454 $user_id == $users[ $user_id ]->id
11455 ) {
11456 return $users[ $user_id ];
11457 }
11458
11459 // If user wasn't found by the key, iterate over all the users collection.
11460 foreach ( $users as $user ) {
11461 /**
11462 * @var FS_User $user
11463 */
11464 if ( $user_id == $user->id ) {
11465 return $user;
11466 }
11467 }
11468 }
11469
11470 return null;
11471 }
11472
11473 /**
11474 * Checks if a Freemius user_id is associated with a super-admin.
11475 *
11476 * @author Vova Feldman (@svovaf)
11477 * @since 2.0.0
11478 *
11479 * @param number $user_id
11480 *
11481 * @return bool
11482 */
11483 private static function is_super_admin( $user_id ) {
11484 $is_super_admin = false;
11485
11486 $user = self::_get_user_by_id( $user_id );
11487
11488 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11489 self::require_pluggable_essentials();
11490
11491 $wp_user = get_user_by( 'email', $user->email );
11492
11493 if ( $wp_user instanceof WP_User ) {
11494 $super_admins = get_super_admins();
11495 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11496 }
11497 }
11498
11499 return $is_super_admin;
11500 }
11501
11502 #----------------------------------------------------------------------------------
11503 #region Plans & Licensing
11504 #----------------------------------------------------------------------------------
11505
11506 /**
11507 * Check if running premium plugin code.
11508 *
11509 * @author Vova Feldman (@svovaf)
11510 * @since 1.0.5
11511 *
11512 * @return bool
11513 */
11514 function is_premium() {
11515 /**
11516 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11517 * `register_constructor_hooks` method.
11518 *
11519 * @author Leo Fajardo (@leorw)
11520 * @since 2.2.3
11521 */
11522 return is_object( $this->_plugin ) ?
11523 $this->_plugin->is_premium :
11524 false;
11525 }
11526
11527 /**
11528 * Get site's plan ID.
11529 *
11530 * @author Vova Feldman (@svovaf)
11531 * @since 1.0.2
11532 *
11533 * @return number
11534 */
11535 function get_plan_id() {
11536 return $this->_site->plan_id;
11537 }
11538
11539 /**
11540 * Get site's plan title.
11541 *
11542 * @author Vova Feldman (@svovaf)
11543 * @since 1.0.2
11544 *
11545 * @return string
11546 */
11547 function get_plan_title() {
11548 $plan = $this->get_plan();
11549
11550 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11551 }
11552
11553 /**
11554 * Get site's plan name.
11555 *
11556 * @author Vova Feldman (@svovaf)
11557 * @since 2.0.0
11558 *
11559 * @return string
11560 */
11561 function get_plan_name() {
11562 $plan = $this->get_plan();
11563
11564 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11565 }
11566
11567 /**
11568 * @author Vova Feldman (@svovaf)
11569 * @since 1.0.9
11570 *
11571 * @return FS_Plugin_Plan|false
11572 */
11573 function get_plan() {
11574 if ( ! is_object( $this->_site ) ) {
11575 return false;
11576 }
11577
11578 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11579 $this->_get_plan_by_id( $this->_site->plan_id ) :
11580 false;
11581 }
11582
11583 /**
11584 * @author Vova Feldman (@svovaf)
11585 * @since 1.0.3
11586 *
11587 * @return bool
11588 */
11589 function is_trial() {
11590 $this->_logger->entrance();
11591
11592 if ( ! $this->is_registered( true ) || ! is_object( $this->_site ) ) {
11593 return false;
11594 }
11595
11596 return $this->_site->is_trial();
11597 }
11598
11599 /**
11600 * Check if currently in a trial with payment method (credit card or paypal).
11601 *
11602 * @author Vova Feldman (@svovaf)
11603 * @since 1.1.7
11604 *
11605 * @return bool
11606 */
11607 function is_paid_trial() {
11608 $this->_logger->entrance();
11609
11610 if ( ! $this->is_trial() ) {
11611 return false;
11612 }
11613
11614 if ( ! $this->has_active_valid_license() ) {
11615 return false;
11616 }
11617
11618 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11619 return false;
11620 }
11621
11622 /**
11623 * @var FS_Subscription $subscription
11624 */
11625 $subscription = $this->_get_subscription( $this->_license->id );
11626
11627 return ( is_object( $subscription ) && $subscription->is_active() );
11628 }
11629
11630 /**
11631 * Check if trial already utilized.
11632 *
11633 * @since 1.0.9
11634 *
11635 * @return bool
11636 */
11637 function is_trial_utilized() {
11638 $this->_logger->entrance();
11639
11640 if ( ! $this->is_registered() ) {
11641 return false;
11642 }
11643
11644 return $this->_site->is_trial_utilized();
11645 }
11646
11647 /**
11648 * Get trial plan information (if in trial).
11649 *
11650 * @author Vova Feldman (@svovaf)
11651 * @since 1.0.9
11652 *
11653 * @return bool|FS_Plugin_Plan
11654 */
11655 function get_trial_plan() {
11656 $this->_logger->entrance();
11657
11658 if ( ! $this->is_trial() ) {
11659 return false;
11660 }
11661
11662 // Try to load plan from local cache.
11663 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11664
11665 if ( ! is_object( $trial_plan ) ) {
11666 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11667
11668 /**
11669 * If managed to fetch the plan, add it to the plans collection.
11670 */
11671 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11672 if ( ! is_array( $this->_plans ) ) {
11673 $this->_plans = array();
11674 }
11675
11676 $this->_plans[] = $trial_plan;
11677 $this->_store_plans();
11678 }
11679 }
11680
11681 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11682 return $trial_plan;
11683 }
11684
11685 /**
11686 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11687 */
11688 $trial_plan = new FS_Plugin_Plan();
11689 $trial_plan->id = $this->_site->trial_plan_id;
11690 $trial_plan->name = 'pro';
11691 $trial_plan->title = 'Pro';
11692
11693 return $trial_plan;
11694 }
11695
11696 /**
11697 * Check if the user has an activate, non-expired license on current plugin's install.
11698 *
11699 * @since 1.0.9
11700 *
11701 * @return bool
11702 */
11703 function is_paying() {
11704 $this->_logger->entrance();
11705
11706 if ( ! $this->is_registered( true ) ) {
11707 return false;
11708 }
11709
11710 if ( ! $this->has_paid_plan() ) {
11711 return false;
11712 }
11713
11714 return (
11715 ! $this->is_trial() &&
11716 'free' !== $this->get_plan_name() &&
11717 $this->has_active_valid_license()
11718 );
11719 }
11720
11721 /**
11722 * @author Vova Feldman (@svovaf)
11723 * @since 1.0.4
11724 *
11725 * @return bool
11726 */
11727 function is_free_plan() {
11728 if ( ! $this->is_registered() ) {
11729 return true;
11730 }
11731
11732 if ( ! $this->has_paid_plan() ) {
11733 return true;
11734 }
11735
11736 return (
11737 'free' === $this->get_plan_name() ||
11738 ! $this->has_features_enabled_license()
11739 );
11740 }
11741
11742 /**
11743 * @author Vova Feldman (@svovaf)
11744 * @since 1.0.5
11745 *
11746 * @return bool
11747 */
11748 function _has_premium_license() {
11749 $this->_logger->entrance();
11750
11751 $premium_license = $this->_get_available_premium_license();
11752
11753 return ( false !== $premium_license );
11754 }
11755
11756 /**
11757 * Check if user has any licenses associated with the plugin (including expired or blocking).
11758 *
11759 * @author Vova Feldman (@svovaf)
11760 * @since 1.1.7.3
11761 *
11762 * @param bool $including_foreign
11763 *
11764 * @return bool
11765 */
11766 function has_any_license( $including_foreign = true ) {
11767 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11768 return false;
11769 }
11770
11771 if ( $including_foreign ) {
11772 return true;
11773 }
11774
11775 foreach ( $this->_licenses as $license ) {
11776 if ( $this->_user->id == $license->user_id ) {
11777 return true;
11778 }
11779 }
11780
11781 return false;
11782 }
11783
11784 /**
11785 * @author Vova Feldman (@svovaf)
11786 * @since 1.0.5
11787 *
11788 * @param bool|null $is_localhost
11789 *
11790 * @return FS_Plugin_License|false
11791 */
11792 function _get_available_premium_license( $is_localhost = null ) {
11793 $this->_logger->entrance();
11794
11795 $licenses = $this->get_available_premium_licenses( $is_localhost );
11796 if ( ! empty( $licenses ) ) {
11797 return $licenses[0];
11798 }
11799
11800 return false;
11801 }
11802
11803 /**
11804 * @author Vova Feldman (@svovaf)
11805 * @since 1.0.5
11806 *
11807 * @param bool|null $is_localhost
11808 *
11809 * @return FS_Plugin_License[]
11810 */
11811 function get_available_premium_licenses( $is_localhost = null ) {
11812 $this->_logger->entrance();
11813
11814 $licenses = array();
11815 if ( ! $this->has_paid_plan() ) {
11816 return $licenses;
11817 }
11818
11819 if ( is_array( $this->_licenses ) ) {
11820 foreach ( $this->_licenses as $license ) {
11821 if ( ! $license->can_activate( $is_localhost ) ) {
11822 continue;
11823 }
11824
11825 $licenses[] = $license;
11826 }
11827 }
11828
11829 return $licenses;
11830 }
11831
11832 /**
11833 * Sync local plugin plans with remote server.
11834 *
11835 * 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).
11836 *
11837 * @author Vova Feldman (@svovaf)
11838 * @since 1.0.5
11839 *
11840 * @return FS_Plugin_Plan[]|object
11841 */
11842 function _sync_plans() {
11843 $plans = $this->_fetch_plugin_plans();
11844
11845 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11846 $plans_map = array();
11847 foreach ( $plans as $plan ) {
11848 $plans_map[ $plan->id ] = true;
11849 }
11850
11851 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11852
11853 foreach ( $plans_ids_to_keep as $plan_id ) {
11854 if ( isset( $plans_map[ $plan_id ] ) ) {
11855 continue;
11856 }
11857
11858 $missing_plan = self::_get_plan_by_id( $plan_id );
11859
11860 if ( is_object( $missing_plan ) ) {
11861 $plans[] = $missing_plan;
11862 }
11863 }
11864
11865 $this->_plans = $plans;
11866 $this->_store_plans();
11867 }
11868
11869 $this->do_action( 'after_plans_sync', $plans );
11870
11871 return $this->_plans;
11872 }
11873
11874 /**
11875 * Check if specified plan exists locally. If not, fetch it and store it.
11876 *
11877 * @author Vova Feldman (@svovaf)
11878 * @since 2.0.0
11879 *
11880 * @param number $plan_id
11881 *
11882 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11883 */
11884 private function sync_plan_if_not_exist( $plan_id ) {
11885 $plan = self::_get_plan_by_id( $plan_id );
11886
11887 if ( is_object( $plan ) ) {
11888 // Plan already exists.
11889 return $plan;
11890 }
11891
11892 $plan = $this->fetch_plan_by_id( $plan_id );
11893
11894 if ( $plan instanceof FS_Plugin_Plan ) {
11895 $this->_plans[] = $plan;
11896 $this->_store_plans();
11897
11898 return $plan;
11899 }
11900
11901 return $plan;
11902 }
11903
11904 /**
11905 * Check if specified license exists locally. If not, fetch it and store it.
11906 *
11907 * @author Vova Feldman (@svovaf)
11908 * @since 2.0.0
11909 *
11910 * @param number $license_id
11911 * @param string $license_key
11912 *
11913 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11914 */
11915 private function sync_license_if_not_exist( $license_id, $license_key ) {
11916 $license = $this->_get_license_by_id( $license_id );
11917
11918 if ( is_object( $license ) ) {
11919 // License already exists.
11920 return $license;
11921 }
11922
11923 $license = $this->fetch_license_by_key( $license_id, $license_key );
11924
11925 if ( $license instanceof FS_Plugin_License ) {
11926 $this->_licenses[] = $license;
11927
11928 $this->set_license( $license );
11929
11930 $this->_store_licenses();
11931
11932 return $license;
11933 }
11934
11935 return $license;
11936 }
11937
11938 /**
11939 * Get a collection of unique plan IDs that are associated with any installs in the network.
11940 *
11941 * @author Leo Fajardo (@leorw)
11942 * @since 2.0.0
11943 *
11944 * @return number[]
11945 */
11946 private function get_plans_ids_associated_with_installs() {
11947 if ( ! is_multisite() ) {
11948 if ( ! is_object( $this->_site ) ||
11949 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11950 ) {
11951 return array();
11952 }
11953
11954 return array( $this->_site->plan_id );
11955 }
11956
11957 $plan_ids = array();
11958 $sites = self::get_sites();
11959 foreach ( $sites as $site ) {
11960 $blog_id = self::get_site_blog_id( $site );
11961 $install = $this->get_install_by_blog_id( $blog_id );
11962
11963 if ( ! is_object( $install ) ||
11964 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11965 ) {
11966 continue;
11967 }
11968
11969 $plan_ids[ $install->plan_id ] = true;
11970 }
11971
11972 return array_keys( $plan_ids );
11973 }
11974
11975 /**
11976 * Get a collection of unique license IDs that are associated with any installs in the network.
11977 *
11978 * @author Leo Fajardo (@leorw)
11979 * @since 2.0.0
11980 *
11981 * @return number[]
11982 */
11983 private function get_license_ids_associated_with_installs() {
11984 if ( ! $this->_is_network_active ) {
11985 if ( ! is_object( $this->_site ) ||
11986 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11987 ) {
11988 return array();
11989 }
11990
11991 return array( $this->_site->license_id );
11992 }
11993
11994 $license_ids = array();
11995 $sites = self::get_sites();
11996 foreach ( $sites as $site ) {
11997 $blog_id = self::get_site_blog_id( $site );
11998 $install = $this->get_install_by_blog_id( $blog_id );
11999
12000 if ( ! is_object( $install ) ||
12001 ! FS_Plugin_License::is_valid_id( $install->license_id )
12002 ) {
12003 continue;
12004 }
12005
12006 $license_ids[ $install->license_id ] = true;
12007 }
12008
12009 return array_keys( $license_ids );
12010 }
12011
12012 /**
12013 * @author Vova Feldman (@svovaf)
12014 * @since 1.0.5
12015 *
12016 * @param number $id
12017 *
12018 * @return FS_Plugin_Plan|false
12019 */
12020 function _get_plan_by_id( $id ) {
12021 $this->_logger->entrance();
12022
12023 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
12024 $this->_sync_plans();
12025 }
12026
12027 foreach ( $this->_plans as $plan ) {
12028 if ( $id == $plan->id ) {
12029 return $plan;
12030 }
12031 }
12032
12033 return false;
12034 }
12035
12036 /**
12037 * @author Vova Feldman (@svovaf)
12038 * @since 1.1.8.1
12039 *
12040 * @param string $name
12041 *
12042 * @return FS_Plugin_Plan|false
12043 */
12044 private function get_plan_by_name( $name ) {
12045 $this->_logger->entrance();
12046
12047 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
12048 $this->_sync_plans();
12049 }
12050
12051 foreach ( $this->_plans as $plan ) {
12052 if ( $name == $plan->name ) {
12053 return $plan;
12054 }
12055 }
12056
12057 return false;
12058 }
12059
12060 /**
12061 * Sync local licenses with remote server.
12062 *
12063 * @author Vova Feldman (@svovaf)
12064 * @since 1.0.6
12065 *
12066 * @param number|bool $site_license_id
12067 * @param number|null $blog_id
12068 *
12069 * @return FS_Plugin_License[]|object
12070 */
12071 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
12072 $this->_logger->entrance();
12073
12074 $is_network_admin = fs_is_network_admin();
12075
12076 if ( $is_network_admin && is_null( $blog_id ) ) {
12077 $all_licenses = self::get_all_licenses( $this->_module_id );
12078 } else {
12079 $all_licenses = $this->get_user_licenses( $this->_user->id );
12080 }
12081
12082 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
12083
12084 $all_licenses_map = array();
12085 foreach ( $all_licenses as $license ) {
12086 $all_licenses_map[ $license->id ] = true;
12087 }
12088
12089 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
12090
12091 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
12092 $licenses_map = array();
12093 foreach ( $licenses as $license ) {
12094 $licenses_map[ $license->id ] = true;
12095 }
12096
12097 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
12098 // foreach ( $license_ids_to_keep as $license_id ) {
12099 // if ( isset( $licenses_map[ $license_id ] ) ) {
12100 // continue;
12101 // }
12102 //
12103 // $missing_license = self::_get_license_by_id( $license_id, false );
12104 // if ( is_object( $missing_license ) ) {
12105 // $licenses[] = $missing_license;
12106 // $licenses_map[ $missing_license->id ] = true;
12107 // }
12108 // }
12109
12110 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
12111
12112 foreach ( $user_license_ids as $key => $license_id ) {
12113 if ( ! isset( $licenses_map[ $license_id ] ) ) {
12114 // Remove access to licenses that no longer exist.
12115 unset( $user_license_ids[ $key ] );
12116 }
12117 }
12118
12119 if ( ! empty( $user_license_ids ) ) {
12120 foreach ( $licenses_map as $license_id => $value ) {
12121 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
12122 // Associate new licenses with the user who triggered the license syncing.
12123 $user_license_ids[] = $license_id;
12124 }
12125 }
12126
12127 $user_license_ids = array_unique( $user_license_ids );
12128 } else {
12129 $user_license_ids = array_keys( $licenses_map );
12130 }
12131
12132 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
12133 $user_licenses = array();
12134 foreach ( $licenses as $license ) {
12135 if ( ! in_array( $license->id, $user_license_ids ) ) {
12136 continue;
12137 }
12138
12139 $user_licenses[] = $license;
12140 }
12141
12142 $this->_licenses = $user_licenses;
12143 } else {
12144 $this->_licenses = $licenses;
12145 }
12146
12147 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
12148
12149 $this->_store_licenses( true, $this->_module_id, $licenses );
12150 }
12151
12152 // Update current license.
12153 if ( is_object( $this->_license ) ) {
12154 $license = $this->_get_license_by_id( $this->_license->id );
12155
12156 if ( is_object( $license ) ) {
12157 /**
12158 * `$license` can be `false` in case a user change action has just been completed and this method
12159 * has synced the `$this->_licenses` collection for the new user. In this case, the
12160 * `$this->_licenses` collection may have only the newly activated license that is associated with
12161 * the new user. `set_license` will eventually be called in the same request by the logic that
12162 * follows outside this method which will detect that the install's license has been updated, and
12163 * then `_update_site_license` will be called which in turn will call `set_license`.
12164 *
12165 * @author Leo Fajardo (@leorw)
12166 * @since 2.3.2
12167 */
12168 $this->set_license( $license );
12169 }
12170 }
12171
12172 return $this->_licenses;
12173 }
12174
12175 /**
12176 * @author Vova Feldman (@svovaf)
12177 * @since 1.0.5
12178 *
12179 * @param number $id
12180 * @param bool $sync_licenses
12181 *
12182 * @return FS_Plugin_License|false
12183 */
12184 function _get_license_by_id( $id, $sync_licenses = true ) {
12185 $this->_logger->entrance();
12186
12187 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
12188 return false;
12189 }
12190
12191 /**
12192 * When running from the network level admin and opted-in from the network,
12193 * check if the license exists in the network user licenses collection.
12194 *
12195 * @author Vova Feldman (@svovaf)
12196 * @since 2.0.0
12197 */
12198 if ( fs_is_network_admin() &&
12199 $this->is_network_registered() &&
12200 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
12201 ) {
12202 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
12203
12204 foreach ( $licenses as $license ) {
12205 if ( $id == $license->id ) {
12206 return $license;
12207 }
12208 }
12209 }
12210
12211 if ( ! $this->has_any_license() && $sync_licenses ) {
12212 $this->_sync_licenses( $id );
12213 }
12214
12215 if ( is_array( $this->_licenses ) ) {
12216 foreach ( $this->_licenses as $license ) {
12217 if ( $id == $license->id ) {
12218 return $license;
12219 }
12220 }
12221 }
12222
12223 return false;
12224 }
12225
12226 /**
12227 * 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.
12228 *
12229 * @author Vova Feldman (@svovaf)
12230 * @since 2.0.0
12231 *
12232 * @param number $id
12233 *
12234 * @return FS_Plugin_License
12235 */
12236 private function get_license_by_id( $id ) {
12237 $licenses = self::get_all_licenses( $this->_module_id );
12238
12239 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
12240 foreach ( $licenses as $license ) {
12241 if ( $id == $license->id ) {
12242 return $license;
12243 }
12244 }
12245 }
12246
12247 return null;
12248 }
12249
12250 /**
12251 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
12252 *
12253 * @author Vova Feldman (@svovaf)
12254 * @since 2.0.0
12255 *
12256 * @return \FS_Plugin_License|mixed
12257 */
12258 private function sync_site_license() {
12259 $api = $this->get_api_user_scope();
12260
12261 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
12262
12263 if ( ! $this->is_api_result_entity( $result ) ) {
12264 return $result;
12265 }
12266
12267 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
12268 $this->_store_licenses();
12269
12270 return $license;
12271 }
12272
12273 /**
12274 * Get all user's available licenses for the current module.
12275 *
12276 * @author Vova Feldman (@svovaf)
12277 * @since 2.0.0
12278 *
12279 * @param number $user_id
12280 *
12281 * @return FS_Plugin_License[]
12282 */
12283 private function get_user_licenses( $user_id ) {
12284 $all_licenses = self::get_all_licenses( $this->_module_id );
12285 if ( empty( $all_licenses ) ) {
12286 return array();
12287 }
12288
12289 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
12290 if ( empty( $user_license_ids ) ) {
12291 return array();
12292 }
12293
12294 $licenses = array();
12295 foreach ( $all_licenses as $license ) {
12296 if ( in_array( $license->id, $user_license_ids ) ) {
12297 $licenses[] = $license;
12298 }
12299 }
12300
12301 return $licenses;
12302 }
12303
12304 /**
12305 * Checks if the context license is network activated except on the given blog ID.
12306 *
12307 * @author Vova Feldman (@svovaf)
12308 * @since 2.0.0
12309 *
12310 * @param int $except_blog_id
12311 *
12312 * @return bool
12313 */
12314 private function is_license_network_active( $except_blog_id = 0 ) {
12315 $this->_logger->entrance();
12316
12317 if ( ! is_object( $this->_license ) ) {
12318 return false;
12319 }
12320
12321 $sites = self::get_sites();
12322
12323 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
12324 // There are more sites than the number of activations, so license cannot be network activated.
12325 return false;
12326 }
12327
12328 foreach ( $sites as $site ) {
12329 $blog_id = self::get_site_blog_id( $site );
12330
12331 if ( $except_blog_id == $blog_id ) {
12332 // Skip excluded blog.
12333 continue;
12334 }
12335
12336 $install = $this->get_install_by_blog_id( $blog_id );
12337
12338 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
12339 return false;
12340 }
12341 }
12342
12343 return true;
12344 }
12345
12346 /**
12347 * 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.
12348 *
12349 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
12350 *
12351 * @author Vova Feldman (@svovaf)
12352 * @since 2.0.0
12353 *
12354 * @param \FS_User $user
12355 * @param \FS_Plugin_License $license
12356 *
12357 * @return bool
12358 */
12359 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
12360 $this->_logger->entrance();
12361
12362 $result = $this->can_activate_license_on_network( $license );
12363
12364 if ( false === $result ) {
12365 return false;
12366 }
12367
12368 $installs_without_license = $result['installs'];
12369 if ( ! empty( $installs_without_license ) ) {
12370 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
12371 }
12372
12373 $disconnected_site_ids = $result['sites'];
12374 if ( ! empty( $disconnected_site_ids ) ) {
12375 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
12376 }
12377
12378 $this->link_license_2_user( $license->id, $user->id );
12379
12380 // Sync license after activations.
12381 $license->activated += $result['production_count'];
12382 $license->activated_local += $result['localhost_count'];
12383
12384 // $this->_store_licenses()
12385
12386 return true;
12387 }
12388
12389 /**
12390 * Checks if the given license can be activated on the whole network.
12391 *
12392 * @author Vova Feldman (@svovaf)
12393 * @since 2.0.0
12394 *
12395 * @param \FS_Plugin_License $license
12396 *
12397 * @return false|array {
12398 * @type array[int]FS_Site $installs Blog ID to install map.
12399 * @type int[] $sites Non-connected blog IDs.
12400 * @type int $production_count Production sites count.
12401 * @type int $localhost_count Production sites count.
12402 * }
12403 */
12404 private function can_activate_license_on_network( FS_Plugin_License $license ) {
12405 $sites = self::get_sites();
12406
12407 $production_count = 0;
12408 $localhost_count = 0;
12409
12410 $installs_without_license = array();
12411 $disconnected_site_ids = array();
12412
12413 foreach ( $sites as $site ) {
12414 $blog_id = self::get_site_blog_id( $site );
12415 $install = $this->get_install_by_blog_id( $blog_id );
12416
12417 if ( is_object( $install ) ) {
12418 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
12419 // License already activated on the install.
12420 continue;
12421 }
12422
12423 $url = $install->url;
12424
12425 $installs_without_license[ $blog_id ] = $install;
12426 } else {
12427 $url = is_object( $site ) ?
12428 $site->siteurl :
12429 self::get_unfiltered_site_url( $blog_id );
12430
12431 $disconnected_site_ids[] = $blog_id;
12432 }
12433
12434 if ( FS_Site::is_localhost_by_address( $url ) ) {
12435 $localhost_count ++;
12436 } else {
12437 $production_count ++;
12438 }
12439 }
12440
12441 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12442 return false;
12443 }
12444
12445 return array(
12446 'installs' => $installs_without_license,
12447 'sites' => $disconnected_site_ids,
12448 'production_count' => $production_count,
12449 'localhost_count' => $localhost_count,
12450 );
12451 }
12452
12453 /**
12454 * Activate a given license on a collection of installs.
12455 *
12456 * @author Vova Feldman (@svovaf)
12457 * @since 2.0.0
12458 *
12459 * @param \FS_User $user
12460 * @param string $license_key
12461 * @param array $blog_2_install_map {
12462 * @key int Blog ID.
12463 * @value FS_Site Blog's associated install.
12464 * }
12465 *
12466 * @return mixed|true
12467 */
12468 private function activate_license_on_many_installs(
12469 FS_User $user,
12470 $license_key,
12471 array $blog_2_install_map
12472 ) {
12473 $params = array(
12474 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12475 );
12476
12477 $install_2_blog_map = array();
12478 foreach ( $blog_2_install_map as $blog_id => $install ) {
12479 $params[] = array( 'id' => $install->id, 'url' => $install->url );
12480
12481 $install_2_blog_map[ $install->id ] = $blog_id;
12482 }
12483
12484 $result = $this->get_api_user_scope_by_user( $user )->call(
12485 "plugins/{$this->_plugin->id}/installs.json",
12486 'PUT',
12487 $params
12488 );
12489
12490 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12491 return $result;
12492 }
12493
12494 foreach ( $result->installs as $r_install ) {
12495 $install = new FS_Site( $r_install );
12496 $install->is_disconnected = false;
12497
12498 // Update install.
12499 $this->_store_site(
12500 true,
12501 $install_2_blog_map[ $r_install->id ],
12502 $install
12503 );
12504 }
12505
12506 return true;
12507 }
12508
12509 /**
12510 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12511 *
12512 * @author Vova Feldman (@svovaf)
12513 * @since 2.3.1
12514 *
12515 * @param \FS_User $user
12516 * @param string $license_key
12517 *
12518 * @return true|mixed True if successful, otherwise, the API result.
12519 */
12520 private function activate_license_on_site( FS_User $user, $license_key ) {
12521 return $this->activate_license_on_many_sites( $user, $license_key );
12522 }
12523
12524 /**
12525 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12526 *
12527 * @author Vova Feldman (@svovaf)
12528 * @since 2.0.0
12529 *
12530 * @param \FS_User $user
12531 * @param string $license_key
12532 * @param int[] $site_ids
12533 *
12534 * @return true|mixed True if successful, otherwise, the API result.
12535 */
12536 private function activate_license_on_many_sites(
12537 FS_User $user,
12538 $license_key,
12539 array $site_ids = array()
12540 ) {
12541 $sites = array();
12542 foreach ( $site_ids as $site_id ) {
12543 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12544 }
12545
12546 // Install the plugin.
12547 $result = $this->create_installs_with_user(
12548 $user,
12549 $license_key,
12550 false,
12551 $sites,
12552 false,
12553 true
12554 );
12555
12556 if ( ! $this->is_api_result_entity( $result ) &&
12557 ! $this->is_api_result_object( $result, 'installs' )
12558 ) {
12559 return $result;
12560 }
12561
12562 $installs = array();
12563
12564 if ( $this->is_api_result_entity( $result ) ) {
12565 $install = new FS_Site( $result );
12566
12567 $this->_user = $user;
12568
12569 $this->_store_site( true, null, $install );
12570
12571 $this->_site = $install;
12572
12573 $this->reset_anonymous_mode();
12574 } else {
12575 foreach ( $result->installs as $install ) {
12576 $installs[] = new FS_Site( $install );
12577 }
12578
12579 // Map site addresses to their blog IDs.
12580 $address_to_blog_map = $this->get_address_to_blog_map();
12581
12582 $first_blog_id = null;
12583
12584 foreach ( $installs as $install ) {
12585 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12586 $blog_id = $address_to_blog_map[ $address ];
12587
12588 $this->_store_site( true, $blog_id, $install );
12589
12590 $this->reset_anonymous_mode( $blog_id );
12591
12592 if ( is_null( $first_blog_id ) ) {
12593 $first_blog_id = $blog_id;
12594 }
12595 }
12596
12597 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12598 $this->_storage->network_install_blog_id = $first_blog_id;
12599 }
12600 }
12601
12602 return true;
12603 }
12604
12605 /**
12606 * Sync site's license with user licenses.
12607 *
12608 * @author Vova Feldman (@svovaf)
12609 * @since 1.0.6
12610 *
12611 * @param FS_Plugin_License|null $new_license
12612 *
12613 * @return FS_Plugin_License|null
12614 */
12615 function _update_site_license( $new_license ) {
12616 $this->_logger->entrance();
12617
12618 /**
12619 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12620 * accordingly so that it will also handle the case when an ownership change is done via license
12621 * activation.
12622 *
12623 * @author Leo Fajardo (@leorw)
12624 * @since 2.3.2
12625 */
12626 $this->set_license( $new_license );
12627
12628 if ( ! is_object( $new_license ) ) {
12629 $this->_site->license_id = null;
12630 $this->_sync_site_subscription( null );
12631
12632 return $this->_license;
12633 }
12634
12635 $this->_site->license_id = $this->_license->id;
12636
12637 if ( ! is_array( $this->_licenses ) ) {
12638 $this->_licenses = array();
12639 }
12640
12641 $is_license_found = false;
12642 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12643 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12644 $this->_licenses[ $i ] = $new_license;
12645
12646 $is_license_found = true;
12647 break;
12648 }
12649 }
12650
12651 // If new license just append.
12652 if ( ! $is_license_found ) {
12653 $this->_licenses[] = $new_license;
12654 }
12655
12656 $this->_sync_site_subscription( $new_license );
12657
12658 return $this->_license;
12659 }
12660
12661 /**
12662 * @author Vova Feldman (@svovaf)
12663 * @since 2.3.1
12664 *
12665 * @param \FS_Plugin_License $license
12666 */
12667 private function set_license( FS_Plugin_License $license = null ) {
12668 $this->_license = $license;
12669
12670 $this->maybe_update_whitelabel_flag( $license );
12671 }
12672
12673 /**
12674 * @author Leo Fajardo (@leorw)
12675 * @since 2.3.1
12676 *
12677 * @param FS_Plugin_License $license
12678 */
12679 private function maybe_update_whitelabel_flag( $license ) {
12680 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12681 $this->_storage->is_whitelabeled :
12682 false;
12683
12684 if ( is_object( $license ) ) {
12685 $license_user = self::_get_user_by_id( $license->user_id );
12686
12687 if ( ! is_object( $license_user ) ) {
12688 // If foreign license, do not update the `is_whitelabeled` flag.
12689 return;
12690 }
12691
12692 if ( $this->is_addon() ) {
12693 /**
12694 * Store the last license data to the parent's storage since it's needed only when showing the
12695 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12696 * iterate over the add-ons just to get the last license data.
12697 */
12698 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12699 } else {
12700 $this->store_last_activated_license_data( $license );
12701 }
12702
12703 if ( $license->is_whitelabeled ) {
12704 // Activated a developer license, data should be hidden.
12705 $is_whitelabeled = true;
12706 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12707 // The account owner activated a regular license key, no need to hide the data.
12708 $is_whitelabeled = false;
12709 }
12710 }
12711
12712 $this->_storage->is_whitelabeled = $is_whitelabeled;
12713
12714 // Reset the whitelabeled status after update.
12715 $this->is_whitelabeled = null;
12716 if ( $this->is_addon() ) {
12717 $parent_fs = $this->get_parent_instance();
12718
12719 if ( is_object( $parent_fs ) ) {
12720 $parent_fs->is_whitelabeled = null;
12721 }
12722 }
12723 }
12724
12725 /**
12726 * @author Leo Fajardo (@leorw)
12727 * @since 2.3.1
12728 *
12729 * @param FS_Plugin_License $license
12730 * @param FS_User $license_user
12731 */
12732 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12733 if ( ! is_object( $license_user ) ) {
12734 $this->_storage->last_license_key = md5( $license->secret_key );
12735 $this->_storage->last_license_user_id = null;
12736 } else {
12737 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12738 $this->_storage->last_license_user_id = $license_user->id;
12739 }
12740 }
12741
12742 /**
12743 * @author Leo Fajardo (@leorw)
12744 * @since 2.3.1
12745 *
12746 * @param bool $ignore_data_debug_mode
12747 *
12748 * @return bool
12749 */
12750 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12751 if ( true !== $this->_storage->is_whitelabeled ) {
12752 return false;
12753 } else if ( $ignore_data_debug_mode ) {
12754 return true;
12755 }
12756
12757 $fs = $this->is_addon() ?
12758 $this->get_parent_instance() :
12759 $this;
12760
12761 return ! $fs->is_data_debug_mode();
12762 }
12763
12764 /**
12765 * @author Leo Fajardo (@leorw)
12766 * @since 2.3.1
12767 *
12768 * @return number
12769 */
12770 function get_last_license_user_id() {
12771 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12772 $this->_storage->last_license_user_id :
12773 null;
12774 }
12775
12776 /**
12777 * @author Leo Fajardo (@leorw)
12778 * @since 2.3.1
12779 *
12780 * @param int $blog_id
12781 * @param bool $ignore_data_debug_mode
12782 *
12783 * @return bool
12784 */
12785 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12786 if ( ! is_null( $blog_id ) ) {
12787 $this->switch_to_blog( $blog_id );
12788 }
12789
12790 if ( ! is_null( $this->is_whitelabeled ) ) {
12791 $is_whitelabeled = $this->is_whitelabeled;
12792 } else {
12793 $is_whitelabeled = false;
12794
12795 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12796
12797 if ( ! $this->has_addons() ) {
12798 $is_whitelabeled = $is_whitelabeled_flag;
12799 } else if ( $is_whitelabeled_flag ) {
12800 $is_whitelabeled = true;
12801 } else {
12802 if ( $this->is_registered() || $this->is_premium() ) {
12803 $addon_ids = $this->get_updated_account_addons();
12804 } else {
12805 $addons = self::get_all_addons();
12806
12807 $plugin_addons = isset( $addons[ $this->_plugin->id ] ) ?
12808 $addons[ $this->_plugin->id ] :
12809 array();
12810
12811 $addon_ids = array();
12812 foreach ( $plugin_addons as $addon ) {
12813 $addon_ids[] = $addon->id;
12814 }
12815 }
12816
12817 $installed_addons = $this->get_installed_addons();
12818 foreach ( $installed_addons as $fs_addon ) {
12819 $addon_ids[] = $fs_addon->get_id();
12820 }
12821
12822 if ( ! empty( $addon_ids ) ) {
12823 $addon_ids = array_unique( $addon_ids );
12824
12825 $is_network_level = (
12826 fs_is_network_admin() &&
12827 $this->is_network_active()
12828 );
12829
12830 foreach ( $addon_ids as $addon_id ) {
12831 $addon = $this->get_addon( $addon_id );
12832
12833 if ( ! is_object( $addon ) ) {
12834 continue;
12835 }
12836
12837 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12838 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12839 self::get_addon_instance( $addon_id ) :
12840 null;
12841
12842 $was_addon_network_activated = false;
12843
12844 if ( is_object( $fs_addon ) ) {
12845 $was_addon_network_activated = $fs_addon->is_network_active();
12846 } else if ( $is_network_level ) {
12847 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12848 }
12849
12850 $network_delegated_connection = (
12851 $was_addon_network_activated &&
12852 $addon_storage->get( 'is_delegated_connection', false, true )
12853 );
12854
12855 if (
12856 $is_network_level &&
12857 ( ! $was_addon_network_activated || $network_delegated_connection )
12858 ) {
12859 $sites = self::get_sites();
12860
12861 /**
12862 * If in network admin area and the add-on was not network-activated or network-activated
12863 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12864 */
12865 foreach ( $sites as $site ) {
12866 $site_info = $this->get_site_info( $site );
12867
12868 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12869 $is_whitelabeled = true;
12870 break;
12871 }
12872 }
12873
12874 if ( $is_whitelabeled ) {
12875 break;
12876 }
12877 } else {
12878 /**
12879 * This will be executed when any of the following is met:
12880 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12881 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12882 * 3. Add-on was not network-activated and in site admin area.
12883 */
12884 if ( true === $addon_storage->is_whitelabeled ) {
12885 $is_whitelabeled = true;
12886 break;
12887 }
12888 }
12889 }
12890 }
12891 }
12892
12893 $this->is_whitelabeled = $is_whitelabeled;
12894
12895 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12896 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12897 }
12898
12899 if ( ! is_null( $blog_id ) ) {
12900 $this->restore_current_blog();
12901 }
12902 }
12903
12904 return (
12905 $is_whitelabeled &&
12906 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12907 );
12908 }
12909
12910 /**
12911 * Sync site's subscription.
12912 *
12913 * @author Vova Feldman (@svovaf)
12914 * @since 1.0.9
12915 *
12916 * @param FS_Plugin_License|null $license
12917 *
12918 * @return bool|\FS_Subscription
12919 */
12920 private function _sync_site_subscription( $license ) {
12921 if ( ! is_object( $license ) ) {
12922 $this->delete_unused_subscriptions();
12923
12924 return false;
12925 }
12926
12927 // Load subscription details if not lifetime.
12928 $subscription = $license->is_lifetime() ?
12929 false :
12930 $this->_fetch_site_license_subscription();
12931
12932 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12933 $this->store_subscription( $subscription );
12934 } else {
12935 $this->delete_unused_subscriptions();
12936 }
12937
12938 return $subscription;
12939 }
12940
12941 /**
12942 * @author Vova Feldman (@svovaf)
12943 * @since 1.0.6
12944 *
12945 * @return bool|\FS_Plugin_License
12946 */
12947 function _get_license() {
12948 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12949 return $this->_license;
12950 }
12951
12952 return $this->_get_available_premium_license();
12953 }
12954
12955 /**
12956 * @param number $license_id
12957 *
12958 * @return null|\FS_Subscription
12959 */
12960 function _get_subscription( $license_id ) {
12961 if ( ! isset( $this->_storage->subscriptions ) ||
12962 empty( $this->_storage->subscriptions )
12963 ) {
12964 return null;
12965 }
12966
12967 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12968 if ( $subscription->license_id == $license_id ) {
12969 return $subscription;
12970 }
12971 }
12972
12973 return null;
12974 }
12975
12976 /**
12977 * @author Leo Fajardo (@leorw)
12978 * @since 2.0.0
12979 *
12980 * @param FS_Subscription $subscription
12981 */
12982 function store_subscription( FS_Subscription $subscription ) {
12983 if ( ! isset( $this->_storage->subscriptions ) ) {
12984 $this->_storage->subscriptions = array();
12985 }
12986
12987 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12988 $this->_storage->subscriptions = array( $subscription );
12989
12990 return;
12991 }
12992
12993 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
12994
12995 $updated_subscription = false;
12996 foreach ( $subscriptions as $key => $existing_subscription ) {
12997 if ( $existing_subscription->id == $subscription->id ) {
12998 $subscriptions[ $key ] = $subscription;
12999 $updated_subscription = true;
13000 break;
13001 }
13002 }
13003
13004 if ( ! $updated_subscription ) {
13005 $subscriptions[] = $subscription;
13006 }
13007
13008 $this->_storage->subscriptions = $subscriptions;
13009 }
13010
13011 /**
13012 * @author Leo Fajardo (@leorw)
13013 * @since 2.0.0
13014 */
13015 function delete_unused_subscriptions() {
13016 if ( ! isset( $this->_storage->subscriptions ) ||
13017 empty( $this->_storage->subscriptions ) ||
13018 // Clean up only if there are already at least 3 subscriptions.
13019 ( count( $this->_storage->subscriptions ) < 3 )
13020 ) {
13021 return;
13022 }
13023
13024 if ( ! is_multisite() ) {
13025 // If not multisite, there should only be 1 subscription, so just clear the array.
13026 $this->_storage->subscriptions = array();
13027
13028 return;
13029 }
13030
13031 $subscriptions_to_keep_by_license_id_map = array();
13032 $sites = self::get_sites();
13033 foreach ( $sites as $site ) {
13034 $blog_id = self::get_site_blog_id( $site );
13035 $install = $this->get_install_by_blog_id( $blog_id );
13036
13037 if ( ! is_object( $install ) ||
13038 ! FS_Plugin_License::is_valid_id( $install->license_id )
13039 ) {
13040 continue;
13041 }
13042
13043 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
13044 }
13045
13046 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
13047 $this->_storage->subscriptions = array();
13048
13049 return;
13050 }
13051
13052 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
13053 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
13054 unset( $this->_storage->subscriptions[ $key ] );
13055 }
13056 }
13057 }
13058
13059 /**
13060 * @author Vova Feldman (@svovaf)
13061 * @since 1.0.2
13062 *
13063 * @param string $plan Plan name
13064 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
13065 *
13066 * @return bool
13067 */
13068 function is_plan( $plan, $exact = false ) {
13069 $this->_logger->entrance();
13070
13071 if ( ! $this->is_registered() ) {
13072 return false;
13073 }
13074
13075 $plan = strtolower( $plan );
13076
13077 $current_plan_name = $this->get_plan_name();
13078
13079 if ( $current_plan_name === $plan ) {
13080 // Exact plan.
13081 return true;
13082 } else if ( $exact ) {
13083 // Required exact, but plans are different.
13084 return false;
13085 }
13086
13087 $current_plan_order = - 1;
13088 $required_plan_order = PHP_INT_MAX;
13089 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
13090 if ( $plan === $this->_plans[ $i ]->name ) {
13091 $required_plan_order = $i;
13092 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
13093 $current_plan_order = $i;
13094 }
13095 }
13096
13097 return ( $current_plan_order > $required_plan_order );
13098 }
13099
13100 /**
13101 * Check if module has only one plan.
13102 *
13103 * @author Vova Feldman (@svovaf)
13104 * @since 1.2.1.7
13105 *
13106 * @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.
13107 *
13108 * @return bool
13109 */
13110 function is_single_plan( $double_check = false ) {
13111 $this->_logger->entrance();
13112
13113 if ( ! $this->is_registered() ||
13114 ! is_array( $this->_plans ) ||
13115 0 === count( $this->_plans )
13116 ) {
13117 return true;
13118 }
13119
13120 $has_free_plan = $this->has_free_plan();
13121
13122 if ( ! $has_free_plan && $double_check ) {
13123 foreach ( $this->_plans as $plan ) {
13124 if ( $plan->is_free() ) {
13125 $has_free_plan = true;
13126 break;
13127 }
13128 }
13129 }
13130
13131 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
13132 }
13133
13134 /**
13135 * Check if plan based on trial. If not in trial mode, should return false.
13136 *
13137 * @since 1.0.9
13138 *
13139 * @param string $plan Plan name
13140 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
13141 *
13142 * @return bool
13143 */
13144 function is_trial_plan( $plan, $exact = false ) {
13145 $this->_logger->entrance();
13146
13147 if ( ! $this->is_registered() ) {
13148 return false;
13149 }
13150
13151 if ( ! $this->is_trial() ) {
13152 return false;
13153 }
13154
13155 $trial_plan = $this->get_trial_plan();
13156
13157 if ( $trial_plan->name === $plan ) {
13158 // Exact plan.
13159 return true;
13160 } else if ( $exact ) {
13161 // Required exact, but plans are different.
13162 return false;
13163 }
13164
13165 $current_plan_order = - 1;
13166 $required_plan_order = - 1;
13167 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
13168 if ( $plan === $this->_plans[ $i ]->name ) {
13169 $required_plan_order = $i;
13170 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
13171 $current_plan_order = $i;
13172 }
13173 }
13174
13175 return ( $current_plan_order > $required_plan_order );
13176 }
13177
13178 /**
13179 * Check if plugin has any paid plans.
13180 *
13181 * @author Vova Feldman (@svovaf)
13182 * @since 1.0.7
13183 *
13184 * @return bool
13185 */
13186 function has_paid_plan() {
13187 return $this->_has_paid_plans ||
13188 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
13189 }
13190
13191 /**
13192 * Check if plugin has any plan with a trail.
13193 *
13194 * @author Vova Feldman (@svovaf)
13195 * @since 1.0.9
13196 *
13197 * @return bool
13198 */
13199 function has_trial_plan() {
13200 /**
13201 * @author Vova Feldman(@svovaf)
13202 * @since 1.2.1.5
13203 *
13204 * Allow setting a trial from the SDK without calling the API.
13205 * But, if the user did opt-in, continue using the real data from the API.
13206 */
13207 if ( $this->_trial_days >= 0 ) {
13208 return true;
13209 }
13210
13211 return $this->_storage->get( 'has_trial_plan', false );
13212 }
13213
13214 /**
13215 * Check if plugin has any free plan, or is it premium only.
13216 *
13217 * Note: If no plans configured, assume plugin is free.
13218 *
13219 * @author Vova Feldman (@svovaf)
13220 * @since 1.0.7
13221 *
13222 * @return bool
13223 */
13224 function has_free_plan() {
13225 return ! $this->is_only_premium();
13226 }
13227
13228 /**
13229 * Displays a license activation dialog box when the user clicks on the "Activate License"
13230 * or "Change License" link on the plugins
13231 * page.
13232 *
13233 * @author Leo Fajardo (@leorw)
13234 * @since 1.1.9
13235 */
13236 function _add_license_activation_dialog_box() {
13237 $vars = array(
13238 'id' => $this->_module_id,
13239 );
13240
13241 fs_require_template( 'forms/license-activation.php', $vars );
13242 fs_require_template( 'forms/resend-key.php', $vars );
13243 }
13244
13245 /**
13246 * Displays an email address update dialog box when the user clicks on the email address "Edit" button on the "Account" page.
13247 *
13248 * @author Leo Fajardo (@leorw)
13249 * @since 2.5.0
13250 */
13251 function _add_email_address_update_dialog_box() {
13252 $vars = array( 'id' => $this->_module_id );
13253
13254 fs_require_template( 'forms/email-address-update.php', $vars );
13255 }
13256
13257 /**
13258 * @author Leo Fajardo (@leorw)
13259 * @since 2.5.0
13260 */
13261 function _add_email_address_update_option() {
13262 if ( ! $this->should_handle_user_change() ) {
13263 return;
13264 }
13265
13266 // Add email address update AJAX handler.
13267 $this->add_ajax_action( 'update_email_address', array( &$this, '_email_address_update_ajax_handler' ) );
13268 }
13269
13270 /**
13271 * @author Leo Fajardo (@leorw)
13272 * @since 2.5.0
13273 */
13274 function _email_address_update_ajax_handler() {
13275 $this->check_ajax_referer( 'update_email_address' );
13276
13277 $new_email_address = fs_request_get( 'email_address' );
13278 $transfer_type = fs_request_get( 'transfer_type' );
13279
13280 $result = $this->update_email( $new_email_address );
13281
13282 if ( ! FS_Api::is_api_error( $result ) ) {
13283 self::shoot_ajax_success();
13284 }
13285
13286 $error = '';
13287
13288 if ( FS_Api::is_api_error_object( $result ) ) {
13289 switch ( $result->error->code ) {
13290 case 'user_exist':
13291 case 'account_verification_required':
13292 $error = array(
13293 'code' => 'change_ownership',
13294 'url' => $this->get_account_url( 'change_owner', array(
13295 'state' => 'init',
13296 'candidate_email' => $new_email_address,
13297 'transfer_type' => $transfer_type,
13298 ) ),
13299 );
13300
13301 break;
13302 }
13303 }
13304
13305 if ( empty( $error ) ) {
13306 $error = is_object( $result ) ?
13307 var_export( $result->error, true ) :
13308 $result;
13309 }
13310
13311 self::shoot_ajax_failure( $error );
13312 }
13313
13314 /**
13315 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
13316 *
13317 * @author Leo Fajardo (@leorw)
13318 * @since 2.3.2
13319 *
13320 * @return number[]
13321 */
13322 function get_installs_ids_with_foreign_licenses() {
13323 $installs = array();
13324
13325 if (
13326 is_object( $this->_license ) &&
13327 $this->_site->user_id != $this->_license->user_id
13328 ) {
13329 $installs[] = $this->_site->id;
13330 }
13331
13332 /**
13333 * Also try to get foreign licenses for the context product's add-ons.
13334 */
13335 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
13336
13337 foreach ( $installs_by_slug_map as $slug => $install_info ) {
13338 if ( $slug == $this->get_slug() ) {
13339 continue;
13340 }
13341
13342 $install = $install_info['install'];
13343 $license = $install_info['license'];
13344
13345 if (
13346 is_object( $license ) &&
13347 $install->user_id != $license->user_id
13348 ) {
13349 $installs[] = $install->id;
13350 }
13351 }
13352
13353 return $installs;
13354 }
13355
13356 /**
13357 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
13358 *
13359 * @author Leo Fajardo (@leorw)
13360 * @since 2.3.2
13361 *
13362 * @param number[] $install_ids
13363 */
13364 function _add_user_change_dialog_box( $install_ids ) {
13365 $vars = array(
13366 'id' => $this->_module_id,
13367 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
13368 );
13369
13370 fs_require_template( 'forms/user-change.php', $vars );
13371 }
13372
13373 /**
13374 * @author Leo Fajardo (@leorw)
13375 * @since 2.3.1
13376 */
13377 function _add_data_debug_mode_dialog_box() {
13378 $vars = array(
13379 'id' => $this->_module_id,
13380 );
13381
13382 fs_require_template( 'forms/data-debug-mode.php', $vars );
13383 }
13384
13385 /**
13386 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
13387 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
13388 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
13389 * is only activated on a single production site.
13390 *
13391 * @author Leo Fajardo (@leorw)
13392 * @since 2.2.1
13393 *
13394 * @param bool $is_license_deactivation
13395 *
13396 * @return array
13397 */
13398 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
13399 if ( fs_is_network_admin() ) {
13400 // Subscription cancellation dialog box is currently not supported for multisite networks.
13401 return array();
13402 }
13403
13404 if ( $this->is_whitelabeled() ) {
13405 return array();
13406 }
13407
13408 $license = $this->_get_license();
13409
13410 /**
13411 * 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.
13412 *
13413 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
13414 * @since 2.2.1
13415 */
13416 if ( ! is_object( $license ) ||
13417 $license->is_lifetime() ||
13418 ( ! $license->is_single_site() && $license->activated > 1 )
13419 ) {
13420 return array();
13421 }
13422
13423 /**
13424 * @var FS_Subscription $subscription
13425 */
13426 $subscription = $this->_get_subscription( $license->id );
13427 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
13428 return array();
13429 }
13430
13431 return array(
13432 'id' => $this->_module_id,
13433 'license' => $license,
13434 'has_trial' => $this->is_paid_trial(),
13435 'is_license_deactivation' => $is_license_deactivation,
13436 );
13437 }
13438
13439 /**
13440 * @author Leo Fajardo (@leorw)
13441 * @since 2.0.2
13442 */
13443 function _add_premium_version_upgrade_selection_dialog_box() {
13444 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
13445 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
13446 return;
13447 }
13448
13449 $vars = array(
13450 'id' => $this->_module_id,
13451 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
13452 $modules_update->response[ $this->_plugin_basename ]->new_version :
13453 $modules_update->response[ $this->_plugin_basename ]['new_version']
13454 );
13455
13456 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13457 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13458 }
13459
13460 /**
13461 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13462 * page.
13463 *
13464 * @author Leo Fajardo (@leorw)
13465 * @since 1.2.1.5
13466 */
13467 function _add_optout_dialog() {
13468 if ( $this->is_theme() ) {
13469 $vars = null;
13470 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13471 }
13472
13473 $vars = array( 'id' => $this->_module_id );
13474 fs_require_template( 'forms/optout.php', $vars );
13475 }
13476
13477 /**
13478 * Prepare page to include all required UI and logic for the license activation dialog.
13479 *
13480 * @author Vova Feldman (@svovaf)
13481 * @since 1.2.0
13482 */
13483 function _add_license_activation() {
13484 if ( $this->is_migration() ) {
13485 return;
13486 }
13487
13488 if ( ! $this->is_user_admin() ) {
13489 // Only admins can activate a license.
13490 return;
13491 }
13492
13493 if ( ! $this->has_paid_plan() ) {
13494 // Module doesn't have any paid plans.
13495 return;
13496 }
13497
13498 if (
13499 $this->has_premium_version() &&
13500 ! $this->is_premium() &&
13501 /**
13502 * Also handle the case when an upgrade was made using the free version.
13503 *
13504 * @author Leo Fajardo (@leorw)
13505 * @since 2.3.2
13506 */
13507 ! is_object( $this->_get_license() )
13508 ) {
13509 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13510 return;
13511 }
13512
13513 // Add license activation link and AJAX request handler.
13514 if ( self::is_plugins_page() ) {
13515 $is_network_admin = fs_is_network_admin();
13516
13517 if (
13518 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13519 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13520 ) {
13521 if (
13522 $this->is_premium() ||
13523 ( $this->has_paid_plan() && ! $this->has_premium_version() )
13524 ) {
13525 /**
13526 * @since 1.2.0 Add license action link only on plugins page.
13527 */
13528 $this->_add_license_action_link();
13529 }
13530 }
13531 }
13532
13533 // Add license activation AJAX callback.
13534 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13535
13536 // Add resend license AJAX callback.
13537 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13538 }
13539
13540 /**
13541 * Prepares page to include all required UI and logic for the "Change User" dialog.
13542 *
13543 * @author Leo Fajardo (@leorw)
13544 * @since 2.3.2
13545 */
13546 function _add_user_change_option() {
13547 if ( ! $this->should_handle_user_change() ) {
13548 return;
13549 }
13550
13551 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13552
13553 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13554 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13555 return;
13556 }
13557
13558 // Add user change AJAX handler.
13559 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13560 }
13561
13562 /**
13563 * @author Leo Fajardo (@leorw)
13564 * @since 2.3.2
13565 */
13566 function should_handle_user_change() {
13567 if ( ! $this->is_user_admin() ) {
13568 // Only admins can change user.
13569 return false;
13570 }
13571
13572 if ( $this->is_addon() ) {
13573 return false;
13574 }
13575
13576 if ( ! $this->is_registered() ) {
13577 return false;
13578 }
13579
13580 if (
13581 $this->is_network_active() &&
13582 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13583 ) {
13584 // Handle only on site-level "Account" section for now.
13585 return false;
13586 }
13587
13588 return true;
13589 }
13590
13591 /**
13592 * @author Leo Fajardo (@leorw)
13593 * @since 2.0.2
13594 */
13595 function _add_premium_version_upgrade_selection() {
13596 if ( ! $this->is_user_admin() ) {
13597 return;
13598 }
13599
13600 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13601 // This is relevant only to the free versions and premium versions without an active license.
13602 return;
13603 }
13604
13605 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13606 $this->_add_premium_version_upgrade_selection_action();
13607 }
13608 }
13609
13610 /**
13611 * @author Edgar Melkonyan
13612 * @since 2.4.1
13613 *
13614 * @throws Freemius_Exception
13615 */
13616 function _toggle_whitelabel_mode_ajax_handler() {
13617 $this->_logger->entrance();
13618
13619 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13620
13621 if ( ! $this->is_user_admin() ) {
13622 // Only for admins.
13623 self::shoot_ajax_failure();
13624 }
13625
13626 $license = $this->get_api_user_scope()->call(
13627 "/licenses/{$this->_site->license_id}.json",
13628 'put',
13629 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13630 );
13631
13632 if ( ! $this->is_api_result_entity( $license ) ) {
13633 self::shoot_ajax_failure(
13634 FS_Api::is_api_error_object( $license ) ?
13635 $license->error->message :
13636 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13637 );
13638 }
13639
13640 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13641 $this->_store_licenses();
13642
13643 $this->_sync_license();
13644
13645 if ( ! $license->is_whitelabeled ) {
13646 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13647 } else {
13648 $this->_admin_notices->add_sticky(
13649 sprintf(
13650 $this->get_text_inline(
13651 '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.',
13652 'license_whitelabeled'
13653 ),
13654 "<strong>{$this->get_plugin_title()}</strong>",
13655 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13656 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13657 ),
13658 'license_whitelabeled'
13659 );
13660 }
13661
13662 self::shoot_ajax_response( array( 'success' => true ) );
13663 }
13664
13665 /**
13666 * @author Leo Fajardo (@leorw)
13667 * @since 2.3.0
13668 */
13669 function _add_beta_mode_update_handler() {
13670 if ( ! $this->is_user_admin() ) {
13671 return;
13672 }
13673
13674 if ( ! $this->is_premium() ) {
13675 return;
13676 }
13677
13678 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13679 }
13680
13681 /**
13682 * @author Leo Fajardo (@leorw)
13683 * @since 2.3.0
13684 */
13685 function _set_beta_mode_ajax_handler() {
13686 $this->_logger->entrance();
13687
13688 $this->check_ajax_referer( 'set_beta_mode' );
13689
13690 if ( ! $this->is_user_admin() ) {
13691 // Only for admins.
13692 self::shoot_ajax_failure();
13693 }
13694
13695 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13696
13697 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13698 self::shoot_ajax_failure();
13699 }
13700
13701 $site = $this->api_site_call(
13702 '',
13703 'put',
13704 array(
13705 'is_beta' => ( 'true' == $is_beta ),
13706 'fields' => 'is_beta'
13707 )
13708 );
13709
13710 if ( ! $this->is_api_result_entity( $site ) ) {
13711 self::shoot_ajax_failure(
13712 FS_Api::is_api_error_object( $site ) ?
13713 $site->error->message :
13714 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13715 );
13716 }
13717
13718 $this->_site->is_beta = $site->is_beta;
13719 $this->_store_site();
13720
13721 self::shoot_ajax_response( array( 'success' => true ) );
13722 }
13723
13724 /**
13725 * License activation WP AJAX handler.
13726 *
13727 * @author Leo Fajardo (@leorw)
13728 * @since 1.1.9
13729 *
13730 * @uses Freemius::activate_license()
13731 */
13732 function _activate_license_ajax_action() {
13733 $this->_logger->entrance();
13734
13735 $this->check_ajax_referer( 'activate_license' );
13736
13737 $license_key = trim( fs_request_get_raw( 'license_key' ) );
13738
13739 if ( empty( $license_key ) ) {
13740 exit;
13741 }
13742
13743 $sites = fs_is_network_admin() ?
13744 fs_request_get( 'sites', array(), 'post' ) :
13745 array();
13746
13747 $result = $this->activate_license(
13748 $license_key,
13749 $sites,
13750 fs_request_get_bool( 'is_marketing_allowed', null ),
13751 fs_request_get( 'blog_id', null ),
13752 fs_request_get( 'module_id', null, 'post' ),
13753 fs_request_get( 'user_id', null ),
13754 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
13755 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null )
13756 );
13757
13758 if (
13759 $result['success'] &&
13760 $this->is_bundle_license_auto_activation_enabled()
13761 ) {
13762 $license = new FS_Plugin_License();
13763 $license->secret_key = $license_key;
13764
13765 $this->maybe_activate_bundle_license( $license, $sites );
13766 }
13767
13768 echo json_encode( $result );
13769
13770 exit;
13771 }
13772
13773 /**
13774 * User change WP AJAX handler.
13775 *
13776 * @author Leo Fajardo (@leorw)
13777 * @since 2.3.2
13778 */
13779 function _user_change_ajax_action() {
13780 $this->_logger->entrance();
13781
13782 $this->check_ajax_referer( 'change_user' );
13783
13784 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13785 $new_user_id = fs_request_get( 'user_id' );
13786
13787 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13788 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13789 }
13790
13791 $params = array();
13792
13793 if ( ! empty( $new_email_address ) ) {
13794 $params['user_email'] = $new_email_address;
13795 } else {
13796 $params['user_id'] = $new_user_id;
13797 }
13798
13799 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13800 $install_ids = array();
13801
13802 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13803 $install_ids[ $slug ] = $install_info['install']->id;
13804 }
13805
13806 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13807
13808 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13809
13810 if ( FS_Api::is_api_error( $install ) ) {
13811 $error = '';
13812
13813 if ( is_object( $install ) ) {
13814 switch ( $install->error->code ) {
13815 case 'user_exist':
13816 $error = (
13817 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13818 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
13819 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>' ) .
13820 sprintf(
13821 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13822 $this->get_account_url( 'change_owner', array(
13823 'state' => 'init',
13824 'candidate_email' => $new_email_address
13825 ) ),
13826 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
13827 )
13828 );
13829 break;
13830 }
13831 }
13832
13833 if ( empty( $error ) ) {
13834 $error = FS_Api::is_api_error_object( $install ) ?
13835 $install->error->message :
13836 var_export( $install->error, true );
13837 }
13838
13839 self::shoot_ajax_failure( $error );
13840 } else {
13841 if (
13842 // If successful ownership change.
13843 $this->get_user()->id != $install->user_id ||
13844 ! empty( $new_email_address )
13845 ) {
13846 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
13847 }
13848 }
13849
13850 self::shoot_ajax_success();
13851 }
13852
13853 /**
13854 * @author Leo Fajardo (@leorw)
13855 * @since 2.3.2.14
13856 */
13857 function starting_migration() {
13858 if ( ! empty( $this->_storage->license_migration ) ) {
13859 // Do not overwrite the data if already set.
13860 return;
13861 }
13862
13863 $this->_storage->license_migration = array(
13864 'is_migrating' => true,
13865 'start_timestamp' => time()
13866 );
13867 }
13868
13869 /**
13870 * @author Leo Fajardo (@leorw)
13871 * @since 2.3.2.14
13872 */
13873 function is_migration() {
13874 if ( $this->is_addon() ) {
13875 return $this->get_parent_instance()->is_migration();
13876 }
13877
13878 if ( empty( $this->_storage->license_migration ) ) {
13879 return false;
13880 }
13881
13882 if ( ! $this->_storage->license_migration['is_migrating'] ) {
13883 return false;
13884 }
13885
13886 return (
13887 // Return `true` if the migration is within 5 minutes from the starting time.
13888 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
13889 );
13890 }
13891
13892 /**
13893 *
13894 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
13895 *
13896 * @author Vova Feldman (@svovaf)
13897 * @since 2.3.0
13898 *
13899 * @param string $license_key
13900 * @param null|bool $is_marketing_allowed
13901 * @param null|number $plugin_id
13902 * @param array $sites
13903 * @param int $blog_id
13904 *
13905 * @return array {
13906 * @var bool $success
13907 * @var string $error
13908 * @var string $next_page
13909 * }
13910 *
13911 * @uses Freemius::activate_license()
13912 */
13913 function activate_migrated_license(
13914 $license_key,
13915 $is_marketing_allowed = null,
13916 $plugin_id = null,
13917 $sites = array(),
13918 $blog_id = null
13919 ) {
13920 $this->_logger->entrance();
13921
13922 $result = $this->activate_license(
13923 $license_key,
13924 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
13925 $this->get_sites_for_network_level_optin() :
13926 $sites,
13927 $is_marketing_allowed,
13928 $blog_id,
13929 $plugin_id
13930 );
13931
13932 // No need to show the sticky after license activation notice after migrating a license.
13933 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13934
13935 return $result;
13936 }
13937
13938 /**
13939 * @author Leo Fajardo (@leorw)
13940 * @since 2.3.1
13941 *
13942 * @return string
13943 */
13944 function get_pricing_js_path() {
13945 if ( ! isset( $this->_pricing_js_path ) ) {
13946 $pricing_js_path = $this->apply_filters( 'freemius_pricing_js_path', '' );
13947
13948 if ( empty( $pricing_js_path ) ) {
13949 global $fs_active_plugins;
13950
13951 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
13952 if ( $data->plugin_path == $this->get_plugin_basename() ) {
13953 $plugin_or_theme_root_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
13954
13955 $pricing_js_path = $plugin_or_theme_root_dir
13956 . '/'
13957 // The basename will be `plugins`, `themes`, or the basename of a custom plugins or themes directory.
13958 . str_replace( '../' . basename( $plugin_or_theme_root_dir ) . '/', '', $sdk_path )
13959 . '/includes/freemius-pricing/freemius-pricing.js';
13960
13961 break;
13962 }
13963 }
13964 }
13965
13966 $this->_pricing_js_path = $pricing_js_path;
13967 }
13968
13969 return $this->_pricing_js_path;
13970 }
13971
13972 /**
13973 * @author Leo Fajardo (@leorw)
13974 * @since 2.3.1
13975 *
13976 * @return bool
13977 */
13978 function should_use_external_pricing() {
13979 if ( is_null( $this->_use_external_pricing ) ) {
13980 $pricing_js_path = $this->get_pricing_js_path();
13981
13982 $this->_use_external_pricing = ( empty( $pricing_js_path ) || ! file_exists( $pricing_js_path ) );
13983 }
13984
13985 return $this->_use_external_pricing;
13986 }
13987
13988 /**
13989 * The implementation of this method was previously in `_activate_license_ajax_action()`.
13990 *
13991 * @author Vova Feldman (@svovaf)
13992 * @since 2.2.4
13993 * @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).
13994 * @param string $license_key
13995 * @param array $sites
13996 * @param null|bool $is_marketing_allowed
13997 * @param null|int $blog_id
13998 * @param null|number $plugin_id
13999 * @param null|number $license_owner_id
14000 * @param bool|null $is_extensions_tracking_allowed
14001 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint.
14002 *
14003 *
14004 * @return array {
14005 * @var bool $success
14006 * @var string $error
14007 * @var string $next_page
14008 * }
14009 */
14010 private function activate_license(
14011 $license_key,
14012 $sites = array(),
14013 $is_marketing_allowed = null,
14014 $blog_id = null,
14015 $plugin_id = null,
14016 $license_owner_id = null,
14017 $is_extensions_tracking_allowed = null,
14018 $is_diagnostic_tracking_allowed = null
14019 ) {
14020 $this->_logger->entrance();
14021
14022 $license_key = trim( $license_key );
14023
14024 $is_network_activation_or_migration = (
14025 fs_is_network_admin() ||
14026 ( ! empty( $sites ) && $this->is_migration() )
14027 );
14028
14029 if ( ! $is_network_activation_or_migration ) {
14030 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
14031 $sites = array();
14032 }
14033
14034 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
14035 $this :
14036 $this->get_addon_instance( $plugin_id );
14037
14038 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
14039 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
14040 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
14041 ) );
14042
14043 $error = false;
14044 $next_page = false;
14045
14046 $has_valid_blog_id = is_numeric( $blog_id );
14047
14048 $user = null;
14049
14050 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
14051 /**
14052 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
14053 *
14054 * @author Vova Feldman (@svovaf)
14055 */
14056 $user = $fs->get_parent_instance()->get_current_or_network_user();
14057 } else if ( $fs->is_registered() ) {
14058 $user = $fs->get_current_or_network_user();
14059 }
14060
14061 if ( $has_valid_blog_id ) {
14062 /**
14063 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
14064 *
14065 * @author Leo Fajardo (@leorw)
14066 */
14067 $fs->switch_to_blog( $blog_id );
14068 }
14069
14070 if ( is_object( $user ) ) {
14071 $result = true;
14072
14073 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
14074 // If no specific blog ID was provided, activate the license for all sites in the network.
14075 $blog_2_install_map = array();
14076 $site_ids = array();
14077
14078 foreach ( $sites as $site ) {
14079 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
14080 continue;
14081 }
14082
14083 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
14084
14085 if ( is_object( $install ) ) {
14086 $blog_2_install_map[ $site['blog_id'] ] = $install;
14087 } else {
14088 $site_ids[] = $site['blog_id'];
14089 }
14090 }
14091
14092 if ( ! empty( $blog_2_install_map ) ) {
14093 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
14094 }
14095
14096 if ( true === $result && ! empty( $site_ids ) ) {
14097 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
14098 }
14099 } else {
14100 if ( $fs->is_registered() ) {
14101 $params = array(
14102 'license_key' => $fs->apply_filters( 'license_key', $license_key )
14103 );
14104
14105 $install_ids = array();
14106
14107 $change_owner = FS_User::is_valid_id( $license_owner_id );
14108
14109 if ( $change_owner ) {
14110 $params['user_id'] = $license_owner_id;
14111
14112 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
14113
14114 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
14115 $install_ids[ $slug ] = $install_info['install']->id;
14116 }
14117
14118 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
14119 }
14120
14121 $api = $fs->get_api_site_scope();
14122
14123 $result = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
14124
14125 if ( ! FS_Api::is_api_error( $result ) ) {
14126 $install = $result;
14127
14128 $fs->reconnect_locally( $has_valid_blog_id );
14129
14130 if (
14131 $change_owner &&
14132 // If successful ownership change.
14133 $fs->get_user()->id != $install->user_id
14134 ) {
14135 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
14136 }
14137 }
14138 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
14139 $result = $fs->activate_license_on_site( $user, $license_key );
14140 }
14141 }
14142
14143 if ( true !== $result && ! FS_Api::is_api_result_entity( $result ) ) {
14144 if ( FS_Api::is_blocked( $result ) ) {
14145 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
14146 }
14147
14148 $error = FS_Api::is_api_error_object( $result ) ?
14149 $result->error->message :
14150 var_export( $result, true );
14151 } else {
14152 $fs->network_upgrade_mode_completed();
14153
14154 $fs->_user = $user;
14155
14156 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
14157 $fs->_site = $fs->get_network_install();
14158 }
14159
14160 $fs->_sync_license( true, $has_valid_blog_id );
14161
14162 $this->maybe_sync_install_user();
14163
14164 $next_page = $fs->is_addon() ?
14165 $fs->get_parent_instance()->get_account_url() :
14166 $fs->get_after_activation_url( 'after_connect_url' );
14167 }
14168 } else {
14169 $next_page = $fs->opt_in(
14170 false,
14171 false,
14172 false,
14173 $license_key,
14174 false,
14175 false,
14176 false,
14177 $is_marketing_allowed,
14178 $sites
14179 );
14180
14181 if ( isset( $next_page->error ) ) {
14182 $error = $next_page->error;
14183 } else {
14184 if ( $is_network_activation_or_migration ) {
14185 /**
14186 * Get the list of sites that were just opted-in (and license activated).
14187 * This is an optimization for the next part below saving some DB queries.
14188 */
14189 $connected_sites = array();
14190 foreach ( $sites as $site ) {
14191 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
14192 $connected_sites[ $site['blog_id'] ] = true;
14193 }
14194 }
14195
14196 $all_sites = self::get_sites();
14197 $pending_blog_ids = array();
14198
14199 /**
14200 * 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.
14201 *
14202 * @author Vova Feldman (@svovaf)
14203 */
14204 foreach ( $all_sites as $site ) {
14205 $blog_id = self::get_site_blog_id( $site );
14206
14207 if ( isset( $connected_sites[ $blog_id ] ) ) {
14208 // Site was just connected.
14209 continue;
14210 }
14211
14212 if ( $fs->is_installed_on_site( $blog_id ) ) {
14213 // Site was already connected before.
14214 continue;
14215 }
14216
14217 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
14218 // Site's connection was delegated.
14219 continue;
14220 }
14221
14222 if ( $fs->is_anonymous_site( $blog_id ) ) {
14223 // Site connection was already skipped.
14224 continue;
14225 }
14226
14227 $pending_blog_ids[] = $blog_id;
14228 }
14229
14230 if ( ! empty( $pending_blog_ids ) ) {
14231 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
14232 $fs->skip_connection( $pending_blog_ids );
14233 } else {
14234 $fs->delegate_connection( $pending_blog_ids );
14235 }
14236 }
14237 }
14238 }
14239 }
14240
14241 if ( false === $error && true === $fs->_storage->require_license_activation ) {
14242 $fs->_storage->require_license_activation = false;
14243 }
14244
14245 $result = array(
14246 'success' => ( false === $error )
14247 );
14248
14249 if ( false !== $error ) {
14250 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
14251 } else {
14252 if ( $fs->is_addon() || $fs->has_addons() ) {
14253 /**
14254 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
14255 * an updated valid user licenses collection will be fetched from the server which is used to also
14256 * update the account add-ons (add-ons the user has licenses for).
14257 *
14258 * @author Leo Fajardo (@leorw)
14259 * @since 2.2.4
14260 */
14261 $fs->purge_valid_user_licenses_cache();
14262 }
14263
14264 $result['next_page'] = $next_page;
14265 }
14266
14267 return $result;
14268 }
14269
14270 /**
14271 * @author Leo Fajardo (@leorw)
14272 * @since 2.3.2
14273 *
14274 * @return array {
14275 * @key string Product slug.
14276 * @value array {
14277 * @property FS_Site $site
14278 * @property FS_Plugin_License $license
14279 * }
14280 * }
14281 */
14282 private function get_parent_and_addons_installs_info() {
14283 $fs = $this->is_addon() ?
14284 $this->get_parent_instance() :
14285 $this;
14286
14287 $installed_addons_ids = array();
14288
14289 $installed_addons_instances = $fs->get_installed_addons();
14290 foreach ( $installed_addons_instances as $instance ) {
14291 $installed_addons_ids[] = $instance->get_id();
14292 }
14293
14294 $addons_ids = array_unique( array_merge(
14295 $installed_addons_ids,
14296 $fs->get_updated_account_addons()
14297 ) );
14298
14299 // Add parent product info.
14300 $installs_info_by_slug_map = array(
14301 $fs->get_slug() => array(
14302 'install' => $fs->get_site(),
14303 'license' => $fs->_get_license()
14304 )
14305 );
14306
14307 foreach ( $addons_ids as $addon_id ) {
14308 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
14309
14310 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
14311
14312 if ( ! isset( $addon_info['is_connected'] ) || ! $addon_info['is_connected'] ) {
14313 // Add-on is not associated with an install entity.
14314 continue;
14315 }
14316
14317 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
14318 'install' => $addon_info['site'],
14319 'license' => isset( $addon_info['license'] ) ?
14320 $addon_info['license'] :
14321 null
14322 );
14323 }
14324
14325 return $installs_info_by_slug_map;
14326 }
14327
14328 /**
14329 * @author Leo Fajardo (@leorw)
14330 * @since 1.2.3.1
14331 */
14332 function _network_activate_ajax_action() {
14333 $this->_logger->entrance();
14334
14335 $this->check_ajax_referer( 'network_activate' );
14336
14337 $plugin_id = fs_request_get( 'module_id', '', 'post' );
14338 $fs = ( $plugin_id == $this->_module_id ) ?
14339 $this :
14340 $this->get_addon_instance( $plugin_id );
14341
14342 $error = false;
14343
14344 $sites = fs_request_get( 'sites', array(), 'post' );
14345 if ( is_array( $sites ) && ! empty( $sites ) ) {
14346 $sites_by_action = array(
14347 'allow' => array(),
14348 'delegate' => array(),
14349 'skip' => array()
14350 );
14351
14352 foreach ( $sites as $site ) {
14353 $sites_by_action[ $site['action'] ][] = $site;
14354 }
14355
14356 $total_sites = count( $sites );
14357 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
14358
14359 $next_page = '';
14360
14361 $has_any_install = fs_request_get_bool( 'has_any_install' );
14362
14363 if ( $total_sites === $total_sites_to_delegate &&
14364 ! $this->is_network_upgrade_mode() &&
14365 ! $has_any_install
14366 ) {
14367 $this->delegate_connection();
14368 } else {
14369 if ( ! empty( $sites_by_action['delegate'] ) ) {
14370 $this->delegate_connection( self::get_sites_blog_ids( $sites_by_action['delegate'] ) );
14371 }
14372
14373 if ( ! empty( $sites_by_action['skip'] ) ) {
14374 $this->skip_connection( self::get_sites_blog_ids( $sites_by_action['skip'] ) );
14375 }
14376
14377 if ( empty( $sites_by_action['allow'] ) ) {
14378 if ( $has_any_install ) {
14379 $first_install = $fs->find_first_install();
14380
14381 if ( ! is_null( $first_install ) ) {
14382 $fs->_site = $first_install['install'];
14383 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
14384
14385 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
14386 $fs->_storage->network_user_id = $fs->_user->id;
14387 }
14388 }
14389 } else {
14390 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
14391 $next_page = $fs->opt_in(
14392 false,
14393 false,
14394 false,
14395 false,
14396 false,
14397 false,
14398 false,
14399 fs_request_get_bool( 'is_marketing_allowed', null ),
14400 $sites_by_action['allow']
14401 );
14402 } else {
14403 $next_page = $fs->install_with_user(
14404 $this->get_network_user(),
14405 false,
14406 false,
14407 false,
14408 true,
14409 $sites_by_action['allow']
14410 );
14411 }
14412
14413 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
14414 $error = $next_page->error;
14415 }
14416 }
14417 }
14418
14419 if ( empty( $next_page ) ) {
14420 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
14421 }
14422 } else {
14423 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
14424 }
14425
14426 $result = array(
14427 'success' => ( false === $error )
14428 );
14429
14430 if ( false !== $error ) {
14431 $result['error'] = $error;
14432 } else {
14433 $result['next_page'] = $next_page;
14434 }
14435
14436 echo json_encode( $result );
14437
14438 exit;
14439 }
14440
14441 /**
14442 * Billing update AJAX callback.
14443 *
14444 * @author Vova Feldman (@svovaf)
14445 * @since 1.2.1.5
14446 */
14447 function _update_billing_ajax_action() {
14448 $this->_logger->entrance();
14449
14450 $this->check_ajax_referer( 'update_billing' );
14451
14452 if ( ! $this->is_user_admin() ) {
14453 // Only for admins.
14454 self::shoot_ajax_failure();
14455 }
14456
14457 $billing = fs_request_get( 'billing' );
14458
14459 $api = $this->get_api_user_scope();
14460 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
14461 'plugin_id' => $this->get_parent_id(),
14462 ) ) );
14463
14464 if ( ! $this->is_api_result_entity( $result ) ) {
14465 self::shoot_ajax_failure();
14466 }
14467
14468 // Purge cached billing.
14469 $this->get_api_user_scope()->purge_cache( 'billing.json' );
14470
14471 self::shoot_ajax_success();
14472 }
14473
14474 /**
14475 * Trial start for anonymous users (AJAX callback).
14476 *
14477 * @author Vova Feldman (@svovaf)
14478 * @since 1.2.1.5
14479 */
14480 function _start_trial_ajax_action() {
14481 $this->_logger->entrance();
14482
14483 $this->check_ajax_referer( 'start_trial' );
14484
14485 if ( ! $this->is_user_admin() ) {
14486 // Only for admins.
14487 self::shoot_ajax_failure();
14488 }
14489
14490 $trial_data = fs_request_get( 'trial' );
14491
14492 $next_page = $this->opt_in(
14493 false,
14494 false,
14495 false,
14496 false,
14497 false,
14498 $trial_data['plan_id']
14499 );
14500
14501 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14502 self::shoot_ajax_failure(
14503 isset( $next_page->error ) ?
14504 $next_page->error->message :
14505 var_export( $next_page, true )
14506 );
14507 }
14508
14509 $this->shoot_ajax_success( array(
14510 'next_page' => $next_page,
14511 ) );
14512 }
14513
14514 /**
14515 * @author Leo Fajardo (@leorw)
14516 * @since 1.2.0
14517 */
14518 function _resend_license_key_ajax_action() {
14519 $this->_logger->entrance();
14520
14521 $this->check_ajax_referer( 'resend_license_key' );
14522
14523 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14524
14525 if ( empty( $email_address ) ) {
14526 exit;
14527 }
14528
14529 $error = false;
14530
14531 $api = $this->get_api_plugin_scope();
14532 $result = $api->call( '/licenses/resend.json', 'post',
14533 array(
14534 'email' => $email_address,
14535 'url' => home_url(),
14536 )
14537 );
14538
14539 if ( is_object( $result ) && isset( $result->error ) ) {
14540 $error = $result->error;
14541
14542 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14543 $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' );
14544 } else if ( 'no_license' === $error->code ) {
14545 $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' );
14546 } else {
14547 $error = $error->message;
14548 }
14549 }
14550
14551 $licenses = array(
14552 'success' => ( false === $error )
14553 );
14554
14555 if ( false !== $error ) {
14556 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14557 }
14558
14559 echo json_encode( $licenses );
14560
14561 exit;
14562 }
14563
14564 /**
14565 * @author Vova Feldman (@svovaf)
14566 * @since 1.2.1.8
14567 *
14568 * @var string
14569 */
14570 private static $_pagenow;
14571
14572 /**
14573 * Get current page or the referer if executing a WP AJAX request.
14574 *
14575 * @author Vova Feldman (@svovaf)
14576 * @since 1.2.1.8
14577 *
14578 * @return string
14579 */
14580 static function get_current_page() {
14581 if ( ! isset( self::$_pagenow ) ) {
14582 global $pagenow;
14583 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14584 /**
14585 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14586 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14587 *
14588 * @author Leo Fajardo (@leorw)
14589 * @since 2.2.3
14590 */
14591 if ( is_network_admin() ) {
14592 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14593 } else if ( is_user_admin() ) {
14594 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14595 } else {
14596 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14597 }
14598
14599 $pagenow = $self_matches[1];
14600 $pagenow = trim( $pagenow, '/' );
14601 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14602 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14603 $pagenow = 'index.php';
14604 } else {
14605 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14606 $pagenow = strtolower( $self_matches[1] );
14607 if ( '.php' !== substr($pagenow, -4, 4) )
14608 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14609 }
14610 }
14611
14612 self::$_pagenow = $pagenow;
14613
14614 if ( self::is_ajax() &&
14615 'admin-ajax.php' === $pagenow
14616 ) {
14617 $referer = fs_get_raw_referer();
14618
14619 if ( is_string( $referer ) ) {
14620 $parts = explode( '?', $referer );
14621
14622 self::$_pagenow = basename( $parts[0] );
14623 }
14624 }
14625 }
14626
14627 return self::$_pagenow;
14628 }
14629
14630 /**
14631 * Helper method to check if user in the plugins page.
14632 *
14633 * @author Vova Feldman (@svovaf)
14634 * @since 1.2.1.5
14635 *
14636 * @return bool
14637 */
14638 static function is_plugins_page() {
14639 return ( 'plugins.php' === self::get_current_page() );
14640 }
14641
14642 /**
14643 * @author Leo Fajardo (@leorw)
14644 * @since 2.2.3
14645 *
14646 * @return bool
14647 */
14648 static function is_plugin_install_page() {
14649 return ( 'plugin-install.php' === self::get_current_page() );
14650 }
14651
14652 /**
14653 * @author Leo Fajardo (@leorw)
14654 * @since 2.0.2
14655 *
14656 * @return bool
14657 */
14658 static function is_updates_page() {
14659 return ( 'update-core.php' === self::get_current_page() );
14660 }
14661
14662 /**
14663 * Helper method to check if user in the themes page.
14664 *
14665 * @author Vova Feldman (@svovaf)
14666 * @since 1.2.2.6
14667 *
14668 * @return bool
14669 */
14670 static function is_themes_page() {
14671 return ( 'themes.php' === self::get_current_page() );
14672 }
14673
14674 #----------------------------------------------------------------------------------
14675 #region Affiliation
14676 #----------------------------------------------------------------------------------
14677
14678 /**
14679 * @author Leo Fajardo (@leorw)
14680 * @since 1.2.3
14681 *
14682 * @return bool
14683 */
14684 function has_affiliate_program() {
14685 if ( ! is_object( $this->_plugin ) ) {
14686 return false;
14687 }
14688
14689 return $this->_plugin->has_affiliate_program();
14690 }
14691
14692 /**
14693 * Get Plugin ID under which we will track affiliate application.
14694 *
14695 * This could either be the Bundle ID or the main plugin ID.
14696 *
14697 * @return number Bundle ID if developer has provided one, else the main plugin ID.
14698 */
14699 private function get_plugin_id_for_affiliate_terms() {
14700 return $this->has_bundle_context() ?
14701 $this->get_bundle_id() :
14702 $this->_plugin->id;
14703 }
14704
14705 /**
14706 * @author Leo Fajardo (@leorw)
14707 * @since 1.2.4
14708 */
14709 private function fetch_affiliate_terms() {
14710 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14711 /**
14712 * In case we have a bundle set in SDK configuration, we would like to use that for affiliates, not the main plugin.
14713 */
14714 $plugins_api = $this->has_bundle_context() ?
14715 $this->get_api_bundle_scope() :
14716 $this->get_api_plugin_scope();
14717
14718 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14719
14720 /**
14721 * 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.
14722 */
14723 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14724 return;
14725 }
14726
14727 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14728 }
14729 }
14730
14731 /**
14732 * @author Leo Fajardo (@leorw)
14733 * @since 1.2.4
14734 */
14735 private function fetch_affiliate_and_custom_terms() {
14736 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14737 $application_data = $this->_storage->affiliate_application_data;
14738 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14739
14740 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14741
14742 $users_api = $this->get_api_user_scope();
14743 $result = $users_api->get( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14744 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14745 if ( ! empty( $result->affiliates ) ) {
14746 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14747
14748 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14749 $application_data['status'] = $affiliate->status;
14750 $this->_storage->affiliate_application_data = $application_data;
14751 }
14752
14753 if ( $affiliate->is_using_custom_terms ) {
14754 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14755 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14756 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14757 }
14758 }
14759
14760 $this->affiliate = $affiliate;
14761 }
14762 }
14763 }
14764 }
14765
14766 /**
14767 * @author Leo Fajardo (@leorw)
14768 * @since 1.2.3
14769 */
14770 private function fetch_affiliate_and_terms() {
14771 $this->_logger->entrance();
14772
14773 $this->fetch_affiliate_terms();
14774 $this->fetch_affiliate_and_custom_terms();
14775 }
14776
14777 /**
14778 * @author Leo Fajardo (@leorw)
14779 * @since 1.2.3
14780 *
14781 * @return FS_Affiliate
14782 */
14783 function get_affiliate() {
14784 return $this->affiliate;
14785 }
14786
14787
14788 /**
14789 * @author Leo Fajardo (@leorw)
14790 * @since 1.2.3
14791 *
14792 * @return FS_AffiliateTerms
14793 */
14794 function get_affiliate_terms() {
14795 return is_object( $this->custom_affiliate_terms ) ?
14796 $this->custom_affiliate_terms :
14797 $this->plugin_affiliate_terms;
14798 }
14799
14800 /**
14801 * @author Leo Fajardo (@leorw)
14802 * @since 1.2.3
14803 */
14804 function _submit_affiliate_application() {
14805 $this->_logger->entrance();
14806
14807 $this->check_ajax_referer( 'submit_affiliate_application' );
14808
14809 if ( ! $this->is_user_admin() ) {
14810 // Only for admins.
14811 self::shoot_ajax_failure();
14812 }
14813
14814 $affiliate = fs_request_get( 'affiliate' );
14815
14816 if ( empty( $affiliate['promotion_methods'] ) ) {
14817 unset( $affiliate['promotion_methods'] );
14818 }
14819
14820 if ( ! empty( $affiliate['additional_domains'] ) ) {
14821 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
14822 }
14823
14824 if ( ! $this->is_registered() ) {
14825 $email_address = isset( $affiliate['email'] ) ? $affiliate['email'] : '';
14826
14827 if ( ! is_email( $email_address ) ) {
14828 self::shoot_ajax_failure('Invalid email address.');
14829 }
14830
14831 // Opt in but don't track usage.
14832 $next_page = $this->opt_in(
14833 $email_address,
14834 false,
14835 false,
14836 false,
14837 false,
14838 false,
14839 true
14840 );
14841
14842 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14843 self::shoot_ajax_failure(
14844 isset( $next_page->error ) ?
14845 $next_page->error->message :
14846 var_export( $next_page, true )
14847 );
14848 } else if ( $this->is_pending_activation() ) {
14849 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' ) );
14850 }
14851 }
14852
14853 $this->fetch_affiliate_terms();
14854
14855 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14856
14857 $api = $this->get_api_user_scope();
14858 $result = $api->call(
14859 ( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
14860 'post',
14861 $affiliate
14862 );
14863
14864 if ( $this->is_api_error( $result ) ) {
14865 self::shoot_ajax_failure(
14866 isset( $result->error ) ?
14867 $result->error->message :
14868 var_export( $result, true )
14869 );
14870 } else {
14871 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
14872 $this->_admin_notices->remove_sticky( 'affiliate_program' );
14873 }
14874
14875 $affiliate_application_data = array(
14876 'status' => 'pending',
14877 'stats_description' => $affiliate['stats_description'],
14878 'promotion_method_description' => $affiliate['promotion_method_description'],
14879 );
14880
14881 if ( ! empty( $affiliate['promotion_methods'] ) ) {
14882 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
14883 }
14884
14885 if ( ! empty( $affiliate['domain'] ) ) {
14886 $affiliate_application_data['domain'] = $affiliate['domain'];
14887 }
14888
14889 if ( ! empty( $affiliate['additional_domains'] ) ) {
14890 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
14891 }
14892
14893 $this->_storage->affiliate_application_data = $affiliate_application_data;
14894 }
14895
14896 // Purge cached affiliate.
14897 $api->purge_cache( 'affiliate.json' );
14898
14899 self::shoot_ajax_success( $result );
14900 }
14901
14902 /**
14903 * @author Leo Fajardo (@leorw)
14904 * @since 1.2.3
14905 *
14906 * @return array|null
14907 */
14908 function get_affiliate_application_data() {
14909 if ( empty( $this->_storage->affiliate_application_data ) ) {
14910 return null;
14911 }
14912
14913 return $this->_storage->affiliate_application_data;
14914 }
14915
14916 #endregion Affiliation ------------------------------------------------------------
14917
14918 #----------------------------------------------------------------------------------
14919 #region URL Generators
14920 #----------------------------------------------------------------------------------
14921
14922 /**
14923 * Alias to pricing_url().
14924 *
14925 * @author Vova Feldman (@svovaf)
14926 * @since 1.0.2
14927 *
14928 * @uses pricing_url()
14929 *
14930 * @param string $period Billing cycle
14931 * @param bool $is_trial
14932 *
14933 * @return string
14934 */
14935 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14936 return $this->pricing_url( $period, $is_trial );
14937 }
14938
14939 /**
14940 * @author Vova Feldman (@svovaf)
14941 * @since 1.0.9
14942 *
14943 * @uses get_upgrade_url()
14944 *
14945 * @return string
14946 */
14947 function get_trial_url() {
14948 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
14949 }
14950
14951 /**
14952 * @author Leo Fajardo (@leorw)
14953 * @since 2.1.4
14954 *
14955 * @param string $new_version
14956 *
14957 * @return string
14958 */
14959 function version_upgrade_checkout_link( $new_version ) {
14960 if ( ! is_object( $this->_license ) ) {
14961 $url = $this->pricing_url();
14962
14963 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
14964 } else {
14965 $subscription = $this->_get_subscription( $this->_license->id );
14966
14967 $url = $this->checkout_url(
14968 is_object( $subscription ) ?
14969 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
14970 WP_FS__PERIOD_LIFETIME,
14971 false,
14972 array( 'licenses' => $this->_license->quota )
14973 );
14974
14975 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
14976 }
14977
14978 return sprintf(
14979 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
14980 sprintf(
14981 '<a href="%s">%s</a>',
14982 $this->apply_filters( 'update_notice_checkout_url', $url ),
14983 $purchase_license_text
14984 ),
14985 $new_version
14986 );
14987 }
14988
14989 /**
14990 * Plugin's pricing URL.
14991 *
14992 * @author Vova Feldman (@svovaf)
14993 * @since 1.0.4
14994 *
14995 * @param string $billing_cycle Billing cycle
14996 *
14997 * @param bool $is_trial
14998 *
14999 * @return string
15000 */
15001 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
15002 $this->_logger->entrance();
15003
15004 $params = array(
15005 'billing_cycle' => $billing_cycle
15006 );
15007
15008 if ( $is_trial ) {
15009 $params['trial'] = 'true';
15010 }
15011
15012 $url = $this->is_addon() ?
15013 $this->_parent->addon_url( $this->_slug ) :
15014 $this->_get_admin_page_url( 'pricing', $params );
15015
15016 return $this->apply_filters( 'pricing_url', $url );
15017 }
15018
15019 /**
15020 * Checkout page URL.
15021 *
15022 * @author Vova Feldman (@svovaf)
15023 * @since 1.0.6
15024 *
15025 * @param string $billing_cycle Billing cycle
15026 * @param bool $is_trial
15027 * @param array $extra (optional) Extra parameters, override other query params.
15028 * @param bool|null $network
15029 *
15030 * @return string
15031 */
15032 function checkout_url(
15033 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
15034 $is_trial = false,
15035 $extra = array(),
15036 $network = null
15037 ) {
15038 $this->_logger->entrance();
15039
15040 $params = array(
15041 'checkout' => 'true',
15042 'billing_cycle' => $billing_cycle,
15043 );
15044
15045 if ( $is_trial ) {
15046 $params['trial'] = 'true';
15047 }
15048
15049 /**
15050 * Params in extra override other params.
15051 */
15052 $params = array_merge( $params, $extra );
15053
15054 return $this->apply_filters( 'checkout_url', $this->_get_admin_page_url( 'pricing', $params, $network ) );
15055 }
15056
15057 /**
15058 * Add-on checkout URL.
15059 *
15060 * @author Vova Feldman (@svovaf)
15061 * @since 1.1.7
15062 *
15063 * @param number $addon_id
15064 * @param number $pricing_id
15065 * @param string $billing_cycle
15066 * @param bool $is_trial
15067 * @param bool|null $network
15068 *
15069 * @return string
15070 */
15071 function addon_checkout_url(
15072 $addon_id,
15073 $pricing_id,
15074 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
15075 $is_trial = false,
15076 $network = null
15077 ) {
15078 return $this->checkout_url( $billing_cycle, $is_trial, array(
15079 'plugin_id' => $addon_id,
15080 'pricing_id' => $pricing_id,
15081 ), $network );
15082 }
15083
15084 #endregion
15085
15086 #endregion ------------------------------------------------------------------
15087
15088 /**
15089 * Check if plugin has any add-ons.
15090 *
15091 * @author Vova Feldman (@svovaf)
15092 * @since 1.0.5
15093 *
15094 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
15095 *
15096 * @return bool
15097 */
15098 function has_addons() {
15099 $this->_logger->entrance();
15100
15101 return $this->_has_addons;
15102 }
15103
15104 /**
15105 * Check if plugin can work in anonymous mode.
15106 *
15107 * @author Vova Feldman (@svovaf)
15108 * @since 1.0.9
15109 *
15110 * @return bool
15111 *
15112 * @deprecated Please use is_enable_anonymous() instead.
15113 */
15114 function enable_anonymous() {
15115 return $this->_enable_anonymous;
15116 }
15117
15118 /**
15119 * Check if plugin can work in anonymous mode.
15120 *
15121 * @author Vova Feldman (@svovaf)
15122 * @since 1.1.9
15123 *
15124 * @return bool
15125 */
15126 function is_enable_anonymous() {
15127 return $this->_enable_anonymous;
15128 }
15129
15130 /**
15131 * Check if plugin is premium only (no free plans).
15132 *
15133 * @author Vova Feldman (@svovaf)
15134 * @since 1.1.9
15135 *
15136 * @return bool
15137 */
15138 function is_only_premium() {
15139 return $this->_is_premium_only;
15140 }
15141
15142 /**
15143 * Checks if the plugin's type is "plugin". The other type is "theme".
15144 *
15145 * @author Leo Fajardo (@leorw)
15146 * @since 1.2.2
15147 *
15148 * @return bool
15149 */
15150 function is_plugin() {
15151 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
15152 }
15153
15154 /**
15155 * @author Leo Fajardo (@leorw)
15156 * @since 1.2.2
15157 *
15158 * @return string
15159 */
15160 function get_module_type() {
15161 if ( ! isset( $this->_module_type ) ) {
15162 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
15163 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
15164 }
15165
15166 return $this->_module_type;
15167 }
15168
15169 /**
15170 * @author Leo Fajardo (@leorw)
15171 * @since 1.2.2
15172 *
15173 * @return string
15174 */
15175 function get_plugin_main_file_path() {
15176 return $this->_plugin_main_file_path;
15177 }
15178
15179 /**
15180 * Check if module has a premium code version.
15181 *
15182 * Serviceware module might be freemium without any
15183 * premium code version, where the paid features
15184 * are all part of the service.
15185 *
15186 * @author Vova Feldman (@svovaf)
15187 * @since 1.2.1.6
15188 *
15189 * @return bool
15190 */
15191 function has_premium_version() {
15192 return $this->_has_premium_version;
15193 }
15194
15195 /**
15196 * Check if feature supported with current site's plan.
15197 *
15198 * @author Vova Feldman (@svovaf)
15199 * @since 1.0.1
15200 *
15201 * @todo IMPLEMENT
15202 *
15203 * @param number $feature_id
15204 *
15205 * @throws Exception
15206 */
15207 function is_feature_supported( $feature_id ) {
15208 throw new Exception( 'not implemented' );
15209 }
15210
15211 /**
15212 * @author Vova Feldman (@svovaf)
15213 * @since 1.0.1
15214 *
15215 * @return bool Is running in SSL/HTTPS
15216 */
15217 function is_ssl() {
15218 return WP_FS__IS_HTTPS;
15219 }
15220
15221 /**
15222 * @author Vova Feldman (@svovaf)
15223 * @since 1.0.9
15224 *
15225 * @return bool Is running in AJAX call.
15226 *
15227 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
15228 */
15229 static function is_ajax() {
15230 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
15231 }
15232
15233 /**
15234 * Check if it's an AJAX call targeted for the current module.
15235 *
15236 * @author Vova Feldman (@svovaf)
15237 * @since 1.2.0
15238 *
15239 * @param array|string $actions Collection of AJAX actions.
15240 *
15241 * @return bool
15242 */
15243 function is_ajax_action( $actions ) {
15244 // Verify it's an ajax call.
15245 if ( ! self::is_ajax() ) {
15246 return false;
15247 }
15248
15249 // Verify the call is relevant for the plugin.
15250 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
15251 return false;
15252 }
15253
15254 // Verify it's one of the specified actions.
15255 if ( is_string( $actions ) ) {
15256 $actions = explode( ',', $actions );
15257 }
15258
15259 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15260 $ajax_action = fs_request_get( 'action' );
15261
15262 foreach ( $actions as $action ) {
15263 if ( $ajax_action === $this->get_action_tag( $action ) ) {
15264 return true;
15265 }
15266 }
15267 }
15268
15269 return false;
15270 }
15271
15272 /**
15273 * Check if it's an AJAX call targeted for current request.
15274 *
15275 * @author Vova Feldman (@svovaf)
15276 * @since 1.2.0
15277 *
15278 * @param array|string $actions Collection of AJAX actions.
15279 * @param number|null $module_id
15280 *
15281 * @return bool
15282 */
15283 static function is_ajax_action_static( $actions, $module_id = null ) {
15284 // Verify it's an ajax call.
15285 if ( ! self::is_ajax() ) {
15286 return false;
15287 }
15288
15289
15290 if ( ! empty( $module_id ) ) {
15291 // Verify the call is relevant for the plugin.
15292 if ( $module_id != fs_request_get( 'module_id' ) ) {
15293 return false;
15294 }
15295 }
15296
15297 // Verify it's one of the specified actions.
15298 if ( is_string( $actions ) ) {
15299 $actions = explode( ',', $actions );
15300 }
15301
15302 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15303 $ajax_action = fs_request_get( 'action' );
15304
15305 foreach ( $actions as $action ) {
15306 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
15307 return true;
15308 }
15309 }
15310 }
15311
15312 return false;
15313 }
15314
15315 /**
15316 * @author Vova Feldman (@svovaf)
15317 * @since 1.1.7
15318 *
15319 * @return bool
15320 */
15321 static function is_cron() {
15322 return ( defined( 'DOING_CRON' ) && DOING_CRON );
15323 }
15324
15325 /**
15326 * @author Leo Fajardo (@leorw)
15327 * @since 2.5.0
15328 *
15329 * @return bool
15330 */
15331 static function is_admin_post() {
15332 return ( 'admin-post.php' === self::get_current_page() );
15333 }
15334
15335 /**
15336 * Check if a real user is visiting the admin dashboard.
15337 *
15338 * @author Vova Feldman (@svovaf)
15339 * @since 1.1.7
15340 *
15341 * @return bool
15342 */
15343 function is_user_in_admin() {
15344 return (
15345 is_admin() &&
15346 ! self::is_ajax() &&
15347 ! self::is_cron() &&
15348 ! self::is_admin_post()
15349 );
15350 }
15351
15352 /**
15353 * Check if a real user is in the customizer view.
15354 *
15355 * @author Vova Feldman (@svovaf)
15356 * @since 1.2.2.7
15357 *
15358 * @return bool
15359 */
15360 static function is_customizer() {
15361 return is_customize_preview();
15362 }
15363
15364 /**
15365 * Check if running in HTTPS and if site's plan matching the specified plan.
15366 *
15367 * @param string $plan
15368 * @param bool $exact
15369 *
15370 * @return bool
15371 */
15372 function is_ssl_and_plan( $plan, $exact = false ) {
15373 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
15374 }
15375
15376 /**
15377 * Construct plugin's settings page URL.
15378 *
15379 * @author Vova Feldman (@svovaf)
15380 * @since 1.0.4
15381 *
15382 * @param string $page
15383 * @param array $params
15384 * @param bool|null $network
15385 *
15386 * @return string
15387 */
15388 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
15389 if ( is_null( $network ) ) {
15390 $network = (
15391 $this->_is_network_active &&
15392 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
15393 );
15394 }
15395
15396 if ( 0 < count( $params ) ) {
15397 foreach ( $params as $k => $v ) {
15398 $params[ $k ] = urlencode( $v );
15399 }
15400 }
15401
15402 $page_param = $this->_menu->get_slug( $page );
15403
15404 if ( empty( $page ) &&
15405 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
15406 $this->show_opt_in_on_themes_page()
15407 ) {
15408 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
15409
15410 return add_query_arg(
15411 $params,
15412 $this->admin_url( 'themes.php', 'admin', $network )
15413 );
15414 }
15415
15416 if ( ! $this->has_settings_menu() ) {
15417 if ( ! empty( $page ) ) {
15418 // Module doesn't have a setting page, but since the request is for
15419 // a specific Freemius page, use the admin.php path.
15420 return add_query_arg( array_merge( $params, array(
15421 'page' => $page_param,
15422 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15423 } else {
15424 if ( $this->is_activation_mode() ) {
15425 /**
15426 * @author Vova Feldman
15427 * @since 1.2.1.6
15428 *
15429 * If plugin doesn't have a settings page, create one for the opt-in screen.
15430 */
15431 return add_query_arg( array_merge( $params, array(
15432 'page' => $this->_slug,
15433 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15434 } else {
15435 // Plugin without a settings page.
15436 return add_query_arg(
15437 $params,
15438 $this->admin_url( 'plugins.php', 'admin', $network )
15439 );
15440 }
15441 }
15442 }
15443
15444 // Module has a submenu settings page.
15445 if ( ! $this->_menu->is_top_level() ) {
15446 $parent_slug = $this->_menu->get_parent_slug();
15447 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
15448 $parent_slug :
15449 'admin.php';
15450
15451 return add_query_arg( array_merge( $params, array(
15452 'page' => $page_param,
15453 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
15454 }
15455
15456 // Module has a top level CPT settings page.
15457 if ( $this->_menu->is_cpt() ) {
15458 if ( empty( $page ) && $this->is_activation_mode() ) {
15459 return add_query_arg( array_merge( $params, array(
15460 'page' => $page_param
15461 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15462 } else {
15463 if ( ! empty( $page ) ) {
15464 $params['page'] = $page_param;
15465 }
15466
15467 return add_query_arg(
15468 $params,
15469 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
15470 );
15471 }
15472 }
15473
15474 // Module has a custom top level settings page.
15475 return add_query_arg( array_merge( $params, array(
15476 'page' => $page_param,
15477 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15478 }
15479
15480 #--------------------------------------------------------------------------------
15481 #region Multisite
15482 #--------------------------------------------------------------------------------
15483
15484 /**
15485 * @author Leo Fajardo (@leorw)
15486 * @since 2.0.0
15487 *
15488 * @return bool
15489 */
15490 function is_network_active() {
15491 return $this->_is_network_active;
15492 }
15493
15494 /**
15495 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
15496 *
15497 * @author Leo Fajardo (@leorw)
15498 * @since 2.0.0
15499 *
15500 * @param bool|int[] $all_or_blog_ids
15501 */
15502 private function delegate_connection( $all_or_blog_ids = true ) {
15503 $this->_logger->entrance();
15504
15505 $this->_admin_notices->remove_sticky( 'connect_account' );
15506
15507 if ( true === $all_or_blog_ids ) {
15508 // All sites delegation.
15509 $this->_storage->store( 'is_delegated_connection', true, true );
15510 } else {
15511 // Specified sites delegation.
15512 foreach ( $all_or_blog_ids as $blog_id ) {
15513 $this->delegate_site_connection( $blog_id );
15514 }
15515 }
15516
15517 $this->network_upgrade_mode_completed();
15518 }
15519
15520 /**
15521 * Delegate specific network site conncetion to the site admin.
15522 *
15523 * @author Vova Feldman (@svovaf)
15524 * @since 2.0.0
15525 *
15526 * @param int $blog_id
15527 */
15528 private function delegate_site_connection( $blog_id ) {
15529 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
15530 }
15531
15532 /**
15533 * Check if super-admin delegated the connection of ALL sites to the site admins.
15534 *
15535 * @author Vova Feldman (@svovaf)
15536 * @since 2.0.0
15537 *
15538 * @return bool
15539 */
15540 function is_network_delegated_connection() {
15541 if ( ! $this->_is_network_active ) {
15542 return false;
15543 }
15544
15545 return $this->_storage->get( 'is_delegated_connection', false, true );
15546 }
15547
15548 /**
15549 * @author Leo Fajardo (@leorw)
15550 * @since 2.0.0
15551 *
15552 * @param int $blog_id
15553 *
15554 * @return bool
15555 */
15556 function is_site_delegated_connection( $blog_id = 0 ) {
15557 if ( ! $this->_is_network_active ) {
15558 return false;
15559 }
15560
15561 if ( 0 == $blog_id ) {
15562 $blog_id = get_current_blog_id();
15563 }
15564
15565 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15566 }
15567
15568 /**
15569 * Check if delegated the connection. When running within the network admin,
15570 * and haven't specified the blog ID, checks if network level delegated. If running
15571 * within a site admin or specified a blog ID, check if delegated the connection for
15572 * the current context site.
15573 *
15574 * If executed outside the the admin, check if delegated the connection
15575 * for the current context site OR the whole network.
15576 *
15577 * @author Vova Feldman (@svovaf)
15578 * @since 2.0.0
15579 *
15580 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15581 *
15582 * @return bool
15583 */
15584 function is_delegated_connection( $blog_id = 0 ) {
15585 if ( ! $this->_is_network_active ) {
15586 return false;
15587 }
15588
15589 if ( fs_is_network_admin() && 0 == $blog_id ) {
15590 return $this->is_network_delegated_connection();
15591 }
15592
15593 return (
15594 $this->is_network_delegated_connection() ||
15595 $this->is_site_delegated_connection( $blog_id )
15596 );
15597 }
15598
15599 /**
15600 * Check if the current module is active for the site.
15601 *
15602 * @author Vova Feldman (@svovaf)
15603 * @since 2.0.0
15604 *
15605 * @param int $blog_id
15606 *
15607 * @return bool
15608 */
15609 function is_active_for_site( $blog_id ) {
15610 if ( ! is_multisite() ) {
15611 // Not a multisite and this code is executed, means that the plugin is active.
15612 return true;
15613 }
15614
15615 if ( $this->is_theme() ) {
15616 // All themes are site level activated.
15617 return true;
15618 }
15619
15620 if ( $this->_is_network_active ) {
15621 // Plugin was network activated so it's active.
15622 return true;
15623 }
15624
15625 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15626 }
15627
15628 /**
15629 * @todo Implement pagination when accessing the subsites collection.
15630 *
15631 * @author Leo Fajardo (@leorw)
15632 * @since 2.0.0
15633 *
15634 * @param int $limit Default to 1,000
15635 * @param int $offset Default to 0
15636 *
15637 * @return array Active & public sites collection.
15638 */
15639 static function get_sites( $limit = 1000, $offset = 0 ) {
15640 if ( ! is_multisite() ) {
15641 return array();
15642 }
15643
15644 /**
15645 * For consistency with get_blog_list() which only return active public sites.
15646 *
15647 * @author Vova Feldman (@svovaf)
15648 */
15649 $args = array(
15650 /**
15651 * Commented out in order to handle the migration of site options whether the site is public or not.
15652 *
15653 * @author Leo Fajardo (@leorw)
15654 * @since 2.2.1
15655 */
15656 // 'public' => 1,
15657 'archived' => 0,
15658 'mature' => 0,
15659 'spam' => 0,
15660 'deleted' => 0,
15661 'number' => $limit,
15662 'offset' => $offset,
15663 );
15664
15665 return get_sites( $args );
15666 }
15667
15668 /**
15669 * Checks if a given blog is active.
15670 *
15671 * @author Vova Feldman (@svovaf)
15672 * @since 2.0.0
15673 *
15674 * @param $blog_id
15675 *
15676 * @return bool
15677 */
15678 private static function is_site_active( $blog_id ) {
15679 global $wpdb;
15680
15681 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15682
15683 if ( ! is_object( $blog_info ) ) {
15684 return false;
15685 }
15686
15687 return (
15688 true == $blog_info->public &&
15689 false == $blog_info->archived &&
15690 false == $blog_info->mature &&
15691 false == $blog_info->spam &&
15692 false == $blog_info->deleted
15693 );
15694 }
15695
15696 /**
15697 * Get a mapping between the site addresses to their blog IDs.
15698 *
15699 * @author Vova Feldman (@svovaf)
15700 * @since 2.0.0
15701 *
15702 * @return array {
15703 * @key string Site address without protocol with a trailing slash.
15704 * @value int Site's blog ID.
15705 * }
15706 */
15707 private function get_address_to_blog_map() {
15708 $sites = self::get_sites();
15709
15710 // Map site addresses to their blog IDs.
15711 $address_to_blog_map = array();
15712 foreach ( $sites as $site ) {
15713 $blog_id = self::get_site_blog_id( $site );
15714 $address = self::get_unfiltered_site_url( $blog_id, true, true );
15715 $address_to_blog_map[ $address ] = $blog_id;
15716 }
15717
15718 return $address_to_blog_map;
15719 }
15720
15721 /**
15722 * Get a mapping between the site addresses to their blog IDs.
15723 *
15724 * @author Vova Feldman (@svovaf)
15725 * @since 2.0.0
15726 *
15727 * @return array {
15728 * @key int Site's blog ID.
15729 * @value FS_Site Associated install.
15730 * }
15731 */
15732 function get_blog_install_map() {
15733 $sites = self::get_sites();
15734
15735 // Map site blog ID to its install.
15736 $install_map = array();
15737
15738 foreach ( $sites as $site ) {
15739 $blog_id = self::get_site_blog_id( $site );
15740 $install = $this->get_install_by_blog_id( $blog_id );
15741
15742 if ( is_object( $install ) ) {
15743 $install_map[ $blog_id ] = $install;
15744 }
15745 }
15746
15747 return $install_map;
15748 }
15749
15750 /**
15751 * @author Vova Feldman (@svovaf)
15752 * @since 2.5.1
15753 *
15754 * @param bool|null $is_delegated When `true`, returns only connection delegated blog IDs. When `false`, only non-delegated blog IDs.
15755 *
15756 * @return int[]
15757 */
15758 private function get_blog_ids( $is_delegated = null ) {
15759 $blog_ids = array();
15760
15761 $sites = self::get_sites();
15762 foreach ( $sites as $site ) {
15763 $blog_id = self::get_site_blog_id( $site );
15764
15765 if (
15766 is_null( $is_delegated ) ||
15767 $is_delegated === $this->is_site_delegated_connection( $blog_id )
15768 ) {
15769 $blog_ids[] = $blog_id;
15770 }
15771 }
15772
15773 return $blog_ids;
15774 }
15775
15776 /**
15777 * @author Vova Feldman (@svovaf)
15778 * @since 2.5.1
15779 *
15780 * @return int[]
15781 */
15782 private function get_non_delegated_blog_ids() {
15783 return $this->get_blog_ids( false );
15784 }
15785
15786 /**
15787 * Gets a map of module IDs that the given user has opted-in to.
15788 *
15789 * @author Leo Fajardo (@leorw)
15790 * @since 2.1.0
15791 *
15792 * @param number $fs_user_id
15793 *
15794 * @return array {
15795 * @key number $plugin_id
15796 * @value bool Always true.
15797 * }
15798 */
15799 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15800 self::$_static_logger->entrance();
15801
15802 if ( ! is_multisite() ) {
15803 $installs = array_merge(
15804 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15805 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15806 );
15807 } else {
15808 $sites = self::get_sites();
15809
15810 $installs = array();
15811 foreach ( $sites as $site ) {
15812 $blog_id = self::get_site_blog_id( $site );
15813
15814 $installs = array_merge(
15815 $installs,
15816 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15817 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15818 );
15819 }
15820 }
15821
15822 $module_ids_map = array();
15823 foreach ( $installs as $install ) {
15824 if ( is_object( $install ) &&
15825 FS_Site::is_valid_id( $install->id ) &&
15826 FS_User::is_valid_id( $install->user_id ) &&
15827 ( $install->user_id == $fs_user_id )
15828 ) {
15829 $module_ids_map[ $install->plugin_id ] = true;
15830 }
15831 }
15832
15833 return $module_ids_map;
15834 }
15835
15836 /**
15837 * @author Leo Fajardo (@leorw)
15838 *
15839 * @return null|array {
15840 * 'install' => FS_Site Module's install,
15841 * 'blog_id' => string The associated blog ID.
15842 * }
15843 */
15844 function find_first_install() {
15845 $sites = self::get_sites();
15846
15847 foreach ( $sites as $site ) {
15848 $blog_id = self::get_site_blog_id( $site );
15849 $install = $this->get_install_by_blog_id( $blog_id );
15850
15851 if ( is_object( $install ) ) {
15852 return array(
15853 'install' => $install,
15854 'blog_id' => $blog_id
15855 );
15856 }
15857 }
15858
15859 return null;
15860 }
15861
15862 /**
15863 * Switches the Freemius site level context to a specified blog.
15864 *
15865 * @author Vova Feldman (@svovaf)
15866 * @since 2.0.0
15867 *
15868 * @param int $blog_id
15869 * @param FS_Site $install
15870 * @param bool $flush
15871 *
15872 * @return bool Since 2.3.1 returns if a switch was made.
15873 */
15874 function switch_to_blog( $blog_id, FS_Site $install = null, $flush = false ) {
15875 if ( ! is_numeric( $blog_id ) ) {
15876 return false;
15877 }
15878
15879 if ( ! $flush && $blog_id == $this->_context_is_network_or_blog_id ) {
15880 return false;
15881 }
15882
15883 switch_to_blog( $blog_id );
15884 $this->_context_is_network_or_blog_id = $blog_id;
15885
15886 self::$_accounts->set_site_blog_context( $blog_id );
15887 $this->_storage->set_site_blog_context( $blog_id );
15888 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
15889
15890 $this->_site = is_object( $install ) ?
15891 $install :
15892 $this->get_install_by_blog_id( $blog_id );
15893
15894 $this->_user = false;
15895 $this->_licenses = false;
15896 $this->_license = null;
15897 $this->is_whitelabeled = null;
15898
15899 if ( is_object( $this->_site ) ) {
15900 // Try to fetch user from install.
15901 $this->_user = self::_get_user_by_id( $this->_site->user_id );
15902
15903 if ( ! is_object( $this->_user ) &&
15904 FS_User::is_valid_id( $this->_storage->prev_user_id )
15905 ) {
15906 // Try to fetch previously saved user.
15907 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
15908
15909 if ( ! is_object( $this->_user ) ) {
15910 // Fallback to network's user.
15911 $this->_user = $this->get_network_user();
15912 }
15913 }
15914
15915 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
15916
15917 if ( ! empty( $all_plugin_licenses ) ) {
15918 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
15919 $this->_license = null;
15920 } else {
15921 $license_found = false;
15922 foreach ( $all_plugin_licenses as $license ) {
15923 if ( $license->id == $this->_site->license_id ) {
15924 // License found.
15925 $this->_license = $license;
15926 $license_found = true;
15927 break;
15928 }
15929 }
15930
15931 if ( $license_found ) {
15932 $this->link_license_2_user( $this->_license->id, $this->_user->id );
15933 }
15934 }
15935
15936 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15937 }
15938 }
15939
15940 unset( $this->_site_api );
15941 unset( $this->_user_api );
15942
15943 return true;
15944 }
15945
15946 /**
15947 * Restore the blog context to the blog that originally loaded the module.
15948 *
15949 * @author Vova Feldman (@svovaf)
15950 * @since 2.0.0
15951 */
15952 function restore_current_blog() {
15953 $this->switch_to_blog( $this->_blog_id );
15954 }
15955
15956 /**
15957 * @author Vova Feldman (@svovaf)
15958 * @since 2.0.0
15959 *
15960 * @param array|WP_Site $site
15961 *
15962 * @return int
15963 */
15964 static function get_site_blog_id( &$site ) {
15965 return ( $site instanceof WP_Site ) ?
15966 $site->blog_id :
15967 ( is_object( $site ) && isset( $site->userblog_id ) ?
15968 $site->userblog_id :
15969 $site['blog_id'] );
15970 }
15971
15972 /**
15973 * @author Vova Feldman (@svovaf)
15974 * @since 2.5.1
15975 *
15976 * @param WP_Site[]|array[] $sites
15977 *
15978 * @return int[]
15979 */
15980 static function get_sites_blog_ids( $sites ) {
15981 $blog_ids = array();
15982 foreach ( $sites as $site ) {
15983 $blog_ids[] = self::get_site_blog_id( $site );
15984 }
15985
15986 return $blog_ids;
15987 }
15988
15989 /**
15990 * @author Leo Fajardo (@leorw)
15991 * @since 2.0.0
15992 *
15993 * @param array|WP_Site|null $site
15994 * @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.
15995 *
15996 * @return array
15997 */
15998 function get_site_info( $site = null, $load_registration = false ) {
15999 $this->_logger->entrance();
16000
16001 $switched = false;
16002
16003 $registration_date = null;
16004
16005 if ( is_null( $site ) ) {
16006 $url = self::get_unfiltered_site_url();
16007 $name = get_bloginfo( 'name' );
16008 $blog_id = null;
16009 } else {
16010 $blog_id = self::get_site_blog_id( $site );
16011
16012 if ( get_current_blog_id() != $blog_id ) {
16013 switch_to_blog( $blog_id );
16014 $switched = true;
16015 }
16016
16017 if ( $site instanceof WP_Site ) {
16018 $url = $site->siteurl;
16019 $name = $site->blogname;
16020 $registration_date = $site->registered;
16021 } else {
16022 $url = self::get_unfiltered_site_url( $blog_id );
16023 $name = get_bloginfo( 'name' );
16024 }
16025 }
16026
16027 if ( empty( $registration_date ) && $load_registration ) {
16028 $blog_details = get_blog_details( $blog_id, false );
16029
16030 if ( is_object( $blog_details ) && isset( $blog_details->registered ) ) {
16031 $registration_date = $blog_details->registered;
16032 }
16033 }
16034
16035 $info = array(
16036 'uid' => $this->get_anonymous_id( $blog_id ),
16037 'url' => $url,
16038 );
16039
16040 // Add these diagnostic information only if user allowed to track.
16041 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
16042 $info = array_merge( $info, array(
16043 'title' => $name,
16044 'language' => self::get_sanitized_language(),
16045 ) );
16046 }
16047
16048 if ( is_numeric( $blog_id ) ) {
16049 $info['blog_id'] = $blog_id;
16050 }
16051
16052 if ( ! empty( $registration_date ) ) {
16053 $info[ 'registration_date' ] = $registration_date;
16054 }
16055
16056 if ( $switched ) {
16057 restore_current_blog();
16058 }
16059
16060 return $info;
16061 }
16062
16063 /**
16064 * Load the module's install based on the blog ID.
16065 *
16066 * @author Vova Feldman (@svovaf)
16067 * @since 2.0.0
16068 *
16069 * @param int|null $blog_id
16070 *
16071 * @return FS_Site
16072 */
16073 function get_install_by_blog_id( $blog_id = null ) {
16074 $installs = self::get_all_sites( $this->_module_type, $blog_id );
16075 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
16076
16077 if ( is_object( $install ) &&
16078 is_numeric( $install->id ) &&
16079 is_numeric( $install->user_id ) &&
16080 FS_Plugin_Plan::is_valid_id( $install->plan_id )
16081 ) {
16082 // Load site.
16083 $install = clone $install;
16084 }
16085
16086 return $install;
16087 }
16088
16089 /**
16090 * Check if module is installed on a specified site.
16091 *
16092 * @author Vova Feldman (@svovaf)
16093 * @since 2.0.0
16094 *
16095 * @param int|null $blog_id
16096 *
16097 * @return bool
16098 */
16099 function is_installed_on_site( $blog_id = null ) {
16100 $installs = self::get_all_sites( $this->_module_type, $blog_id );
16101 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
16102
16103 return (
16104 is_object( $install ) &&
16105 is_numeric( $install->id ) &&
16106 is_numeric( $install->user_id ) &&
16107 FS_Plugin_Plan::is_valid_id( $install->plan_id )
16108 );
16109 }
16110
16111 /**
16112 * Check if super-admin connected at least one site via the network opt-in.
16113 *
16114 * @author Vova Feldman (@svovaf)
16115 * @since 2.0.0
16116 *
16117 * @return bool
16118 */
16119 function is_network_registered() {
16120 if ( ! $this->_is_network_active ) {
16121 return false;
16122 }
16123
16124 return FS_User::is_valid_id( $this->_storage->network_user_id );
16125 }
16126
16127 /**
16128 * Returns the main user associated with the network.
16129 *
16130 * @author Vova Feldman (@svovaf)
16131 * @since 2.0.0
16132 *
16133 * @return FS_User
16134 */
16135 function get_network_user() {
16136 if ( ! $this->_is_network_active ) {
16137 return null;
16138 }
16139
16140 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
16141 self::_get_user_by_id( $this->_storage->network_user_id ) :
16142 null;
16143 }
16144
16145 /**
16146 * Returns the current context user or the network's main user.
16147 *
16148 * @author Vova Feldman (@svovaf)
16149 * @since 2.0.0
16150 *
16151 * @return FS_User
16152 */
16153 function get_current_or_network_user() {
16154 return ( $this->_user instanceof FS_User ) ?
16155 $this->_user :
16156 $this->get_network_user();
16157 }
16158
16159 /**
16160 * Returns the main install associated with the network.
16161 *
16162 * @author Vova Feldman (@svovaf)
16163 * @since 2.0.0
16164 *
16165 * @return FS_Site
16166 */
16167 function get_network_install() {
16168 if ( ! $this->_is_network_active ) {
16169 return null;
16170 }
16171
16172 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
16173 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
16174 null;
16175 }
16176
16177 /**
16178 * Returns the blog ID that is associated with the main install.
16179 *
16180 * @author Leo Fajardo (@leorw)
16181 * @since 2.0.0
16182 *
16183 * @return int|null
16184 */
16185 function get_network_install_blog_id() {
16186 if ( ! $this->_is_network_active ) {
16187 return null;
16188 }
16189
16190 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
16191 $this->_storage->network_install_blog_id :
16192 null;
16193 }
16194
16195 /**
16196 * Returns the current context install or the network's main install.
16197 *
16198 * @author Vova Feldman (@svovaf)
16199 * @since 2.0.0
16200 *
16201 * @return FS_Site
16202 */
16203 function get_current_or_network_install() {
16204 return ( $this->_site instanceof FS_Site ) ?
16205 $this->_site :
16206 $this->get_network_install();
16207 }
16208
16209 /**
16210 * Check if executing a site level action from the network level admin.
16211 *
16212 * @author Vova Feldman (@svovaf)
16213 * @since 2.0.0
16214 *
16215 * @return false|int If yes, return the requested blog ID.
16216 */
16217 private function is_network_level_site_specific_action() {
16218 if ( ! $this->_is_network_active ) {
16219 return false;
16220 }
16221
16222 if ( ! fs_is_network_admin() ) {
16223 return false;
16224 }
16225
16226 $blog_id = fs_request_get( 'blog_id', '' );
16227
16228 return is_numeric( $blog_id ) ? $blog_id : false;
16229 }
16230
16231 /**
16232 * Check if executing an action from the network level admin.
16233 *
16234 * @author Vova Feldman (@svovaf)
16235 * @since 2.0.0
16236 *
16237 * @return bool
16238 */
16239 private function is_network_level_action() {
16240 return ( $this->_is_network_active && fs_is_network_admin() );
16241 }
16242
16243 /**
16244 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
16245 * 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.
16246 *
16247 * @author Vova Feldman (@svovaf)
16248 * @since 2.0.0
16249 *
16250 * @param int $context_blog_id
16251 */
16252 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
16253 $this->_logger->entrance();
16254
16255 if ( $this->_is_network_active ) {
16256 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
16257 $installs_map = $this->get_blog_install_map();
16258
16259 foreach ( $installs_map as $blog_id => $install ) {
16260 /**
16261 * @var FS_Site $install
16262 */
16263 if ( $context_blog_id == $blog_id ) {
16264 continue;
16265 }
16266
16267 if ( $install->user_id != $this->_storage->network_user_id ) {
16268 continue;
16269 }
16270
16271 // Switch reference to a blog that is opted-in and belong to the same super-admin.
16272 $this->_storage->network_install_blog_id = $blog_id;
16273 break;
16274 }
16275 }
16276 }
16277
16278 if ( ! $this->is_registered() ) {
16279 return;
16280 }
16281
16282 if ( $this->is_sync_cron_scheduled() &&
16283 $context_blog_id == $this->get_sync_cron_blog_id()
16284 ) {
16285 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
16286 }
16287
16288 if ( $this->is_install_sync_scheduled() &&
16289 $context_blog_id == $this->get_install_sync_cron_blog_id()
16290 ) {
16291 $this->schedule_install_sync( $context_blog_id );
16292 }
16293 }
16294
16295 /**
16296 * Executed after site deactivation, archive, or flag as spam.
16297 *
16298 * @author Vova Feldman (@svovaf)
16299 * @since 2.0.0
16300 *
16301 * @param int $context_blog_id
16302 */
16303 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
16304 $this->_logger->entrance();
16305
16306 $install = $this->get_install_by_blog_id( $context_blog_id );
16307
16308 if ( ! is_object( $install ) ) {
16309 // Site not connected.
16310 return;
16311 }
16312
16313 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16314
16315 if ( ! $this->is_registered() ) {
16316 return;
16317 }
16318
16319 $current_blog_id = get_current_blog_id();
16320
16321 $this->switch_to_blog( $context_blog_id );
16322
16323 // Send deactivation event.
16324 $this->sync_install( array(
16325 'is_active' => false,
16326 ) );
16327
16328 $this->switch_to_blog( $current_blog_id );
16329 }
16330
16331 /**
16332 * Executed after site deletion.
16333 *
16334 * @author Vova Feldman (@svovaf)
16335 * @since 2.0.0
16336 *
16337 * @param int $context_blog_id
16338 * @param bool $drop True if site's database tables should be dropped. Default is false.
16339 */
16340 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
16341 $this->_logger->entrance();
16342
16343 $install = $this->get_install_by_blog_id( $context_blog_id );
16344
16345 if ( ! is_object( $install ) ) {
16346 // Site not connected.
16347 return;
16348 }
16349
16350 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16351
16352 if ( ! $this->is_registered() ) {
16353 return;
16354 }
16355
16356 $current_blog_id = get_current_blog_id();
16357
16358 $this->switch_to_blog( $context_blog_id );
16359
16360 if ( $drop ) {
16361 // Delete install if dropping site DB.
16362 $this->delete_account_event();
16363 } else {
16364 // Send deactivation event.
16365 $this->sync_install( array(
16366 'is_active' => false,
16367 ) );
16368 }
16369
16370 $this->switch_to_blog( $current_blog_id );
16371 }
16372
16373 /**
16374 * Executed after site deletion, called from wp_delete_site
16375 *
16376 * @author Dario Curvino (@dudo)
16377 * @since 2.5.0
16378 *
16379 * @param WP_Site $old_site
16380 */
16381 public function _after_wpsite_deleted_callback( WP_Site $old_site ) {
16382 $this->_logger->entrance();
16383
16384 $this->_after_site_deleted_callback( $old_site->blog_id, true );
16385 }
16386
16387 /**
16388 * Executed after site re-activation.
16389 *
16390 * @author Vova Feldman (@svovaf)
16391 * @since 2.0.0
16392 *
16393 * @param int $context_blog_id
16394 */
16395 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
16396 $this->_logger->entrance();
16397
16398 $install = $this->get_install_by_blog_id( $context_blog_id );
16399
16400 if ( ! is_object( $install ) ) {
16401 // Site not connected.
16402 return;
16403 }
16404
16405 if ( ! self::is_site_active( $context_blog_id ) ) {
16406 // Site not yet active (can be in spam mode, archived, deleted...).
16407 return;
16408 }
16409
16410 $current_blog_id = get_current_blog_id();
16411
16412 $this->switch_to_blog( $context_blog_id );
16413
16414 // Send re-activation event.
16415 $this->sync_install( array(
16416 'is_active' => true,
16417 ) );
16418
16419 $this->switch_to_blog( $current_blog_id );
16420 }
16421
16422 #endregion Multisite
16423
16424 /**
16425 * @author Leo Fajardo (@leorw)
16426 *
16427 * @param string $path
16428 * @param string $scheme
16429 * @param bool $network
16430 *
16431 * @return string
16432 */
16433 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
16434 return ( $this->_is_network_active && $network ) ?
16435 network_admin_url( $path, $scheme ) :
16436 admin_url( $path, $scheme );
16437 }
16438
16439 /**
16440 * Check if currently in a specified admin page.
16441 *
16442 * @author Vova Feldman (@svovaf)
16443 * @since 1.2.2.7
16444 *
16445 * @param string $page
16446 *
16447 * @return bool
16448 */
16449 function is_admin_page( $page ) {
16450 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
16451 }
16452
16453 /**
16454 * Check if currently in the product's main admin page.
16455 *
16456 * @author Vova Feldman (@svovaf)
16457 * @since 2.3.1
16458 *
16459 * @return bool
16460 */
16461 function is_main_admin_page() {
16462 return $this->is_admin_page( '' );
16463 }
16464
16465 /**
16466 * Get module's main admin setting page URL.
16467 *
16468 * @author Vova Feldman (@svovaf)
16469 * @since 1.2.2.7
16470 *
16471 * @return string
16472 */
16473 function main_menu_url() {
16474 return $this->_menu->main_menu_url();
16475 }
16476
16477 /**
16478 * Check if currently on the theme's setting page or
16479 * on any of the Freemius added pages (via tabs).
16480 *
16481 * @author Vova Feldman (@svovaf)
16482 * @since 1.2.2.7
16483 *
16484 * @return bool
16485 *
16486 * @deprecated Please use is_product_settings_page() instead;
16487 */
16488 function is_theme_settings_page() {
16489 return $this->is_product_settings_page();
16490 }
16491
16492 /**
16493 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
16494 *
16495 * @author Vova Feldman (@svovaf)
16496 * @since 1.2.2.7
16497 *
16498 * @return bool
16499 */
16500 function is_product_settings_page() {
16501 $page = fs_request_get( 'page', '', 'get' );
16502 $menu_slug = $this->_menu->get_slug();
16503
16504 if ( $page === $menu_slug ) {
16505 return true;
16506 }
16507
16508 return fs_starts_with(
16509 // e.g., {$menu_slug}-account, {$menu_slug}-affiliation, etc.
16510 $page,
16511 ( $menu_slug . '-' )
16512 );
16513 }
16514
16515 /**
16516 * Plugin's account page + sync license URL.
16517 *
16518 * @author Vova Feldman (@svovaf)
16519 * @since 1.1.9.1
16520 *
16521 * @param bool|number $plugin_id
16522 * @param bool $add_action_nonce
16523 * @param array $params
16524 *
16525 * @return string
16526 */
16527 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
16528 if ( is_numeric( $plugin_id ) ) {
16529 $params['plugin_id'] = $plugin_id;
16530 }
16531
16532 return $this->get_account_url(
16533 $this->get_unique_affix() . '_sync_license',
16534 $params,
16535 $add_action_nonce
16536 );
16537 }
16538
16539 /**
16540 * Plugin's account URL.
16541 *
16542 * @author Vova Feldman (@svovaf)
16543 * @since 1.0.4
16544 *
16545 * @param bool|string $action
16546 * @param array $params
16547 *
16548 * @param bool $add_action_nonce
16549 *
16550 * @return string
16551 */
16552 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
16553 if ( is_string( $action ) ) {
16554 $params['fs_action'] = $action;
16555 }
16556
16557 self::require_pluggable_essentials();
16558
16559 return ( $add_action_nonce && is_string( $action ) ) ?
16560 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
16561 $this->_get_admin_page_url( 'account', $params );
16562 }
16563
16564 /**
16565 * @author Vova Feldman (@svovaf)
16566 * @since 1.2.0
16567 *
16568 * @param string $tab
16569 * @param bool $action
16570 * @param array $params
16571 * @param bool $add_action_nonce
16572 *
16573 * @return string
16574 *
16575 * @uses get_account_url()
16576 */
16577 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
16578 $params['tab'] = $tab;
16579
16580 return $this->get_account_url( $action, $params, $add_action_nonce );
16581 }
16582
16583 /**
16584 * Plugin's account URL.
16585 *
16586 * @author Vova Feldman (@svovaf)
16587 * @since 1.0.4
16588 *
16589 * @param bool|string $topic
16590 * @param bool|string $message
16591 * @param bool|string $summary Since 2.5.1.
16592 *
16593 * @return string
16594 */
16595 function contact_url( $topic = false, $message = false, $summary = false ) {
16596 $params = array();
16597 if ( is_string( $topic ) ) {
16598 $params['topic'] = $topic;
16599 }
16600 if ( is_string( $message ) ) {
16601 $params['message'] = $message;
16602 }
16603
16604 if ( is_string( $summary ) ) {
16605 $params['summary'] = $summary;
16606 }
16607
16608 if ( $this->is_addon() ) {
16609 $params['addon_id'] = $this->get_id();
16610
16611 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
16612 } else {
16613 return $this->_get_admin_page_url( 'contact', $params );
16614 }
16615 }
16616
16617 /**
16618 * Add-on direct info URL.
16619 *
16620 * @author Vova Feldman (@svovaf)
16621 * @since 1.1.0
16622 *
16623 * @param string $slug
16624 *
16625 * @return string
16626 */
16627 function addon_url( $slug ) {
16628 return $this->_get_admin_page_url( 'addons', array(
16629 'slug' => $slug
16630 ) );
16631 }
16632
16633 /**
16634 * Add-ons URL.
16635 *
16636 * @author Vova Feldman (@svovaf)
16637 * @since 2.4.5
16638 *
16639 * @return string
16640 */
16641 function get_addons_url() {
16642 return $this->_get_admin_page_url( 'addons' );
16643 }
16644
16645 /* Logger
16646 ------------------------------------------------------------------------------------------------------------------*/
16647 /**
16648 * @param string $id
16649 * @param bool $prefix_slug
16650 *
16651 * @return FS_Logger
16652 */
16653 function get_logger( $id = '', $prefix_slug = true ) {
16654 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16655 }
16656
16657 /**
16658 * Note: This method is used externally so don't delete it.
16659 *
16660 * @param $id
16661 * @param bool $load_options
16662 * @param bool $prefix_slug
16663 *
16664 * @return FS_Option_Manager
16665 */
16666 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16667 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16668 }
16669
16670 /* Security
16671 ------------------------------------------------------------------------------------------------------------------*/
16672 private static function _encrypt( $str ) {
16673 if ( is_null( $str ) ) {
16674 return null;
16675 }
16676
16677 /**
16678 * The encrypt/decrypt functions are used to protect
16679 * the user from messing up with some of the sensitive
16680 * data stored for the module as a JSON in the database.
16681 *
16682 * I used the same suggested hack by the theme review team.
16683 * For more details, look at the function `Base64UrlDecode()`
16684 * in `./sdk/FreemiusBase.php`.
16685 *
16686 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16687 *
16688 * @author Vova Feldman (@svovaf)
16689 * @since 1.2.2
16690 */
16691 $fn = 'base64' . '_encode';
16692
16693 return $fn( $str );
16694 }
16695
16696 static function _decrypt( $str ) {
16697 if ( is_null( $str ) ) {
16698 return null;
16699 }
16700
16701 /**
16702 * The encrypt/decrypt functions are used to protect
16703 * the user from messing up with some of the sensitive
16704 * data stored for the module as a JSON in the database.
16705 *
16706 * I used the same suggested hack by the theme review team.
16707 * For more details, look at the function `Base64UrlDecode()`
16708 * in `./sdk/FreemiusBase.php`.
16709 *
16710 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16711 *
16712 * @author Vova Feldman (@svovaf)
16713 * @since 1.2.2
16714 */
16715 $fn = 'base64' . '_decode';
16716
16717 return $fn( $str );
16718 }
16719
16720 /**
16721 * @author Vova Feldman (@svovaf)
16722 * @since 1.0.5
16723 *
16724 * @param FS_Entity $entity
16725 *
16726 * @return FS_Entity Return an encrypted clone entity.
16727 */
16728 private static function _encrypt_entity( FS_Entity $entity ) {
16729 $clone = clone $entity;
16730 $props = get_object_vars( $entity );
16731
16732 foreach ( $props as $key => $val ) {
16733 $clone->{$key} = self::_encrypt( $val );
16734 }
16735
16736 return $clone;
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 decrypted clone entity.
16746 */
16747 private static function decrypt_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::_decrypt( $val );
16753 }
16754
16755 return $clone;
16756 }
16757
16758 /**
16759 * @author Vova Feldman (@svovaf)
16760 * @since 1.0.7
16761 *
16762 * @param string $email
16763 *
16764 * @return FS_User|false
16765 */
16766 public static function _get_user_by_email( $email ) {
16767 self::$_static_logger->entrance();
16768
16769 $email = trim( strtolower( $email ) );
16770
16771 $users = self::get_all_users();
16772
16773 if ( is_array( $users ) ) {
16774 foreach ( $users as $user ) {
16775 if ( $email === trim( strtolower( $user->email ) ) ) {
16776 return $user;
16777 }
16778 }
16779 }
16780
16781 return false;
16782 }
16783
16784 #----------------------------------------------------------------------------------
16785 #region Account (Loading, Updates & Activation)
16786 #----------------------------------------------------------------------------------
16787
16788 /***
16789 * Load account information (user + site).
16790 *
16791 * @author Vova Feldman (@svovaf)
16792 * @since 1.0.1
16793 */
16794 private function _load_account() {
16795 $this->_logger->entrance();
16796
16797 $this->do_action( 'before_account_load' );
16798
16799 $users = self::get_all_users();
16800 $plans = self::get_all_plans( $this->_module_type );
16801
16802 if ( $this->_logger->is_on() && is_admin() ) {
16803 $this->_logger->log( 'users = ' . var_export( $users, true ) );
16804 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
16805 }
16806
16807 $site = fs_is_network_admin() ?
16808 $this->get_network_install() :
16809 $this->get_install_by_blog_id();
16810
16811 if ( fs_is_network_admin() &&
16812 $this->is_network_active() &&
16813 ! is_object( $site ) &&
16814 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
16815 ) {
16816 $first_install = $this->find_first_install();
16817
16818 if ( is_null( $first_install ) ) {
16819 unset( $this->_storage->network_install_blog_id );
16820 } else {
16821 $site = $first_install['install'];
16822 $this->_storage->network_install_blog_id = $first_install['blog_id'];
16823 }
16824 }
16825
16826 if ( is_object( $site ) &&
16827 is_numeric( $site->id ) &&
16828 is_numeric( $site->user_id ) &&
16829 FS_Plugin_Plan::is_valid_id( $site->plan_id )
16830 ) {
16831 // Load site.
16832 $this->_site = $site;
16833 }
16834
16835 $user = null;
16836 if ( fs_is_network_admin() && $this->_is_network_active ) {
16837 $user = $this->get_network_user();
16838 }
16839
16840 if ( is_object( $user ) ) {
16841 $this->_user = clone $user;
16842 } else if ( $this->_site ) {
16843 $user = self::_get_user_by_id( $this->_site->user_id );
16844
16845 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
16846 /**
16847 * Try to load the previous owner. This recovery is used for the following use-case:
16848 * 1. Opt-in
16849 * 2. Cloning site1 to site2
16850 * 3. Ownership switch in site1 (same applies for site2)
16851 * 4. Install data sync on site2
16852 * 5. Now site2's install is associated with the new owner which does not exists locally.
16853 */
16854 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
16855 }
16856
16857 if ( ! is_object( $user ) ) {
16858 /**
16859 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
16860 */
16861 if (
16862 ! isset( $this->_storage->user_recovery_from_install_last_attempt_timestamp ) ||
16863 time() > ( $this->_storage->user_recovery_from_install_last_attempt_timestamp + FS_Clone_Manager::CLONE_RESOLUTION_MAX_EXECUTION_TIME )
16864 ) {
16865 $user = $this->sync_user_by_current_install();
16866 } else {
16867 return;
16868 }
16869
16870 if ( is_object( $user ) ) {
16871 $this->_storage->user_was_recovered_from_install = true;
16872 } else {
16873 $this->_storage->user_recovery_from_install_attempts = isset( $this->_storage->user_recovery_from_install_attempts ) ?
16874 ( $this->_storage->user_recovery_from_install_attempts + 1 ) :
16875 1;
16876
16877 if ( $this->_storage->user_recovery_from_install_attempts >= 3 ) {
16878 $this->delete_current_install( false );
16879 } else {
16880 $this->_storage->user_recovery_from_install_last_attempt_timestamp = time();
16881
16882 return;
16883 }
16884 }
16885 }
16886
16887 $this->_user = ( $user instanceof FS_User ) ?
16888 clone $user :
16889 null;
16890 }
16891
16892 if ( is_object( $this->_user ) ) {
16893 // Load licenses.
16894 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16895 }
16896
16897 if ( is_object( $this->_site ) ) {
16898 // Load plans.
16899 $this->_plans = isset( $plans[ $this->_slug ] ) ?
16900 $plans[ $this->_slug ] :
16901 array();
16902
16903 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
16904 $this->_sync_plans();
16905 } else {
16906 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16907 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
16908 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
16909 } else {
16910 unset( $this->_plans[ $i ] );
16911 }
16912 }
16913 }
16914
16915 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
16916
16917 if ( $this->_site->version != $this->get_plugin_version() ) {
16918 // If stored install version is different than current installed plugin version,
16919 // then update plugin version event.
16920 $this->update_plugin_version_event();
16921 }
16922 }
16923
16924 if ( true === $this->_storage->require_license_activation &&
16925 ! fs_request_get_bool( 'require_license', true )
16926 ) {
16927 $this->_storage->require_license_activation = false;
16928 }
16929
16930 if ( $this->is_theme() ) {
16931 $this->_register_account_hooks();
16932 }
16933
16934 if ( $this->is_user_in_admin() && $this->is_clone() ) {
16935 if ( empty( FS_Clone_Manager::instance()->get_clone_identification_timestamp() ) ) {
16936 FS_Clone_Manager::instance()->store_clone_identification_timestamp();
16937 }
16938
16939 FS_Clone_Manager::instance()->maybe_update_clone_resolution_support_flag( $this->_storage->sdk_last_version );
16940 $this->send_pending_clone_update_once();
16941 }
16942 }
16943
16944 /**
16945 * Special user recovery mechanism.
16946 *
16947 * @author Vova Feldman (@svovaf)
16948 * @since 2.0.0
16949 *
16950 * @param number|null $site_user_id
16951 *
16952 * @return \FS_User|mixed
16953 */
16954 private function sync_user_by_current_install( $site_user_id = null ) {
16955 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
16956 $site_user_id :
16957 $this->_site->user_id;
16958
16959 $api = $this->get_api_site_scope();
16960
16961 $uid = $this->get_anonymous_id();
16962 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
16963
16964 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
16965
16966 if ( $this->is_api_result_entity( $result ) ) {
16967 $user = new FS_User( $result );
16968 $this->_user = $user;
16969 $this->_store_user();
16970
16971 return $user;
16972 }
16973
16974 $error_code = FS_Api::get_error_code( $result );
16975
16976 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
16977 /**
16978 * Those API errors will continue coming and are not recoverable with the
16979 * current site's data. Therefore, extend the API call's cached result to 7 days.
16980 */
16981 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
16982 }
16983
16984 return $result;
16985 }
16986
16987 /**
16988 * @author Vova Feldman (@svovaf)
16989 * @since 1.0.1
16990 *
16991 * @param FS_User $user
16992 * @param FS_Site $site
16993 * @param bool|array $plans
16994 */
16995 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
16996 $site->user_id = $user->id;
16997
16998 $this->_site = $site;
16999 $this->_user = $user;
17000 if ( false !== $plans ) {
17001 $this->_plans = $plans;
17002 }
17003
17004 $this->send_install_update();
17005
17006 $this->_store_account();
17007
17008 }
17009
17010 /**
17011 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
17012 * Each version is trimmed after the 16th char.
17013 *
17014 * @author Vova Feldman (@svovaf)
17015 * @since 2.2.1
17016 *
17017 * @return array
17018 */
17019 private function get_versions() {
17020 $versions = array();
17021 $versions['sdk_version'] = $this->version;
17022
17023 // Collect these diagnostic information only if it's allowed.
17024 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17025 $versions['platform_version'] = get_bloginfo( 'version' );
17026 $versions['programming_language_version'] = phpversion();
17027 }
17028
17029 foreach ( $versions as $k => $version ) {
17030 $versions[ $k ] = self::get_api_sanitized_property( $k, $version );
17031 }
17032
17033 return $versions;
17034 }
17035
17036 /**
17037 * Get sanitized site language.
17038 *
17039 * @param string $language
17040 * @param int $max_len
17041 *
17042 * @since 2.5.1
17043 * @author Vova Feldman (@svovaf)
17044 *
17045 * @return string
17046 */
17047 private static function get_sanitized_language( $language = '', $max_len = self::LANGUAGE_MAX_CHARS ) {
17048 if ( empty( $language ) ) {
17049 $language = get_bloginfo( 'language' );
17050 }
17051
17052 return substr( $language, 0, $max_len );
17053 }
17054
17055 /**
17056 * Get core version stripped from pre-release and build.
17057 *
17058 * @since 2.5.1
17059 * @author Vova Feldman (@svovaf)
17060 *
17061 * @param string $version
17062 * @param int $parts
17063 * @param int $max_len
17064 * @param bool $include_pre_release
17065 *
17066 * @return string
17067 */
17068 private static function get_core_version(
17069 $version,
17070 $parts = 3,
17071 $max_len = self::VERSION_MAX_CHARS,
17072 $include_pre_release = false
17073 ) {
17074 if ( empty( $version ) ) {
17075 // Version is empty.
17076 return '';
17077 }
17078
17079 if ( is_numeric( $version ) ) {
17080 $is_float_version = is_float( $version );
17081
17082 $version = (string) $version;
17083
17084 /**
17085 * 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.
17086 */
17087 if ( $is_float_version && false === strpos( $version, '.' ) ) {
17088 $version .= '.0';
17089 }
17090 }
17091
17092 if ( ! is_string( $version ) ) {
17093 return '';
17094 }
17095
17096 if ( $parts < 1 ) {
17097 return '';
17098 }
17099
17100 $pre_release_regex = $include_pre_release ?
17101 '(\-(alpha|beta|RC)([0-9]+)?)?' :
17102 '';
17103
17104 if ( 0 === preg_match( '/^([0-9]+(\.[0-9]+){0,' . ( $parts - 1 ) . '}' . $pre_release_regex . ')/i', $version, $matches ) ) {
17105 // Version is not starting with a digit.
17106 return '';
17107 }
17108
17109 return substr( $matches[1], 0, $max_len );
17110 }
17111
17112 /**
17113 * @param string $prop
17114 * @param mixed $val
17115 *
17116 * @return mixed
17117 *@author Vova Feldman (@svovaf)
17118 *
17119 * @since 2.5.1
17120 */
17121 private static function get_api_sanitized_property( $prop, $val ) {
17122 if ( ! is_string( $val ) || empty( $val ) ) {
17123 return $val;
17124 }
17125
17126 switch ( $prop ) {
17127 case 'programming_language_version':
17128 // Get core PHP version, which can have up to 3 parts (ignore pre-releases).
17129 return self::get_core_version( $val );
17130 case 'platform_version':
17131 // Get the exact WordPress version, which can have up to 3 parts (including pre-releases).
17132 return self::get_core_version( $val, 3, self::VERSION_MAX_CHARS, true );
17133 case 'sdk_version':
17134 // Get the exact SDK version, which can have up to 4 parts.
17135 return self::get_core_version( $val, 4 );
17136 case 'version':
17137 // Get the entire version but just limited in length.
17138 return substr( $val, 0, self::VERSION_MAX_CHARS );
17139 case 'language':
17140 return self::get_sanitized_language( $val );
17141 default:
17142 return $val;
17143 }
17144 }
17145
17146 /**
17147 * @author Leo Fajardo (@leorw)
17148 * @since 2.3.0
17149 *
17150 * @return bool
17151 */
17152 function has_beta_update() {
17153 return (
17154 ! empty( $this->_storage->beta_data ) &&
17155 ( true === $this->_storage->beta_data['is_beta'] ) &&
17156 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
17157 );
17158 }
17159
17160 /**
17161 * @author Leo Fajardo (@leorw)
17162 * @since 2.3.0
17163 *
17164 * @return bool
17165 */
17166 function is_beta() {
17167 return (
17168 ! empty( $this->_storage->beta_data ) &&
17169 ( true === $this->_storage->beta_data['is_beta'] ) &&
17170 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
17171 );
17172 }
17173
17174 /**
17175 * @author Vova Feldman (@svovaf)
17176 * @since 1.1.7.4
17177 *
17178 * @param array $override_with
17179 * @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.
17180 *
17181 * @return array
17182 */
17183 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
17184 $this->_logger->entrance();
17185
17186 $current_user = self::_get_current_wp_user();
17187
17188 $activation_action = $this->get_unique_affix() . '_activate_new';
17189 $return_url = $this->is_anonymous() ?
17190 // If skipped already, then return to the account page.
17191 $this->get_account_url( $activation_action, array(), false ) :
17192 // Return to the module's main page.
17193 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
17194
17195 $versions = $this->get_versions();
17196
17197 $params = array_merge( $versions, array(
17198 'user_firstname' => $current_user->user_firstname,
17199 'user_lastname' => $current_user->user_lastname,
17200 'user_email' => $current_user->user_email,
17201 'plugin_slug' => $this->_slug,
17202 'plugin_id' => $this->get_id(),
17203 'plugin_public_key' => $this->get_public_key(),
17204 'plugin_version' => $this->get_plugin_version(),
17205 'return_url' => fs_nonce_url( $return_url, $activation_action ),
17206 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
17207 'account',
17208 array( 'fs_action' => 'sync_user' )
17209 ), 'sync_user' ),
17210 'is_premium' => $this->is_premium(),
17211 'is_active' => true,
17212 'is_uninstalled' => false,
17213 'is_localhost' => WP_FS__IS_LOCALHOST,
17214 ) );
17215
17216 if ( $this->is_addon() ) {
17217 $parent_fs = $this->get_parent_instance();
17218
17219 $params['parent_plugin_slug'] = $parent_fs->_slug;
17220 $params['parent_plugin_id'] = $parent_fs->get_id();
17221 }
17222
17223 if ( true === $network_level_or_blog_id ) {
17224 if ( ! isset( $override_with['sites'] ) ) {
17225 $params['sites'] = $this->get_sites_for_network_level_optin();
17226 }
17227 } else {
17228 $site = is_numeric( $network_level_or_blog_id ) ?
17229 array( 'blog_id' => $network_level_or_blog_id ) :
17230 null;
17231
17232 $site = $this->get_site_info( $site );
17233
17234 $diagnostic_info = array();
17235 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17236 $diagnostic_info = array(
17237 'site_name' => $site['title'],
17238 'language' => self::get_sanitized_language( $site['language'] ),
17239 );
17240 }
17241
17242 $params = array_merge( $params, $diagnostic_info, array(
17243 'site_uid' => $site['uid'],
17244 'site_url' => $site['url'],
17245 ) );
17246 }
17247
17248 if ( $this->is_pending_activation() &&
17249 ! empty( $this->_storage->pending_license_key )
17250 ) {
17251 $params['license_key'] = $this->_storage->pending_license_key;
17252 }
17253
17254 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
17255 // Even though rand() is known for its security issues,
17256 // the timestamp adds another layer of protection.
17257 // It would be very hard for an attacker to get the secret key form here.
17258 // Plus, this should never run in production since the secret should never
17259 // be included in the production version.
17260 $params['ts'] = WP_FS__SCRIPT_START_TIME;
17261 $params['salt'] = md5( uniqid( rand() ) );
17262 $params['secure'] = md5(
17263 $params['ts'] .
17264 $params['salt'] .
17265 $this->get_secret_key()
17266 );
17267 }
17268
17269 if ( is_multisite() && function_exists( 'get_network' ) ) {
17270 $params['network_uid'] = $this->get_anonymous_network_id();
17271 }
17272
17273 return array_merge( $params, $override_with );
17274 }
17275
17276 /**
17277 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
17278 * 2. If there was an API error, return the API result.
17279 *
17280 * @author Vova Feldman (@svovaf)
17281 * @since 1.1.7.4
17282 *
17283 * @param string|bool $email
17284 * @param string|bool $first
17285 * @param string|bool $last
17286 * @param string|bool $license_key
17287 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
17288 * In this case, the user and site info will be sent to the server but no
17289 * data will be saved to the WP installation's database.
17290 * @param number|bool $trial_plan_id
17291 * @param bool $is_disconnected Whether to opt in without tracking.
17292 * @param null|bool $is_marketing_allowed
17293 * @param array $sites If network-level opt-in, an array of containing details of sites.
17294 * @param bool $redirect
17295 *
17296 * @return string|object
17297 * @use WP_Error
17298 */
17299 function opt_in(
17300 $email = false,
17301 $first = false,
17302 $last = false,
17303 $license_key = false,
17304 $is_uninstall = false,
17305 $trial_plan_id = false,
17306 $is_disconnected = false,
17307 $is_marketing_allowed = null,
17308 $sites = array(),
17309 $redirect = true
17310 ) {
17311 $this->_logger->entrance();
17312
17313 if ( false === $email ) {
17314 $current_user = self::_get_current_wp_user();
17315 $email = $current_user->user_email;
17316 }
17317
17318 /**
17319 * @since 1.2.1 If activating with license key, ignore the context-user
17320 * since the user will be automatically loaded from the license.
17321 */
17322 if ( empty( $license_key ) ) {
17323 // Clean up pending license if opt-ing in again.
17324 $this->_storage->remove( 'pending_license_key' );
17325
17326 if ( ! $is_uninstall ) {
17327 $fs_user = Freemius::_get_user_by_email( $email );
17328 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
17329 return $this->install_with_user(
17330 $fs_user,
17331 false,
17332 $trial_plan_id,
17333 $redirect,
17334 true,
17335 $sites
17336 );
17337 }
17338 }
17339 }
17340
17341 $user_info = array();
17342 if ( ! empty( $email ) ) {
17343 $user_info['user_email'] = $email;
17344 }
17345 if ( ! empty( $first ) ) {
17346 $user_info['user_firstname'] = $first;
17347 }
17348 if ( ! empty( $last ) ) {
17349 $user_info['user_lastname'] = $last;
17350 }
17351
17352 if ( ! empty( $sites ) ) {
17353 $is_network = true;
17354
17355 $user_info['sites'] = $sites;
17356 } else {
17357 $is_network = false;
17358 }
17359
17360 $params = $this->get_opt_in_params( $user_info, $is_network );
17361
17362 $filtered_license_key = false;
17363 if ( is_string( $license_key ) ) {
17364 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
17365 $params['license_key'] = $filtered_license_key;
17366 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17367 $params['trial_plan_id'] = $trial_plan_id;
17368 }
17369
17370 if ( $is_uninstall ) {
17371 $params['uninstall_params'] = array(
17372 'reason_id' => $this->_storage->uninstall_reason->id,
17373 'reason_info' => $this->_storage->uninstall_reason->info
17374 );
17375 }
17376
17377 if ( isset( $params['license_key'] ) ) {
17378 $fs_user = Freemius::_get_user_by_email( $email );
17379
17380 if ( is_object( $fs_user ) ) {
17381 /**
17382 * If opting in with a context license and the context WP Admin user already opted in
17383 * before from the current site, add the user context security params to avoid the
17384 * unnecessary email activation when the context license is owned by the same context user.
17385 *
17386 * @author Leo Fajardo (@leorw)
17387 * @since 1.2.3
17388 */
17389 $params = array_merge( $params, FS_Security::instance()->get_context_params(
17390 $fs_user,
17391 false,
17392 'install_with_existing_user'
17393 ) );
17394 }
17395 }
17396
17397 if ( is_bool( $is_marketing_allowed ) ) {
17398 $params['is_marketing_allowed'] = $is_marketing_allowed;
17399 }
17400
17401 $params['is_disconnected'] = $is_disconnected;
17402 $params['format'] = 'json';
17403 $params['is_extensions_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed();
17404 $params['is_diagnostic_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed();
17405
17406 $request = array(
17407 'method' => 'POST',
17408 'body' => $params,
17409 'timeout' => 60,
17410 );
17411
17412 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
17413 $response = self::safe_remote_post( $url, $request );
17414
17415 if ( is_wp_error( $response ) ) {
17416 /**
17417 * @var WP_Error $response
17418 */
17419 $result = new stdClass();
17420
17421 $error_code = $response->get_error_code();
17422 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
17423
17424 $result->error = (object) array(
17425 'type' => $error_type,
17426 'message' => $response->get_error_message(),
17427 'code' => $error_code,
17428 'http' => 402
17429 );
17430
17431 $this->maybe_modify_api_curl_error_message( $result );
17432
17433 if ( FS_Api::is_blocked( $result ) ) {
17434 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
17435 }
17436
17437 $is_connected = null;
17438
17439 if ( empty( $license_key ) && $this->is_enable_anonymous() ) {
17440 $this->skip_connection( fs_is_network_admin() );
17441
17442 $is_connected = ( ! FS_Api::is_blocked( $result ) );
17443 }
17444
17445 $this->update_connectivity_info( $is_connected );
17446
17447 return $result;
17448 }
17449
17450 $this->update_connectivity_info( true );
17451
17452 // Module is being uninstalled, don't handle the returned data.
17453 if ( $is_uninstall ) {
17454 return true;
17455 }
17456
17457 /**
17458 * 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.
17459 *
17460 * @author Vova Feldman (@svovaf)
17461 * @since 1.2.3
17462 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
17463 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
17464 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
17465 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
17466 */
17467 $decoded = is_string( $response['body'] ) ?
17468 json_decode( $response['body'] ) :
17469 null;
17470
17471 if ( empty( $decoded ) ) {
17472 return false;
17473 }
17474
17475 if ( ! $this->is_api_result_object( $decoded ) ) {
17476 if ( ! empty( $params['license_key'] ) ) {
17477 // Pass the fully entered license key to the failure handler.
17478 $params['license_key'] = $license_key;
17479 }
17480
17481 return $is_uninstall ?
17482 $decoded :
17483 $this->apply_filters( 'after_install_failure', $decoded, $params );
17484 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
17485 if ( $is_network ) {
17486 $site_ids = array();
17487 foreach ( $sites as $site ) {
17488 $site_ids[] = $site['blog_id'];
17489 }
17490
17491 /**
17492 * Store the sites so that they can be installed once the user has clicked on the activation link
17493 * in the email.
17494 *
17495 * @author Leo Fajardo (@leorw)
17496 */
17497 $this->_storage->pending_sites_info = array(
17498 'blog_ids' => $site_ids,
17499 'license_key' => $license_key,
17500 'trial_plan_id' => $trial_plan_id
17501 );
17502 }
17503
17504 // Pending activation, add message.
17505 return $this->set_pending_confirmation(
17506 ( isset( $decoded->email ) ?
17507 $decoded->email :
17508 true ),
17509 false,
17510 $filtered_license_key,
17511 ! empty( $params['trial_plan_id'] ),
17512 isset( $decoded->is_suspicious_email ) && $decoded->is_suspicious_email
17513 );
17514 } else if ( isset( $decoded->install_secret_key ) ) {
17515 return $this->install_with_new_user(
17516 $decoded->user_id,
17517 $decoded->user_public_key,
17518 $decoded->user_secret_key,
17519 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17520 $decoded->is_marketing_allowed :
17521 null ),
17522 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17523 $decoded->is_extensions_tracking_allowed :
17524 null ),
17525 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17526 $decoded->is_diagnostic_tracking_allowed :
17527 null ),
17528 $decoded->install_id,
17529 $decoded->install_public_key,
17530 $decoded->install_secret_key,
17531 false
17532 );
17533 } else if ( is_array( $decoded->installs ) ) {
17534 return $this->install_many_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->installs,
17548 false
17549 );
17550 }
17551
17552 return $decoded;
17553 }
17554
17555 /**
17556 * Set user and site identities.
17557 *
17558 * @author Vova Feldman (@svovaf)
17559 * @since 1.0.9
17560 *
17561 * @param FS_User $user
17562 * @param FS_Site $site
17563 * @param bool $redirect
17564 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
17565 * redirect (or return a URL) to the account page with a special parameter to
17566 * trigger the auto installation processes.
17567 *
17568 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17569 */
17570 function setup_account(
17571 FS_User $user,
17572 FS_Site $site,
17573 $redirect = true,
17574 $auto_install = false
17575 ) {
17576 return $this->setup_network_account(
17577 $user,
17578 array( $site ),
17579 $redirect,
17580 $auto_install,
17581 false
17582 );
17583 }
17584
17585 /**
17586 * Set user and site identities.
17587 *
17588 * @author Vova Feldman (@svovaf)
17589 * @since 2.0.0
17590 *
17591 * @param FS_User $user
17592 * @param FS_Site[] $installs
17593 * @param bool $redirect
17594 * @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.
17595 * @param bool $is_network_level_opt_in
17596 *
17597 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17598 */
17599 function setup_network_account(
17600 FS_User $user,
17601 array $installs,
17602 $redirect = true,
17603 $auto_install = false,
17604 $is_network_level_opt_in = true
17605 ) {
17606 $first_install = $installs[0];
17607
17608 $this->_user = $user;
17609 $this->_site = $first_install;
17610
17611 $this->_sync_plans();
17612
17613 if ( $this->_storage->handle_gdpr_admin_notice &&
17614 $this->should_handle_gdpr_admin_notice() &&
17615 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
17616 ) {
17617 /**
17618 * Clear user lock after an opt-in.
17619 */
17620 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
17621 FS_User_Lock::instance()->unlock();
17622 }
17623
17624 if ( 1 < count( $installs ) ) {
17625 // Only network level opt-in can have more than one install.
17626 $is_network_level_opt_in = true;
17627 }
17628
17629 $this->update_connectivity_info( true );
17630
17631 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
17632 // If Freemius was OFF before, turn it on.
17633 $this->turn_on();
17634
17635 $this->handle_account_connection(
17636 $installs,
17637 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
17638 );
17639
17640 if ( is_numeric( $first_install->license_id ) ) {
17641 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
17642 }
17643
17644 $this->_admin_notices->remove_sticky( 'connect_account' );
17645
17646 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
17647 $this->clear_pending_activation_mode();
17648
17649 if ( ! $this->is_paying_or_trial() ) {
17650 $this->_admin_notices->add_sticky(
17651 sprintf( $this->get_text_inline( '%s opt-in was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
17652 'activation_complete'
17653 );
17654 }
17655 }
17656
17657 if ( $this->is_paying_or_trial() ) {
17658 if ( ! $this->is_premium() ||
17659 ! $this->has_premium_version() ||
17660 ! $this->has_settings_menu()
17661 ) {
17662 if ( $this->is_paying() ) {
17663 $this->add_complete_upgrade_instructions_notice(
17664 sprintf(
17665 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
17666 $this->get_plan_title()
17667 ),
17668 'plan_upgraded'
17669 );
17670 } else {
17671 $trial_plan = $this->get_trial_plan();
17672
17673 $this->add_complete_upgrade_instructions_notice(
17674 sprintf(
17675 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
17676 '<i>' . $this->get_plugin_name() . '</i>'
17677 ),
17678 'trial_started',
17679 $trial_plan->title
17680 );
17681 }
17682 }
17683
17684 $this->_admin_notices->remove_sticky( array(
17685 'trial_promotion',
17686 ) );
17687 }
17688
17689 $plugin_id = fs_request_get( 'plugin_id', false );
17690
17691 // Store activation time ONLY for plugins & themes (not add-ons).
17692 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
17693 if ( empty( $this->_storage->activation_timestamp ) ) {
17694 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
17695 }
17696 }
17697
17698 $next_page = '';
17699
17700 $extra = array();
17701 if ( $auto_install ) {
17702 $extra['auto_install'] = 'true';
17703 }
17704
17705 if ( is_numeric( $plugin_id ) ) {
17706 /**
17707 * @author Leo Fajardo (@leorw)
17708 * @since 1.2.1.6
17709 *
17710 * Also sync the license after an anonymous user subscribes.
17711 */
17712 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
17713 // Add-on was installed - sync license right after install.
17714 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
17715 }
17716 } else {
17717 /**
17718 * @author Vova Feldman (@svovaf)
17719 * @since 1.1.9 If site installed with a valid license, sync license.
17720 */
17721 if ( $this->is_paying() ) {
17722 $this->_sync_plugin_license(
17723 true,
17724 // Installs data is already synced in the beginning of this method directly or via _set_account().
17725 false
17726 );
17727 }
17728
17729 // Reload the page with the keys.
17730 $next_page = $this->is_anonymous() ?
17731 // If user previously skipped, redirect to account page.
17732 $this->get_account_url( false, $extra ) :
17733 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
17734 }
17735
17736 if ( ! empty( $next_page ) && $redirect ) {
17737 fs_redirect( $next_page );
17738 }
17739
17740 return $next_page;
17741 }
17742
17743 /**
17744 * Install plugin with new user information after approval.
17745 *
17746 * @author Vova Feldman (@svovaf)
17747 * @since 1.0.7
17748 */
17749 function _install_with_new_user() {
17750 $this->_logger->entrance();
17751
17752 if ( $this->is_registered() ) {
17753 return;
17754 }
17755
17756 $has_pending_activation_confirmation_param = fs_request_has( 'pending_activation' );
17757
17758 $this->update_license_required_permissions_if_anonymous();
17759
17760 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17761 // @todo This logic should be improved because it's executed on every load of a theme.
17762 $this->is_theme()
17763 ) {
17764 // check_admin_referer( $this->_slug . '_activate_new' );
17765
17766 if ( fs_request_has( 'user_secret_key' ) ) {
17767 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17768 $pending_sites_info = $this->_storage->pending_sites_info;
17769
17770 $this->install_many_pending_with_user(
17771 fs_request_get( 'user_id' ),
17772 fs_request_get_raw( 'user_public_key' ),
17773 fs_request_get_raw( 'user_secret_key' ),
17774 fs_request_get_bool( 'is_marketing_allowed', null ),
17775 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17776 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17777 $pending_sites_info['blog_ids'],
17778 $pending_sites_info['license_key'],
17779 $pending_sites_info['trial_plan_id']
17780 );
17781 } else {
17782 $this->install_with_new_user(
17783 fs_request_get( 'user_id' ),
17784 fs_request_get_raw( 'user_public_key' ),
17785 fs_request_get_raw( 'user_secret_key' ),
17786 fs_request_get_bool( 'is_marketing_allowed', null ),
17787 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17788 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17789 fs_request_get( 'install_id' ),
17790 fs_request_get_raw( 'install_public_key' ),
17791 fs_request_get_raw( 'install_secret_key' ),
17792 true,
17793 fs_request_get_bool( 'auto_install' )
17794 );
17795 }
17796 } else if ( $has_pending_activation_confirmation_param ) {
17797 $this->set_pending_confirmation(
17798 fs_request_get( 'user_email' ),
17799 true,
17800 false,
17801 false,
17802 fs_request_get_bool( 'is_suspicious_email' ),
17803 fs_request_get_bool( 'has_upgrade_context' ),
17804 fs_request_get( 'support_email_address' )
17805 );
17806 }
17807 }
17808 }
17809
17810 /**
17811 * @author Vova Feldman (@svovaf)
17812 * @since 2.0.0
17813 *
17814 * @param number $id
17815 * @param string $public_key
17816 * @param string $secret_key
17817 *
17818 * @return \FS_User
17819 */
17820 private function setup_user( $id, $public_key, $secret_key ) {
17821 $user = self::_get_user_by_id( $id );
17822
17823 if ( is_object( $user ) ) {
17824 $this->_user = $user;
17825 } else {
17826 $user = new FS_User();
17827 $user->id = $id;
17828 $user->public_key = $public_key;
17829 $user->secret_key = $secret_key;
17830
17831 $this->_user = $user;
17832 $user_result = $this->get_api_user_scope()->get();
17833 $user = new FS_User( $user_result );
17834
17835 $this->_user = $user;
17836 $this->_store_user();
17837 }
17838
17839 return $user;
17840 }
17841
17842 /**
17843 * Install plugin with new user.
17844 *
17845 * @author Vova Feldman (@svovaf)
17846 * @since 1.1.7.4
17847 *
17848 * @param number $user_id
17849 * @param string $user_public_key
17850 * @param string $user_secret_key
17851 * @param bool|null $is_marketing_allowed
17852 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17853 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17854 * @param number $install_id
17855 * @param string $install_public_key
17856 * @param string $install_secret_key
17857 * @param bool $redirect
17858 * @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.
17859 *
17860 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17861 */
17862 private function install_with_new_user(
17863 $user_id,
17864 $user_public_key,
17865 $user_secret_key,
17866 $is_marketing_allowed,
17867 $is_extensions_tracking_allowed,
17868 $is_diagnostic_tracking_allowed,
17869 $install_id,
17870 $install_public_key,
17871 $install_secret_key,
17872 $redirect = true,
17873 $auto_install = false
17874 ) {
17875 /**
17876 * This method is also executed after opting in with a license key since the
17877 * license can be potentially associated with a different owner.
17878 *
17879 * @since 2.0.0
17880 */
17881 $user = self::_get_user_by_id( $user_id );
17882
17883 if ( ! is_object( $user ) ) {
17884 $user = new FS_User();
17885 $user->id = $user_id;
17886 $user->public_key = $user_public_key;
17887 $user->secret_key = $user_secret_key;
17888
17889 $this->_user = $user;
17890 $user_result = $this->get_api_user_scope()->get();
17891 $user = new FS_User( $user_result );
17892 }
17893
17894 $this->_user = $user;
17895
17896 $site = new FS_Site();
17897 $site->id = $install_id;
17898 $site->public_key = $install_public_key;
17899 $site->secret_key = $install_secret_key;
17900
17901 $this->_site = $site;
17902 $site_result = $this->get_api_site_scope( true )->get();
17903 $site = new FS_Site( $site_result );
17904 $this->_site = $site;
17905
17906 if ( ! is_null( $is_marketing_allowed ) ) {
17907 $this->disable_opt_in_notice_and_lock_user();
17908 }
17909
17910 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17911 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17912 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17913 ) );
17914
17915 return $this->setup_account(
17916 $this->_user,
17917 $this->_site,
17918 $redirect,
17919 $auto_install
17920 );
17921 }
17922
17923 /**
17924 * Install plugin with user.
17925 *
17926 * @author Leo Fajardo (@leorw)
17927 * @since 2.0.0
17928 *
17929 * @param number $user_id
17930 * @param string $user_public_key
17931 * @param string $user_secret_key
17932 * @param bool|null $is_marketing_allowed
17933 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17934 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17935 * @param array $site_ids
17936 * @param bool $license_key
17937 * @param bool $trial_plan_id
17938 * @param bool $redirect
17939 *
17940 * @return void
17941 */
17942 private function install_many_pending_with_user(
17943 $user_id,
17944 $user_public_key,
17945 $user_secret_key,
17946 $is_marketing_allowed,
17947 $is_extensions_tracking_allowed,
17948 $is_diagnostic_tracking_allowed,
17949 $site_ids,
17950 $license_key = false,
17951 $trial_plan_id = false,
17952 $redirect = true
17953 ) {
17954 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17955
17956 if ( ! is_null( $is_marketing_allowed ) ) {
17957 $this->disable_opt_in_notice_and_lock_user();
17958 }
17959
17960 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17961 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17962 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17963 ) );
17964
17965 $sites = array();
17966 foreach ( $site_ids as $site_id ) {
17967 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
17968 }
17969
17970 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17971 }
17972
17973 /**
17974 * Multi-site install with a new user.
17975 *
17976 * @author Vova Feldman (@svovaf)
17977 * @since 2.0.0
17978 *
17979 * @param number $user_id
17980 * @param string $user_public_key
17981 * @param string $user_secret_key
17982 * @param bool|null $is_marketing_allowed
17983 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17984 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17985 * @param object[] $installs
17986 * @param bool $redirect
17987 * @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.
17988 *
17989 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17990 */
17991 private function install_many_with_new_user(
17992 $user_id,
17993 $user_public_key,
17994 $user_secret_key,
17995 $is_marketing_allowed,
17996 $is_extensions_tracking_allowed,
17997 $is_diagnostic_tracking_allowed,
17998 array $installs,
17999 $redirect = true,
18000 $auto_install = false
18001 ) {
18002 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
18003
18004 if ( ! is_null( $is_marketing_allowed ) ) {
18005 $this->disable_opt_in_notice_and_lock_user();
18006 }
18007
18008 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
18009 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
18010 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
18011 ) );
18012
18013 $install_ids = array();
18014
18015 foreach ( $installs as $install ) {
18016 $install_ids[] = $install->id;
18017 }
18018
18019 $items_per_request = 25;
18020 $left = count( $install_ids );
18021 $offset = 0;
18022
18023 $installs = array();
18024 while ( $left > 0 ) {
18025 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, $items_per_request ) ) );
18026
18027 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18028 // @todo Handle API error.
18029 }
18030
18031 $installs = array_merge( $installs, $result->installs );
18032
18033 $left -= $items_per_request;
18034 $offset += $items_per_request;
18035 }
18036
18037 foreach ( $installs as &$install ) {
18038 $install = new FS_Site( $install );
18039 }
18040
18041 return $this->setup_network_account(
18042 $this->_user,
18043 $installs,
18044 $redirect,
18045 $auto_install
18046 );
18047 }
18048
18049 /**
18050 * @author Vova Feldman (@svovaf)
18051 * @since 1.1.7.4
18052 *
18053 * @param string|bool $email
18054 * @param bool $redirect
18055 * @param string|bool $license_key Since 1.2.1.5
18056 * @param bool $is_pending_trial Since 1.2.1.5
18057 * @param bool $is_suspicious_email Since 2.5.0
18058 * @param bool $has_upgrade_context Since 2.5.3
18059 * @param bool|string $support_email_address Since 2.5.3
18060 *
18061 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
18062 */
18063 private function set_pending_confirmation(
18064 $email = false,
18065 $redirect = true,
18066 $license_key = false,
18067 $is_pending_trial = false,
18068 $is_suspicious_email = false,
18069 $has_upgrade_context = false,
18070 $support_email_address = false
18071 ) {
18072 $is_network_admin = fs_is_network_admin();
18073
18074 if ( $this->_ignore_pending_mode && ! $has_upgrade_context ) {
18075 /**
18076 * If explicitly asked to ignore pending mode, set to anonymous mode
18077 * 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).
18078 *
18079 * @author Vova Feldman
18080 * @since 1.2.1.6
18081 */
18082 $this->skip_connection( $is_network_admin );
18083 } else {
18084 // Install must be activated via email since
18085 // user with the same email already exist.
18086 $this->_storage->is_pending_activation = true;
18087 $this->_add_pending_activation_notice(
18088 $email,
18089 $is_pending_trial,
18090 $is_suspicious_email,
18091 $has_upgrade_context,
18092 $support_email_address
18093 );
18094 }
18095
18096 if ( ! empty( $license_key ) ) {
18097 $this->_storage->pending_license_key = $license_key;
18098 }
18099
18100 // Remove the opt-in sticky notice.
18101 $this->_admin_notices->remove_sticky( array(
18102 'connect_account',
18103 'trial_promotion',
18104 ) );
18105
18106 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
18107
18108 if ( $redirect ) {
18109 // Reload the page with a pending activation message.
18110 fs_redirect( $next_page );
18111 }
18112
18113 return $next_page;
18114 }
18115
18116 /**
18117 * Install plugin with current logged WP user info.
18118 *
18119 * @author Vova Feldman (@svovaf)
18120 * @since 1.0.7
18121 */
18122 function _install_with_current_user() {
18123 $this->_logger->entrance();
18124
18125 if ( $this->is_registered() ) {
18126 return;
18127 }
18128
18129 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
18130 check_admin_referer( $this->get_unique_affix() . '_activate_existing' );
18131
18132 /**
18133 * @author Vova Feldman (@svovaf)
18134 * @since 1.1.9 Add license key if given.
18135 */
18136 $license_key = fs_request_get_raw( 'license_secret_key' );
18137
18138 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
18139 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
18140 FS_Permission_Manager::PERMISSION_EXTENSIONS => fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
18141 ) );
18142
18143 $this->install_with_current_user( $license_key );
18144 }
18145 }
18146
18147
18148 /**
18149 * @author Vova Feldman (@svovaf)
18150 * @since 1.1.7.4
18151 *
18152 * @param string|bool $license_key
18153 * @param number|bool $trial_plan_id
18154 * @param array $sites Since 2.0.0
18155 * @param bool $redirect
18156 *
18157 * @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.
18158 */
18159 function install_with_current_user(
18160 $license_key = false,
18161 $trial_plan_id = false,
18162 $sites = array(),
18163 $redirect = true
18164 ) {
18165 // Get current logged WP user.
18166 $current_user = self::_get_current_wp_user();
18167
18168 // Find the relevant FS user by the email.
18169 $user = self::_get_user_by_email( $current_user->user_email );
18170
18171 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
18172 }
18173
18174 /**
18175 * @author Vova Feldman (@svovaf)
18176 * @since 2.0.0
18177 *
18178 * @param \FS_User $user
18179 * @param string|bool $license_key
18180 * @param number|bool $trial_plan_id
18181 * @param bool $redirect
18182 * @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.
18183 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
18184 *
18185 * @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.
18186 */
18187 function install_with_user(
18188 FS_User $user,
18189 $license_key = false,
18190 $trial_plan_id = false,
18191 $redirect = true,
18192 $setup_account = true,
18193 $sites = array()
18194 ) {
18195 // We have to set the user before getting user scope API handler.
18196 $this->_user = $user;
18197
18198 // Install the plugin.
18199 $result = $this->create_installs_with_user(
18200 $user,
18201 $license_key,
18202 $trial_plan_id,
18203 $sites,
18204 $redirect
18205 );
18206
18207 if ( ! $this->is_api_result_entity( $result ) &&
18208 ! $this->is_api_result_object( $result, 'installs' )
18209 ) {
18210 // @todo Handler potential API error of the $result
18211 }
18212
18213 if ( empty( $sites ) ) {
18214 $site = new FS_Site( $result );
18215 $this->_site = $site;
18216
18217 if ( ! $setup_account ) {
18218 $this->_store_site();
18219
18220 $this->sync_plan_if_not_exist( $site->plan_id );
18221
18222 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
18223 $this->sync_license_if_not_exist( $site->license_id, $license_key );
18224 }
18225
18226 $this->_admin_notices->remove_sticky( 'connect_account', false );
18227
18228 return $site;
18229 }
18230
18231 return $this->setup_account( $this->_user, $this->_site, $redirect );
18232 } else {
18233 $installs = array();
18234 foreach ( $result->installs as $install ) {
18235 $installs[] = new FS_Site( $install );
18236 }
18237
18238 return $this->setup_network_account(
18239 $user,
18240 $installs,
18241 $redirect
18242 );
18243 }
18244 }
18245
18246 /**
18247 * Initiate an API request to create a collection of installs.
18248 *
18249 * @author Vova Feldman (@svovaf)
18250 * @since 2.0.0
18251 *
18252 * @param \FS_User $user
18253 * @param bool $license_key
18254 * @param bool $trial_plan_id
18255 * @param array $sites
18256 * @param bool $redirect
18257 * @param bool $silent
18258 *
18259 * @return object|mixed
18260 */
18261 private function create_installs_with_user(
18262 FS_User $user,
18263 $license_key = false,
18264 $trial_plan_id = false,
18265 $sites = array(),
18266 $redirect = false,
18267 $silent = false
18268 ) {
18269 $extra_install_params = array(
18270 'uid' => $this->get_anonymous_id(),
18271 'is_disconnected' => false,
18272 );
18273
18274 if ( ! empty( $license_key ) ) {
18275 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
18276
18277 if ( $silent ) {
18278 $extra_install_params['ignore_license_owner'] = true;
18279 }
18280 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
18281 $extra_install_params['trial_plan_id'] = $trial_plan_id;
18282 }
18283
18284 if ( ! empty( $sites ) ) {
18285 $extra_install_params['sites'] = $sites;
18286 }
18287
18288 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
18289
18290 // Install the plugin.
18291 $result = $this->get_api_user_scope_by_user( $user )->call(
18292 "/plugins/{$this->get_id()}/installs.json",
18293 'post',
18294 $args
18295 );
18296
18297 if ( ! $this->is_api_result_entity( $result ) &&
18298 ! $this->is_api_result_object( $result, 'installs' )
18299 ) {
18300 if ( ! empty( $args['license_key'] ) ) {
18301 // Pass the fully entered license key to the failure handler.
18302 $args['license_key'] = $license_key;
18303 }
18304
18305 $result = $this->apply_filters( 'after_install_failure', $result, $args );
18306
18307 if ( ! $silent ) {
18308 $this->_admin_notices->add(
18309 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18310 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
18311 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18312 'error'
18313 );
18314 }
18315
18316 if ( $redirect ) {
18317 /**
18318 * We set the user before getting the user scope API handler, so the user became temporarily
18319 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
18320 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
18321 * activation page based on the return value of `is_registered()`. In addition, in case the
18322 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
18323 * misleading plugin activation errors will be shown on the `Plugins` page.
18324 *
18325 * @author Leo Fajardo (@leorw)
18326 */
18327 $this->_user = null;
18328
18329 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
18330 }
18331 }
18332
18333 return $result;
18334 }
18335
18336 /**
18337 * Tries to activate add-on account based on parent plugin info.
18338 *
18339 * @author Vova Feldman (@svovaf)
18340 * @since 1.0.6
18341 *
18342 * @param Freemius $parent_fs
18343 * @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.
18344 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
18345 */
18346 private function _activate_addon_account(
18347 Freemius $parent_fs,
18348 $network_level_or_blog_id = null,
18349 FS_Plugin_License $bundle_license = null
18350 ) {
18351 if ( $this->is_registered() ) {
18352 // Already activated.
18353 return;
18354 }
18355
18356 $permission_ids = FS_Permission_Manager::get_all_permission_ids();
18357 $permissions = array();
18358 foreach ( $permission_ids as $permission_id ) {
18359 $permissions[ $permission_id ] = FS_Permission_Manager::instance( $parent_fs )->is_permission( $permission_id, true );
18360 }
18361
18362 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( $permissions );
18363
18364 /**
18365 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
18366 * already returns the data for the current blog.
18367 *
18368 * @author Leo Fajardo (@leorw)
18369 * @since 2.3.0
18370 */
18371 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
18372 array() :
18373 array( 'uid' => $this->get_anonymous_id() );
18374
18375 $params = $this->get_install_data_for_api(
18376 $uid_param_to_override,
18377 false,
18378 false,
18379 /**
18380 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
18381 * already includes the data for it.
18382 *
18383 * @author Leo Fajardo (@leorw)
18384 * @since 2.3.0
18385 */
18386 ( true !== $network_level_or_blog_id )
18387 );
18388
18389 if ( true === $network_level_or_blog_id ) {
18390 $params['sites'] = $this->get_sites_for_network_level_optin();
18391
18392 if ( empty( $params['sites'] ) ) {
18393 return;
18394 }
18395 }
18396
18397 if ( is_object( $bundle_license ) ) {
18398 $params['license_key'] = $bundle_license->secret_key;
18399 }
18400
18401 // Activate add-on with parent plugin credentials.
18402 $result = $parent_fs->get_api_site_scope()->call(
18403 "/addons/{$this->_plugin->id}/installs.json",
18404 'post',
18405 $params
18406 );
18407
18408 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18409 if ( is_object( $bundle_license ) ) {
18410 /**
18411 * 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.
18412 *
18413 * @author Leo Fajardo (@leorw)
18414 * @since 2.4.0
18415 */
18416 } else {
18417 $error_message = FS_Api::is_api_error_object( $result ) ?
18418 $result->error->message :
18419 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
18420
18421 $this->_admin_notices->add(
18422 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18423 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
18424 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18425 'error'
18426 );
18427 }
18428
18429 return;
18430 }
18431
18432 $addon_installs = $result->installs;
18433 foreach ( $addon_installs as $key => $addon_install ) {
18434 $addon_installs[ $key ] = new FS_Site( $addon_install );
18435 }
18436
18437 $first_install = $addon_installs[0];
18438
18439 // Get user information based on parent's plugin.
18440 $user = $parent_fs->get_user();
18441
18442 // First of all, set site and user info - otherwise we won't
18443 // be able to invoke API calls.
18444 $this->_site = $first_install;
18445 $this->_user = $user;
18446
18447 // Sync add-on plans.
18448 $this->_sync_plans();
18449
18450 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
18451
18452 // Get site's current plan.
18453 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
18454
18455 // Sync licenses.
18456 $this->_sync_licenses();
18457
18458 if ( ! fs_is_network_admin() ) {
18459 // Try to activate premium license.
18460 $this->_activate_license( true, $bundle_license );
18461
18462 if ( is_object( $bundle_license ) ) {
18463 $this->maybe_activate_bundle_license( $bundle_license );
18464 }
18465 } else {
18466 if ( is_object( $bundle_license ) ) {
18467 $premium_license = $bundle_license;
18468 } else {
18469 $license_id = fs_request_get( 'license_id' );
18470
18471 if ( is_object( $this->_site ) &&
18472 FS_Plugin_License::is_valid_id( $license_id ) &&
18473 $license_id == $this->_site->license_id
18474 ) {
18475 // License is already activated.
18476 return;
18477 }
18478
18479 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
18480 $this->_get_license_by_id( $license_id ) :
18481 $this->_get_available_premium_license();
18482 }
18483
18484 if ( is_object( $premium_license ) ) {
18485 $this->maybe_network_activate_addon_license( $premium_license );
18486 }
18487 }
18488 }
18489
18490 /**
18491 * @author Leo Fajardo (@leorw)
18492 * @since 2.3.0
18493 *
18494 * @param FS_Site[] $installs
18495 * @param bool $is_site_level
18496 */
18497 private function handle_account_connection( $installs, $is_site_level ) {
18498 $first_install = $installs[0];
18499
18500 if ( $is_site_level ) {
18501 $this->_set_account( $this->_user, $first_install );
18502
18503 $this->do_action( 'after_account_connection', $this->_user, $first_install );
18504 } else {
18505 $this->_store_user();
18506
18507 // Map site addresses to their blog IDs.
18508 $address_to_blog_map = $this->get_address_to_blog_map();
18509
18510 $first_blog_id = null;
18511 $blog_2_install_map = array();
18512 foreach ( $installs as $install ) {
18513 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
18514 $blog_id = $address_to_blog_map[ $address ];
18515
18516 $this->_store_site( true, $blog_id, $install );
18517
18518 if ( is_null( $first_blog_id ) ) {
18519 $first_blog_id = $blog_id;
18520 }
18521
18522 $blog_2_install_map[ $blog_id ] = $install;
18523 }
18524
18525 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
18526 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
18527 ) {
18528 // Store network user.
18529 $this->_storage->network_user_id = $this->_user->id;
18530 }
18531
18532 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
18533 $this->_storage->network_install_blog_id = $first_blog_id;
18534 }
18535
18536 if ( count( $installs ) === count( $address_to_blog_map ) ) {
18537 // Super admin opted in for all sites in the network.
18538 $this->_storage->is_network_connected = true;
18539 }
18540
18541 $this->_store_licenses( false );
18542
18543 self::$_accounts->store();
18544
18545 // Don't sync the installs data on network upgrade
18546 if ( ! $this->network_upgrade_mode_completed() ) {
18547 $this->send_installs_update();
18548 }
18549
18550 $current_blog = get_current_blog_id();
18551
18552 foreach ( $blog_2_install_map as $blog_id => $install ) {
18553 $this->switch_to_blog( $blog_id );
18554
18555 $this->do_action( 'after_account_connection', $this->_user, $install );
18556 }
18557
18558 // Switch install context back to the first install.
18559 $this->switch_to_blog(
18560 $current_blog,
18561 $first_install,
18562 ( $this->_site->id != $first_install->id )
18563 );
18564
18565 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
18566 }
18567 }
18568
18569 /**
18570 * Tries to activate parent account based on add-on's info.
18571 *
18572 * @author Vova Feldman (@svovaf)
18573 * @since 1.2.2.7
18574 *
18575 * @param Freemius $parent_fs
18576 */
18577 private function activate_parent_account( Freemius $parent_fs ) {
18578 if ( ! $this->is_addon() ) {
18579 // This is not an add-on.
18580 return;
18581 }
18582
18583 if ( $parent_fs->is_registered() ) {
18584 // Already activated.
18585 return;
18586 }
18587
18588 // Activate parent with add-on's user credentials.
18589 $parent_install = $this->get_api_user_scope()->call(
18590 "/plugins/{$parent_fs->_plugin->id}/installs.json",
18591 'post',
18592 $parent_fs->get_install_data_for_api( array(
18593 'uid' => $parent_fs->get_anonymous_id(),
18594 ), false, false )
18595 );
18596
18597 if ( isset( $parent_install->error ) ) {
18598 $this->_admin_notices->add(
18599 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18600 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
18601 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18602 'error'
18603 );
18604
18605 return;
18606 }
18607
18608 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
18609
18610 if ( $parent_fs->is_pending_activation() ) {
18611 $parent_fs->clear_pending_activation_mode();
18612 }
18613
18614 // Get user information based on parent's plugin.
18615 $user = $this->get_user();
18616
18617 // First of all, set site info - otherwise we won't
18618 // be able to invoke API calls.
18619 $parent_fs->_site = new FS_Site( $parent_install );
18620 $parent_fs->_user = $user;
18621
18622 // Sync add-on plans.
18623 $parent_fs->_sync_plans();
18624
18625 $parent_fs->update_license_required_permissions_if_anonymous();
18626
18627 $parent_fs->_set_account( $user, $parent_fs->_site );
18628 }
18629
18630 #endregion
18631
18632 #----------------------------------------------------------------------------------
18633 #region Admin Menu Items
18634 #----------------------------------------------------------------------------------
18635
18636 private $_menu_items = array();
18637
18638 /**
18639 * @author Vova Feldman (@svovaf)
18640 * @since 1.2.1.8
18641 *
18642 * @return array
18643 */
18644 function get_menu_items() {
18645 return $this->_menu_items;
18646 }
18647
18648 /**
18649 * @author Vova Feldman (@svovaf)
18650 * @since 1.0.7
18651 *
18652 * @return string
18653 */
18654 function get_menu_slug() {
18655 return $this->_menu->get_slug();
18656 }
18657
18658 /**
18659 * @author Vova Feldman (@svovaf)
18660 * @since 1.0.9
18661 */
18662 function _prepare_admin_menu() {
18663 // if ( ! $this->is_on() ) {
18664 // return;
18665 // }
18666
18667 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
18668 return;
18669 }
18670
18671 /**
18672 * When running from a site admin with a network activated module and the connection
18673 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
18674 * site level settings.
18675 *
18676 * @author Vova Feldman (@svovaf)
18677 * @since 2.0.0
18678 */
18679 $should_hide_site_admin_settings = (
18680 $this->_is_network_active &&
18681 ! fs_is_network_admin() &&
18682 ! $this->is_delegated_connection() &&
18683 ! $this->is_anonymous() &&
18684 ! $this->is_registered()
18685 );
18686
18687 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
18688
18689 if ( ( false === $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
18690 $should_hide_site_admin_settings
18691 ) {
18692 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
18693 } else {
18694 $this->do_action( fs_is_network_admin() ?
18695 'before_network_admin_menu_init' :
18696 'before_admin_menu_init'
18697 );
18698
18699 $this->add_menu_action();
18700
18701 $this->add_network_menu_when_missing();
18702
18703 $this->add_submenu_items();
18704 }
18705 }
18706
18707 /**
18708 * Admin dashboard menu items modifications.
18709 *
18710 * NOTE: admin_menu action executed before admin_init.
18711 *
18712 * @author Vova Feldman (@svovaf)
18713 * @since 1.0.7
18714 *
18715 */
18716 private function add_menu_action() {
18717 if ( $this->is_activation_mode() ) {
18718 if ( $this->show_opt_in_on_setting_page() ) {
18719 $this->override_plugin_menu_with_activation();
18720 } else {
18721 /**
18722 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
18723 */
18724 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18725 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
18726 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
18727 fs_request_get_bool( 'pending_activation' )
18728 ) {
18729 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
18730 }
18731 }
18732 } else {
18733 if ( ! $this->is_registered() ) {
18734 // If not registered try to install user.
18735 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18736 $this->_install_with_new_user();
18737 }
18738 } else if (
18739 fs_request_is_action( 'sync_user' ) &&
18740 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
18741 ) {
18742 $this->_handle_account_user_sync();
18743 }
18744 }
18745 }
18746
18747 /**
18748 * @author Vova Feldman (@svovaf)
18749 * @since 1.0.1
18750 */
18751 function _redirect_on_clicked_menu_link() {
18752 $this->_logger->entrance();
18753
18754 $page = fs_request_get('page');
18755 $page = is_string($page) ? strtolower($page) : '';
18756
18757 $this->_logger->log( 'page = ' . $page );
18758
18759 foreach ( $this->_menu_items as $priority => $items ) {
18760 foreach ( $items as $item ) {
18761 if ( isset( $item['url'] ) ) {
18762 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
18763 $this->_logger->log( 'Redirecting to ' . $item['url'] );
18764
18765 fs_redirect( $item['url'] );
18766 }
18767 }
18768 }
18769 }
18770 }
18771
18772 /**
18773 * Remove plugin's all admin menu items & pages, and replace with activation page.
18774 *
18775 * @author Vova Feldman (@svovaf)
18776 * @since 1.0.1
18777 */
18778 private function override_plugin_menu_with_activation() {
18779 $this->_logger->entrance();
18780
18781 $hook = false;
18782
18783 if ( ! $this->has_settings_menu() ) {
18784 // Add the opt-in page without a menu item.
18785 $hook = FS_Admin_Menu_Manager::add_subpage(
18786 '',
18787 $this->get_plugin_name(),
18788 $this->get_plugin_name(),
18789 'manage_options',
18790 $this->_slug,
18791 array( &$this, '_connect_page_render' )
18792 );
18793 } else if ( $this->_menu->is_top_level() ) {
18794 if ( $this->_menu->is_override_exact() ) {
18795 // Make sure the current page is matching the activation page.
18796 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18797 return;
18798 }
18799 }
18800
18801 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18802
18803 if ( false === $hook ) {
18804 // Create new menu item just for the opt-in.
18805 $hook = FS_Admin_Menu_Manager::add_page(
18806 $this->get_plugin_name(),
18807 $this->get_plugin_name(),
18808 'manage_options',
18809 $this->_menu->get_slug(),
18810 array( &$this, '_connect_page_render' )
18811 );
18812 }
18813 } else {
18814 $menus = array( $this->_menu->get_parent_slug() );
18815
18816 if ( $this->_menu->is_override_exact() ) {
18817 // Make sure the current page is matching the activation page.
18818 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18819 return;
18820 }
18821 }
18822
18823 foreach ( $menus as $parent_slug ) {
18824 $hook = $this->_menu->override_submenu_action(
18825 $parent_slug,
18826 $this->_menu->get_raw_slug(),
18827 array( &$this, '_connect_page_render' )
18828 );
18829
18830 if ( false !== $hook ) {
18831 // Found plugin's submenu item.
18832 break;
18833 }
18834 }
18835 }
18836
18837 if ( $this->is_activation_page() ) {
18838 // Clean admin page from distracting content.
18839 self::_clean_admin_content_section();
18840 }
18841
18842 if ( false !== $hook ) {
18843 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18844 $this->_install_with_current_user();
18845 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18846 $this->_install_with_new_user();
18847 }
18848 }
18849 }
18850
18851 /**
18852 * If a plugin was network activated and connected but don't have a network
18853 * level settings, then add an artificial menu item for the Account and other
18854 * Freemius settings.
18855 *
18856 * @author Vova Feldman (@svovaf)
18857 * @since 2.0.0
18858 */
18859 private function add_network_menu_when_missing() {
18860 $this->_logger->entrance();
18861
18862 if ( ! $this->_is_network_active ) {
18863 // Plugin wasn't activated on the network level.
18864 return;
18865 }
18866
18867 if ( ! fs_is_network_admin() ) {
18868 // The context is not the network admin.
18869 return;
18870 }
18871
18872 if ( $this->_menu->has_network_menu() ) {
18873 // Plugin already has a network level menu.
18874 return;
18875 }
18876
18877 if ( $this->is_network_activation_mode() ) {
18878 /**
18879 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
18880 * screen is being shown.
18881 *
18882 * @author Leo Fajardo (@leorw)
18883 */
18884 return;
18885 }
18886
18887 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
18888 if ( $this->is_network_delegated_connection() ) {
18889 // Super-admin delegated the connection to the site admins.
18890 return;
18891 }
18892 }
18893
18894 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
18895
18896 if ( $this->_menu->has_menu() ||
18897 ! $this->is_addon() ||
18898 $this->is_activation_mode()
18899 ) {
18900 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
18901 $this->get_plugin_name(),
18902 $this->get_plugin_name(),
18903 'manage_options',
18904 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
18905 );
18906 }
18907 } else {
18908 $this->_menu->add_subpage_and_update(
18909 $this->_menu->get_parent_slug(),
18910 $this->get_plugin_name(),
18911 $this->get_plugin_name(),
18912 'manage_options',
18913 $this->_menu->get_slug()
18914 );
18915 }
18916 }
18917
18918 /**
18919 * @author Leo Fajardo (@leorw)
18920 * @since 1.2.1
18921 *
18922 * return string
18923 */
18924 function get_top_level_menu_capability() {
18925 global $menu;
18926
18927 $top_level_menu_slug = $this->get_top_level_menu_slug();
18928
18929 foreach ( $menu as $menu_info ) {
18930 /**
18931 * The second element in the menu info array is the capability/role that has access to the menu and the
18932 * third element is the menu slug.
18933 */
18934 if ( $menu_info[2] === $top_level_menu_slug ) {
18935 return $menu_info[1];
18936 }
18937 }
18938
18939 return 'read';
18940 }
18941
18942 /**
18943 * @author Vova Feldman (@svovaf)
18944 * @since 1.0.0
18945 *
18946 * @return string
18947 */
18948 private function get_top_level_menu_slug() {
18949 return ( $this->is_addon() ?
18950 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
18951 $this->_menu->get_top_level_menu_slug() );
18952 }
18953
18954 /**
18955 * @author Vova Feldman (@svovaf)
18956 * @since 1.2.2.7
18957 *
18958 * @return string
18959 */
18960 function get_pricing_cta_label() {
18961 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
18962
18963 if ( $this->is_in_trial_promotion() &&
18964 ! $this->is_paying_or_trial()
18965 ) {
18966 // If running a trial promotion, modify the pricing to load the trial.
18967 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
18968 } else if ( $this->is_paying() ) {
18969 $label = $this->get_text_inline( 'Pricing', 'pricing' );
18970 }
18971
18972 return $label;
18973 }
18974
18975 /**
18976 * @author Vova Feldman (@svovaf)
18977 * @since 1.2.2.7
18978 *
18979 * @return bool
18980 */
18981 function is_pricing_page_visible() {
18982 return (
18983 // Has at least one paid plan.
18984 $this->has_paid_plan() &&
18985 // Didn't ask to hide the pricing page.
18986 $this->is_page_visible( 'pricing' ) &&
18987 // Don't have a valid active license or has more than one plan.
18988 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
18989 );
18990 }
18991
18992 /**
18993 * @author Leo Fajardo (@leorw)
18994 * @since 2.3.0
18995 *
18996 * @param bool $is_activation_mode
18997 *
18998 * @return bool
18999 */
19000 private function should_add_submenu_or_action_links( $is_activation_mode ) {
19001 if ( $this->is_addon() ) {
19002 // No submenu items or action links for add-ons.
19003 return false;
19004 }
19005
19006 if ( $this->show_opt_in_on_themes_page() ) {
19007 if ( ! fs_is_network_admin() ) {
19008 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
19009 return true;
19010 }
19011 } else if ( $is_activation_mode ) {
19012 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
19013 return false;
19014 }
19015
19016 if ( fs_is_network_admin() ) {
19017 /**
19018 * Add submenu items or action links to network level when plugin was network activated and the super
19019 * admin did NOT delegate the connection of all sites to site admins.
19020 */
19021 return (
19022 $this->_is_network_active &&
19023 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
19024 ! $this->is_network_delegated_connection() )
19025 );
19026 }
19027
19028 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
19029 }
19030
19031 /**
19032 * Add default Freemius menu items.
19033 *
19034 * @author Vova Feldman (@svovaf)
19035 * @since 1.0.0
19036 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
19037 */
19038 private function add_submenu_items() {
19039 $this->_logger->entrance();
19040
19041 $is_activation_mode = $this->is_activation_mode();
19042
19043 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
19044
19045 if ( $add_submenu_items ) {
19046 if ( $this->has_affiliate_program() ) {
19047 // Add affiliation page.
19048 $this->add_submenu_item(
19049 $this->get_text_inline( 'Affiliation', 'affiliation' ),
19050 array( &$this, '_affiliation_page_render' ),
19051 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
19052 'manage_options',
19053 'affiliation',
19054 'Freemius::_clean_admin_content_section',
19055 WP_FS__DEFAULT_PRIORITY,
19056 $this->is_submenu_item_visible( 'affiliation' )
19057 );
19058 }
19059 }
19060
19061 if ( $add_submenu_items ||
19062 ( $is_activation_mode &&
19063 $this->is_only_premium() &&
19064 $this->is_admin_page( 'account' ) &&
19065 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
19066 )
19067 ) {
19068 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
19069 $show_account = (
19070 $this->is_submenu_item_visible( 'account' ) &&
19071 /**
19072 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
19073 */
19074 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
19075 );
19076
19077 // Add user account page.
19078 $this->add_submenu_item(
19079 $this->get_text_inline( 'Account', 'account' ),
19080 array( &$this, '_account_page_render' ),
19081 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
19082 'manage_options',
19083 'account',
19084 array( &$this, '_account_page_load' ),
19085 WP_FS__DEFAULT_PRIORITY,
19086 ( $add_submenu_items && $show_account )
19087 );
19088 }
19089 }
19090
19091 if ( $add_submenu_items ) {
19092 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
19093 // Add contact page.
19094 $this->add_submenu_item(
19095 $this->get_text_inline( 'Contact Us', 'contact-us' ),
19096 array( &$this, '_contact_page_render' ),
19097 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
19098 'manage_options',
19099 'contact',
19100 'Freemius::_clean_admin_content_section',
19101 WP_FS__DEFAULT_PRIORITY,
19102 $this->is_submenu_item_visible( 'contact' )
19103 );
19104 }
19105
19106 if ( $this->has_addons() ) {
19107 $this->add_submenu_item(
19108 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
19109 array( &$this, '_addons_page_render' ),
19110 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
19111 'manage_options',
19112 'addons',
19113 array( &$this, '_addons_page_load' ),
19114 WP_FS__LOWEST_PRIORITY - 1,
19115 $this->is_submenu_item_visible( 'addons' )
19116 );
19117 }
19118 }
19119
19120 if ( $add_submenu_items ||
19121 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
19122 ) {
19123 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
19124 $show_pricing = (
19125 $this->is_submenu_item_visible( 'pricing' ) &&
19126 $this->is_pricing_page_visible()
19127 );
19128
19129 $pricing_cta_text = $this->get_pricing_cta_label();
19130 $pricing_class = 'upgrade-mode';
19131 if ( $show_pricing ) {
19132 if ( $this->is_in_trial_promotion() &&
19133 ! $this->is_paying_or_trial()
19134 ) {
19135 // If running a trial promotion, modify the pricing to load the trial.
19136 $pricing_class = 'trial-mode';
19137 } else if ( $this->is_paying() ) {
19138 $pricing_class = '';
19139 }
19140 }
19141
19142 // Add upgrade/pricing page.
19143 $this->add_submenu_item(
19144 $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' ) ),
19145 array( &$this, '_pricing_page_render' ),
19146 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
19147 'manage_options',
19148 'pricing',
19149 'Freemius::_clean_admin_content_section',
19150 WP_FS__LOWEST_PRIORITY,
19151 ( $add_submenu_items && $show_pricing ),
19152 $pricing_class
19153 );
19154 }
19155 }
19156
19157 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
19158 /**
19159 * Add the other menu items if there are any when not in activation mode or license activation is not
19160 * required (license activation is required for registered or anonymous users after activating the
19161 * premium version when the site is not in trial mode or there's no active valid license).
19162 *
19163 * @author Leo Fajardo (@leorw)
19164 * @since 2.2.1
19165 */
19166 if ( 0 < count( $this->_menu_items ) ) {
19167 if ( ! $this->_menu->is_top_level() ) {
19168 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
19169
19170 // Append submenu items right after the plugin's submenu item.
19171 $this->order_sub_submenu_items();
19172 } else {
19173 // Append submenu items.
19174 $this->embed_submenu_items();
19175 }
19176 }
19177 }
19178 }
19179
19180 /**
19181 * Moved the actual submenu item additions to a separated function,
19182 * in order to support sub-submenu items when the plugin's settings
19183 * only have a submenu and not top-level menu item.
19184 *
19185 * @author Vova Feldman (@svovaf)
19186 * @since 1.1.4
19187 */
19188 private function embed_submenu_items() {
19189 $item_template = $this->_menu->is_top_level() ?
19190 '<span class="fs-submenu-item %s %s %s">%s</span>' :
19191 '<span class="fs-submenu-item fs-sub %s %s %s">%s</span>';
19192
19193 $top_level_menu_capability = $this->get_top_level_menu_capability();
19194
19195 ksort( $this->_menu_items );
19196
19197 $is_first_submenu_item = true;
19198
19199 foreach ( $this->_menu_items as $priority => $items ) {
19200 foreach ( $items as $item ) {
19201 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
19202
19203 $menu_item = sprintf(
19204 $item_template,
19205 $this->get_unique_affix(),
19206 $item['menu_slug'],
19207 ! empty( $item['class'] ) ? $item['class'] : '',
19208 $item['menu_title']
19209 );
19210
19211 $top_level_menu_slug = $this->get_top_level_menu_slug();
19212 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
19213
19214 if ( ! isset( $item['url'] ) ) {
19215 $hook = FS_Admin_Menu_Manager::add_subpage(
19216 $item['show_submenu'] ?
19217 $top_level_menu_slug :
19218 '',
19219 $item['page_title'],
19220 $menu_item,
19221 $capability,
19222 $menu_slug,
19223 $item['render_function']
19224 );
19225
19226 if ( false !== $item['before_render_function'] ) {
19227 add_action( "load-$hook", $item['before_render_function'] );
19228 }
19229 } else {
19230 FS_Admin_Menu_Manager::add_subpage(
19231 $item['show_submenu'] ?
19232 $top_level_menu_slug :
19233 '',
19234 $item['page_title'],
19235 $menu_item,
19236 $capability,
19237 $menu_slug,
19238 array( $this, '' )
19239 );
19240 }
19241
19242 if ( $item['show_submenu'] && $is_first_submenu_item ) {
19243 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
19244 /**
19245 * If the top-level menu has been dynamically created, remove the first submenu item that
19246 * WordPress automatically creates when there's no submenu item whose slug matches the
19247 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
19248 *
19249 * Awesome Plugin
19250 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
19251 *
19252 * @author Leo Fajardo (@leorw)
19253 */
19254 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
19255 }
19256
19257 $is_first_submenu_item = false;
19258 }
19259 }
19260 }
19261 }
19262
19263 /**
19264 * Re-order the submenu items so all Freemius added new submenu items
19265 * are added right after the plugin's settings submenu item.
19266 *
19267 * @author Vova Feldman (@svovaf)
19268 * @since 1.1.4
19269 */
19270 private function order_sub_submenu_items() {
19271 global $submenu;
19272
19273 $menu_slug = $this->_menu->get_top_level_menu_slug();
19274
19275 /**
19276 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
19277 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
19278 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
19279 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
19280 *
19281 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
19282 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
19283 * capability needed to access the parent menu as the capability for the submenus that we will add.
19284 */
19285 if ( empty( $submenu[ $menu_slug ] ) ) {
19286 return;
19287 }
19288
19289 $top_level_menu = &$submenu[ $menu_slug ];
19290
19291 $all_submenu_items_after = array();
19292
19293 $found_submenu_item = false;
19294
19295 foreach ( $top_level_menu as $submenu_id => $meta ) {
19296 if ( $found_submenu_item ) {
19297 // Remove all submenu items after the plugin's submenu item.
19298 $all_submenu_items_after[] = $meta;
19299 unset( $top_level_menu[ $submenu_id ] );
19300 }
19301
19302 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
19303 // Found the submenu item, put all below.
19304 $found_submenu_item = true;
19305 continue;
19306 }
19307 }
19308
19309 // Embed all plugin's new submenu items.
19310 $this->embed_submenu_items();
19311
19312 // Start with specially high number to make sure it's appended.
19313 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
19314 foreach ( $all_submenu_items_after as $meta ) {
19315 $top_level_menu[ $i ] = $meta;
19316 $i ++;
19317 }
19318
19319 // Sort submenu items.
19320 ksort( $top_level_menu );
19321 }
19322
19323 /**
19324 * Helper method to return the module's support forum URL.
19325 *
19326 * @author Vova Feldman (@svovaf)
19327 * @since 1.2.2.7
19328 *
19329 * @return string
19330 */
19331 function get_support_forum_url() {
19332 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
19333 }
19334
19335 /**
19336 * Displays the Support Forum link when enabled.
19337 *
19338 * Can be filtered like so:
19339 *
19340 * function _fs_show_support_menu( $is_visible, $menu_id ) {
19341 * if ( 'support' === $menu_id ) {
19342 * return _fs->is_registered();
19343 * }
19344 * return $is_visible;
19345 * }
19346 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
19347 *
19348 */
19349 function _add_default_submenu_items() {
19350 if ( ! $this->is_on() ) {
19351 return;
19352 }
19353
19354 if ( ! $this->is_activation_mode() &&
19355 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
19356 ( ! $this->_is_network_active && is_admin() ) )
19357 ) {
19358 $this->add_submenu_link_item(
19359 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
19360 $this->get_support_forum_url(),
19361 'wp-support-forum',
19362 null,
19363 50,
19364 $this->is_submenu_item_visible( 'support' )
19365 );
19366 }
19367 }
19368
19369 /**
19370 * @author Vova Feldman (@svovaf)
19371 * @since 1.0.1
19372 *
19373 * @param string $menu_title
19374 * @param callable $render_function
19375 * @param bool|string $page_title
19376 * @param string $capability
19377 * @param bool|string $menu_slug
19378 * @param bool|callable $before_render_function
19379 * @param int $priority
19380 * @param bool $show_submenu
19381 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
19382 */
19383 function add_submenu_item(
19384 $menu_title,
19385 $render_function,
19386 $page_title = false,
19387 $capability = 'manage_options',
19388 $menu_slug = false,
19389 $before_render_function = false,
19390 $priority = WP_FS__DEFAULT_PRIORITY,
19391 $show_submenu = true,
19392 $class = ''
19393 ) {
19394 $this->_logger->entrance( 'Title = ' . $menu_title );
19395
19396 if ( $this->is_addon() ) {
19397 $parent_fs = $this->get_parent_instance();
19398
19399 if ( is_object( $parent_fs ) ) {
19400 $parent_fs->add_submenu_item(
19401 $menu_title,
19402 $render_function,
19403 $page_title,
19404 $capability,
19405 $menu_slug,
19406 $before_render_function,
19407 $priority,
19408 $show_submenu,
19409 $class
19410 );
19411
19412 return;
19413 }
19414 }
19415
19416 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19417 $this->_menu_items[ $priority ] = array();
19418 }
19419
19420 $this->_menu_items[ $priority ][] = array(
19421 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
19422 'menu_title' => $menu_title,
19423 'capability' => $capability,
19424 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19425 'render_function' => $render_function,
19426 'before_render_function' => $before_render_function,
19427 'show_submenu' => $show_submenu,
19428 'class' => $class,
19429 );
19430 }
19431
19432 /**
19433 * @author Vova Feldman (@svovaf)
19434 * @since 1.0.1
19435 *
19436 * @param string $menu_title
19437 * @param string $url
19438 * @param bool $menu_slug
19439 * @param string $capability
19440 * @param int $priority
19441 * @param bool $show_submenu
19442 */
19443 function add_submenu_link_item(
19444 $menu_title,
19445 $url,
19446 $menu_slug = false,
19447 $capability = 'read',
19448 $priority = WP_FS__DEFAULT_PRIORITY,
19449 $show_submenu = true
19450 ) {
19451 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
19452
19453 if ( $this->is_addon() ) {
19454 $parent_fs = $this->get_parent_instance();
19455
19456 if ( is_object( $parent_fs ) ) {
19457 $parent_fs->add_submenu_link_item(
19458 $menu_title,
19459 $url,
19460 $menu_slug,
19461 $capability,
19462 $priority,
19463 $show_submenu
19464 );
19465
19466 return;
19467 }
19468 }
19469
19470 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19471 $this->_menu_items[ $priority ] = array();
19472 }
19473
19474 $this->_menu_items[ $priority ][] = array(
19475 'menu_title' => $menu_title,
19476 'capability' => $capability,
19477 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19478 'url' => $url,
19479 'page_title' => $menu_title,
19480 'render_function' => 'fs_dummy',
19481 'before_render_function' => '',
19482 'show_submenu' => $show_submenu,
19483 );
19484 }
19485
19486 #endregion ------------------------------------------------------------------
19487
19488 #--------------------------------------------------------------------------------
19489 #region Admin Notices
19490 #--------------------------------------------------------------------------------
19491
19492 /**
19493 * @author Vova Feldman (@svovaf)
19494 * @since 2.3.1
19495 *
19496 * @param string|string[] $ids
19497 * @param int|null $network_level_or_blog_id
19498 *
19499 * @uses FS_Admin_Notices::remove_sticky()
19500 */
19501 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
19502 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
19503 }
19504
19505 #endregion
19506
19507 #--------------------------------------------------------------------------------
19508 #region Actions / Hooks / Filters
19509 #--------------------------------------------------------------------------------
19510
19511 /**
19512 * @author Vova Feldman (@svovaf)
19513 * @since 1.1.7
19514 *
19515 * @param string $tag
19516 *
19517 * @return string
19518 */
19519 public function get_action_tag( $tag ) {
19520 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
19521 }
19522
19523 /**
19524 * @author Vova Feldman (@svovaf)
19525 * @since 1.2.1.6
19526 *
19527 * @param string $tag
19528 * @param string $slug
19529 * @param bool $is_plugin
19530 *
19531 * @return string
19532 */
19533 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
19534 $action = "fs_{$tag}";
19535
19536 if ( ! empty( $slug ) ) {
19537 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
19538 }
19539
19540 return $action;
19541 }
19542
19543 /**
19544 * Returns a string that can be used to generate a unique action name,
19545 * option name, HTML element ID, or HTML element class.
19546 *
19547 * @author Leo Fajardo (@leorw)
19548 * @since 1.2.2
19549 *
19550 * @return string
19551 */
19552 public function get_unique_affix() {
19553 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
19554 }
19555
19556 /**
19557 * Returns a string that can be used to generate a unique action name,
19558 * option name, HTML element ID, or HTML element class.
19559 *
19560 * @author Vova Feldman (@svovaf)
19561 * @since 1.2.2.5
19562 *
19563 * @param string $slug
19564 * @param bool $is_plugin
19565 *
19566 * @return string
19567 */
19568 static function get_module_unique_affix( $slug, $is_plugin = true ) {
19569 $affix = $slug;
19570
19571 if ( ! $is_plugin ) {
19572 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
19573 }
19574
19575 return $affix;
19576 }
19577
19578 /**
19579 * @author Vova Feldman (@svovaf)
19580 * @since 1.2.1
19581 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19582 * based on the slug for backward compatibility.
19583 *
19584 * @param string $tag
19585 *
19586 * @return string
19587 */
19588 function get_ajax_action( $tag ) {
19589 return self::get_ajax_action_static( $tag, $this->_module_id );
19590 }
19591
19592 /**
19593 * @author Vova Feldman (@svovaf)
19594 * @since 1.2.1.7
19595 *
19596 * @param string $tag
19597 *
19598 * @return string
19599 */
19600 function get_ajax_security( $tag ) {
19601 return wp_create_nonce( $this->get_ajax_action( $tag ) );
19602 }
19603
19604 /**
19605 * @author Vova Feldman (@svovaf)
19606 * @since 1.2.1.7
19607 *
19608 * @param string $tag
19609 */
19610 function check_ajax_referer( $tag ) {
19611 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
19612 }
19613
19614 /**
19615 * @author Vova Feldman (@svovaf)
19616 * @since 1.2.1.6
19617 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19618 * based on the slug for backward compatibility.
19619 *
19620 * @param string $tag
19621 * @param number|null $module_id
19622 *
19623 * @return string
19624 */
19625 static function get_ajax_action_static( $tag, $module_id = null ) {
19626 $action = "fs_{$tag}";
19627
19628 if ( ! empty( $module_id ) ) {
19629 $action .= "_{$module_id}";
19630 }
19631
19632 return $action;
19633 }
19634
19635 /**
19636 * Do action, specific for the current context plugin.
19637 *
19638 * @author Vova Feldman (@svovaf)
19639 * @since 1.0.1
19640 *
19641 * @param string $tag The name of the action to be executed.
19642 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
19643 * functions hooked to the action. Default empty.
19644 *
19645 * @uses do_action()
19646 */
19647 function do_action( $tag, $arg = '' ) {
19648 $args = func_get_args();
19649
19650 $this->_logger->entrance( $tag );
19651
19652 call_user_func_array( 'do_action', array_merge(
19653 array( $this->get_action_tag( $tag ) ),
19654 array_slice( $args, 1 ) )
19655 );
19656 }
19657
19658 /**
19659 * Add action, specific for the current context plugin.
19660 *
19661 * @author Vova Feldman (@svovaf)
19662 * @since 1.0.1
19663 *
19664 * @param string $tag
19665 * @param callable $function_to_add
19666 * @param int $priority
19667 * @param int $accepted_args
19668 *
19669 * @uses add_action()
19670 */
19671 function add_action(
19672 $tag,
19673 $function_to_add,
19674 $priority = WP_FS__DEFAULT_PRIORITY,
19675 $accepted_args = 1
19676 ) {
19677 $this->_logger->entrance( $tag );
19678
19679 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19680 }
19681
19682 /**
19683 * Add AJAX action, specific for the current context plugin.
19684 *
19685 * @author Vova Feldman (@svovaf)
19686 * @since 1.2.1
19687 *
19688 * @param string $tag
19689 * @param callable $function_to_add
19690 * @param int $priority
19691 *
19692 * @uses add_action()
19693 *
19694 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19695 */
19696 function add_ajax_action(
19697 $tag,
19698 $function_to_add,
19699 $priority = WP_FS__DEFAULT_PRIORITY
19700 ) {
19701 $this->_logger->entrance( $tag );
19702
19703 return self::add_ajax_action_static(
19704 $tag,
19705 $function_to_add,
19706 $priority,
19707 $this->_module_id
19708 );
19709 }
19710
19711 /**
19712 * Add AJAX action.
19713 *
19714 * @author Vova Feldman (@svovaf)
19715 * @since 1.2.1.6
19716 *
19717 * @param string $tag
19718 * @param callable $function_to_add
19719 * @param int $priority
19720 * @param number|null $module_id
19721 *
19722 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19723 * @uses add_action()
19724 *
19725 */
19726 static function add_ajax_action_static(
19727 $tag,
19728 $function_to_add,
19729 $priority = WP_FS__DEFAULT_PRIORITY,
19730 $module_id = null
19731 ) {
19732 self::$_static_logger->entrance( $tag );
19733
19734 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
19735 return false;
19736 }
19737
19738 add_action(
19739 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
19740 $function_to_add,
19741 $priority,
19742 0
19743 );
19744
19745 self::$_static_logger->info( "$tag AJAX callback action added." );
19746
19747 return true;
19748 }
19749
19750 /**
19751 * Send a JSON response back to an Ajax request.
19752 *
19753 * @author Vova Feldman (@svovaf)
19754 * @since 1.2.1.5
19755 *
19756 * @param mixed $response
19757 */
19758 static function shoot_ajax_response( $response ) {
19759 wp_send_json( $response );
19760 }
19761
19762 /**
19763 * Send a JSON response back to an Ajax request, indicating success.
19764 *
19765 * @author Vova Feldman (@svovaf)
19766 * @since 1.2.1.5
19767 *
19768 * @param mixed $data Data to encode as JSON, then print and exit.
19769 */
19770 static function shoot_ajax_success( $data = null ) {
19771 wp_send_json_success( $data );
19772 }
19773
19774 /**
19775 * Send a JSON response back to an Ajax request, indicating failure.
19776 *
19777 * @author Vova Feldman (@svovaf)
19778 * @since 1.2.1.5
19779 *
19780 * @param mixed $error Optional error message.
19781 */
19782 static function shoot_ajax_failure( $error = '' ) {
19783 $result = array( 'success' => false );
19784 if ( ! empty( $error ) ) {
19785 $result['error'] = $error;
19786 }
19787
19788 wp_send_json( $result );
19789 }
19790
19791 /**
19792 * Returns an AJAX URL with a special extra param to indicate whether the request was triggered from the network admin or blog admin.
19793 *
19794 * @author Vova Feldman (@svovaf)
19795 * @since 2.5.1
19796 *
19797 * @param string $wrap_with By default, returns the AJAX URL wrapped with single quotes.
19798 *
19799 * @return string
19800 */
19801 static function ajax_url( $wrap_with = "'") {
19802 if ( fs_is_network_admin() ) {
19803 $param_name = '_fs_network_admin';
19804 } else {
19805 $param_name = '_fs_blog_admin';
19806 }
19807
19808 $url = admin_url( 'admin-ajax.php', 'relative' );
19809 $url .= ( false === strpos( $url, '?' ) ) ? '?' : '&';
19810 $url .= "{$param_name}=true";
19811
19812 return "{$wrap_with}{$url}{$wrap_with}";
19813 }
19814
19815 /**
19816 * Apply filter, specific for the current context plugin.
19817 *
19818 * @author Vova Feldman (@svovaf)
19819 * @since 1.0.9
19820 *
19821 * @param string $tag The name of the filter hook.
19822 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
19823 *
19824 * @return mixed The filtered value after all hooked functions are applied to it.
19825 *
19826 * @uses apply_filters()
19827 */
19828 function apply_filters( $tag, $value ) {
19829 $args = func_get_args();
19830
19831 $this->_logger->entrance( $tag );
19832
19833 array_unshift( $args, $this->get_unique_affix() );
19834
19835 return call_user_func_array( 'fs_apply_filter', $args );
19836 }
19837
19838 /**
19839 * Add filter, specific for the current context plugin.
19840 *
19841 * @author Vova Feldman (@svovaf)
19842 * @since 1.0.9
19843 *
19844 * @param string $tag
19845 * @param callable $function_to_add
19846 * @param int $priority
19847 * @param int $accepted_args
19848 *
19849 * @uses add_filter()
19850 */
19851 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
19852 $this->_logger->entrance( $tag );
19853
19854 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19855 }
19856
19857 /**
19858 * Check if has filter.
19859 *
19860 * @author Vova Feldman (@svovaf)
19861 * @since 1.1.4
19862 *
19863 * @param string $tag
19864 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
19865 *
19866 * @return false|int
19867 *
19868 * @uses has_filter()
19869 */
19870 function has_filter( $tag, $function_to_check = false ) {
19871 $this->_logger->entrance( $tag );
19872
19873 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
19874 }
19875
19876 #endregion
19877
19878 /**
19879 * Override default i18n text phrases.
19880 *
19881 * @author Vova Feldman (@svovaf)
19882 * @since 1.1.6
19883 *
19884 * @param string[] string $key_value
19885 *
19886 * @uses fs_override_i18n()
19887 */
19888 function override_i18n( $key_value ) {
19889 fs_override_i18n( $key_value, $this->_slug );
19890 }
19891
19892 /* Account Page
19893 ------------------------------------------------------------------------------------------------------------------*/
19894 /**
19895 * Update site information.
19896 *
19897 * @author Vova Feldman (@svovaf)
19898 * @since 1.0.1
19899 *
19900 * @param bool $store Flush to Database if true.
19901 * @param null|int $network_level_or_blog_id Since 2.0.0
19902 * @param \FS_Site $site Since 2.0.0
19903 */
19904 private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null, $is_backup = false ) {
19905 $this->_logger->entrance();
19906
19907 if ( is_null( $site ) ) {
19908 $site = $this->_site;
19909 }
19910
19911 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
19912 $this->_logger->error( "Empty install ID, can't store site." );
19913
19914 return;
19915 }
19916
19917 $site_clone = clone $site;
19918
19919 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id, $is_backup );
19920
19921 if (
19922 ! $is_backup &&
19923 is_object( $this->_user ) && $this->_user->id != $site->user_id
19924 ) {
19925 $this->sync_user_by_current_install( $site->user_id );
19926
19927 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
19928
19929 if ( empty( $prev_stored_user_id ) &&
19930 is_object($this->_user) && $this->_user->id != $site->user_id
19931 ) {
19932 /**
19933 * Store the current user ID as the previous user ID so that the previous user can be used
19934 * as the install's owner while the new owner's details are not yet available.
19935 *
19936 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
19937 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
19938 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
19939 * into `replica`.
19940 *
19941 * @author Leo Fajardo (@leorw)
19942 */
19943 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
19944 }
19945 }
19946
19947 $sites[ $this->_slug ] = $site_clone;
19948
19949 $this->set_account_option(
19950 ( $is_backup ? 'prev_' : '' ) . 'sites',
19951 $sites,
19952 $store,
19953 $network_level_or_blog_id
19954 );
19955 }
19956
19957 /**
19958 * 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).
19959 *
19960 * @author Leo Fajardo (@leorw)
19961 * @since 2.5.0
19962 */
19963 private function back_up_site() {
19964 $this->_logger->entrance();
19965
19966 $site_clone = clone $this->_site;
19967
19968 $this->_store_site( true, null, $site_clone, true );
19969 }
19970
19971 /**
19972 * Update plugin's plans information.
19973 *
19974 * @author Vova Feldman (@svovaf)
19975 * @since 1.0.2
19976 *
19977 * @param bool $store Flush to Database if true.
19978 */
19979 private function _store_plans( $store = true ) {
19980 $this->_logger->entrance();
19981
19982 $plans = self::get_all_plans( $this->_module_type );
19983
19984 // Copy plans.
19985 $encrypted_plans = array();
19986 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
19987 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
19988 }
19989
19990 $plans[ $this->_slug ] = $encrypted_plans;
19991
19992 $this->set_account_option( 'plans', $plans, $store );
19993 }
19994
19995 /**
19996 * Update user's plugin licenses.
19997 *
19998 * @author Vova Feldman (@svovaf)
19999 * @since 1.0.5
20000 *
20001 * @param bool $store
20002 * @param number|bool $module_id
20003 * @param FS_Plugin_License[] $licenses
20004 */
20005 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
20006 $this->_logger->entrance();
20007
20008 $all_licenses = self::get_all_licenses();
20009
20010 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
20011 $module_id = $this->_module_id;
20012
20013 $user_licenses = is_array( $this->_licenses ) ?
20014 $this->_licenses :
20015 array();
20016
20017 if ( empty( $user_licenses ) ) {
20018 // If the context user doesn't have any license, don't update the licenses collection.
20019 return;
20020 }
20021
20022 $new_user_licenses_map = array();
20023 foreach ( $user_licenses as $user_license ) {
20024 $new_user_licenses_map[ $user_license->id ] = $user_license;
20025 }
20026
20027 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
20028
20029 // Update user licenses.
20030 $licenses_to_update_count = count( $new_user_licenses_map );
20031 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
20032 if ( 0 === $licenses_to_update_count ) {
20033 break;
20034 }
20035
20036 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
20037 // Update license.
20038 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
20039 unset( $new_user_licenses_map[ $license->id ] );
20040
20041 $licenses_to_update_count --;
20042 }
20043 }
20044
20045 if ( ! empty( $new_user_licenses_map ) ) {
20046 // Add new licenses.
20047 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
20048 }
20049
20050 $licenses = $all_licenses[ $module_id ];
20051 }
20052
20053 if ( ! isset( $all_licenses[ $module_id ] ) ) {
20054 $all_licenses[ $module_id ] = array();
20055 }
20056
20057 $all_licenses[ $module_id ] = $licenses;
20058
20059 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
20060 }
20061
20062 /**
20063 * Update user information.
20064 *
20065 * @author Vova Feldman (@svovaf)
20066 * @since 1.0.1
20067 *
20068 * @param bool $store Flush to Database if true.
20069 */
20070 private function _store_user( $store = true ) {
20071 $this->_logger->entrance();
20072
20073 if ( empty( $this->_user->id ) ) {
20074 $this->_logger->error( "Empty user ID, can't store user." );
20075
20076 return;
20077 }
20078
20079 $users = self::get_all_users();
20080 $users[ $this->_user->id ] = $this->_user;
20081 self::$_accounts->set_option( 'users', $users, $store );
20082 }
20083
20084 /**
20085 * Update new updates information.
20086 *
20087 * @author Vova Feldman (@svovaf)
20088 * @since 1.0.4
20089 *
20090 * @param FS_Plugin_Tag|null $update
20091 * @param bool $store Flush to Database if true.
20092 * @param bool|number $plugin_id
20093 */
20094 private function _store_update( $update, $store = true, $plugin_id = false ) {
20095 $this->_logger->entrance();
20096
20097 if ( $update instanceof FS_Plugin_Tag ) {
20098 $update->updated = time();
20099 }
20100
20101 if ( ! is_numeric( $plugin_id ) ) {
20102 $plugin_id = $this->_plugin->id;
20103 }
20104
20105 $updates = self::get_all_updates();
20106 $updates[ $plugin_id ] = $update;
20107 self::$_accounts->set_option( 'updates', $updates, $store );
20108 }
20109
20110 /**
20111 * Update new updates information.
20112 *
20113 * @author Vova Feldman (@svovaf)
20114 * @since 1.0.6
20115 *
20116 * @param FS_Plugin[] $plugin_addons
20117 * @param bool $store Flush to Database if true.
20118 */
20119 private function _store_addons( $plugin_addons, $store = true ) {
20120 $this->_logger->entrance();
20121
20122 $addons = self::get_all_addons();
20123 $addons[ $this->_plugin->id ] = $plugin_addons;
20124 self::$_accounts->set_option( 'addons', $addons, $store );
20125 }
20126
20127 /**
20128 * Delete plugin's associated add-ons.
20129 *
20130 * @author Vova Feldman (@svovaf)
20131 * @since 1.0.8
20132 *
20133 * @param bool $store
20134 *
20135 * @return bool
20136 */
20137 private function _delete_account_addons( $store = true ) {
20138 $all_addons = self::get_all_account_addons();
20139
20140 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
20141 return false;
20142 }
20143
20144 unset( $all_addons[ $this->_plugin->id ] );
20145
20146 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20147
20148 return true;
20149 }
20150
20151 /**
20152 * Update account add-ons list.
20153 *
20154 * @author Vova Feldman (@svovaf)
20155 * @since 1.0.6
20156 *
20157 * @param FS_Plugin[] $addons
20158 * @param bool $store Flush to Database if true.
20159 */
20160 private function _store_account_addons( $addons, $store = true ) {
20161 $this->_logger->entrance();
20162
20163 $all_addons = self::get_all_account_addons();
20164 $all_addons[ $this->_plugin->id ] = $addons;
20165 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20166 }
20167
20168 /**
20169 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
20170 * the valid user licenses will be fetched again and the account add-ons may be updated.
20171 *
20172 * @author Leo Fajardo (@leorw)
20173 * @since 2.2.4
20174 */
20175 private function purge_valid_user_licenses_cache() {
20176 if ( ! $this->is_registered() ) {
20177 return;
20178 }
20179
20180 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
20181 }
20182
20183 /**
20184 * @author Leo Fajardo (@leorw)
20185 * @since 2.3.0
20186 *
20187 * @param array $all_licenses
20188 * @param number|null $site_license_id
20189 * @param bool $include_parent_licenses
20190 *
20191 * @return array
20192 */
20193 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
20194 $foreign_licenses = array(
20195 'ids' => array(),
20196 'license_keys' => array()
20197 );
20198
20199 $parent_license_ids_map = array();
20200
20201 foreach ( $all_licenses as $license ) {
20202 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
20203 continue;
20204 }
20205
20206 $foreign_licenses['ids'][] = $license->id;
20207 $foreign_licenses['license_keys'][] = $license->secret_key;
20208
20209 if (
20210 $include_parent_licenses &&
20211 is_object( $this->_license ) &&
20212 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
20213 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
20214 ) {
20215 /**
20216 * Include the parent license's info only if it has not been included before since child licenses
20217 * can have the same parent license.
20218 */
20219 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
20220 $foreign_licenses['license_keys'][] = $license->secret_key;
20221
20222 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
20223 }
20224 }
20225
20226 if ( empty( $foreign_licenses['ids'] ) ) {
20227 $foreign_licenses = array();
20228 }
20229
20230 return $foreign_licenses;
20231 }
20232
20233 /**
20234 * @author Leo Fajardo (@leorw)
20235 * @since 2.3.0
20236 *
20237 * @return string
20238 */
20239 private function get_valid_user_licenses_endpoint() {
20240 $user_licenses_endpoint = '/licenses.json?type=active' .
20241 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
20242
20243 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
20244
20245 if ( ! empty ( $foreign_licenses ) ) {
20246 $foreign_licenses = array(
20247 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20248 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20249 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20250 );
20251
20252 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20253 }
20254
20255 return $user_licenses_endpoint;
20256 }
20257
20258 /**
20259 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
20260 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
20261 * method filters out non–add-on product IDs and stores the add-on IDs.
20262 *
20263 * @author Leo Fajardo (@leorw)
20264 * @since 2.2.4
20265 *
20266 * @return stdClass[] array
20267 */
20268 private function fetch_valid_user_licenses() {
20269 $this->_logger->entrance();
20270
20271 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
20272
20273 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
20274 ! is_array( $result->licenses )
20275 ) {
20276 return array();
20277 }
20278
20279 return $result->licenses;
20280 }
20281
20282 /**
20283 * @author Leo Fajardo (@leorw)
20284 * @since 2.2.4
20285 *
20286 * @return number[] Account add-on IDs.
20287 */
20288 function get_updated_account_addons() {
20289 $addons = $this->get_addons();
20290 if ( empty( $addons ) ) {
20291 return array();
20292 }
20293
20294 $account_addons = $this->get_account_addons();
20295 if ( ! is_array( $account_addons ) ) {
20296 $account_addons = array();
20297 }
20298
20299 $user_licenses = $this->is_registered() ?
20300 $this->fetch_valid_user_licenses() :
20301 array();
20302
20303 if ( empty( $user_licenses ) ) {
20304 return $account_addons;
20305 }
20306
20307 $addon_ids = array();
20308 foreach ( $addons as $addon ) {
20309 $addon_ids[] = $addon->id;
20310 }
20311
20312 $license_product_ids = array();
20313
20314 foreach ( $user_licenses as $license ) {
20315 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
20316 $license_product_ids = array_merge( $license_product_ids, $license->products );
20317 } else {
20318 $license_product_ids[] = $license->plugin_id;
20319 }
20320 }
20321
20322 // Filter out non–add-on IDs.
20323 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
20324 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
20325 $this->_store_account_addons( array_unique( $new_account_addons ) );
20326 }
20327
20328 return $new_account_addons;
20329 }
20330
20331 /**
20332 * Store account params in the Database.
20333 *
20334 * @author Vova Feldman (@svovaf)
20335 * @since 1.0.1
20336 *
20337 * @param null|int $blog_id Since 2.0.0
20338 */
20339 private function _store_account( $blog_id = null ) {
20340 $this->_logger->entrance();
20341
20342 $this->_store_site( false, $blog_id );
20343 $this->_store_user( false );
20344 $this->_store_plans( false );
20345 $this->_store_licenses( false );
20346
20347 self::$_accounts->store( $blog_id );
20348 }
20349
20350 /**
20351 * Sync user's information.
20352 *
20353 * @author Vova Feldman (@svovaf)
20354 * @since 1.0.3
20355 * @uses FS_Api
20356 */
20357 private function _handle_account_user_sync() {
20358 $this->_logger->entrance();
20359
20360 $api = $this->get_api_user_scope();
20361
20362 // Get user's information.
20363 $user = $api->get( '/', true );
20364
20365 if ( isset( $user->id ) ) {
20366 $this->_user->first = $user->first;
20367 $this->_user->last = $user->last;
20368 $this->_user->email = $user->email;
20369
20370 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
20371
20372 if ( $user->is_verified &&
20373 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
20374 ) {
20375 $this->_user->is_verified = true;
20376
20377 $this->do_action( 'account_email_verified', $user->email );
20378
20379 $this->_admin_notices->add(
20380 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
20381 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
20382 'success',
20383 // Make admin sticky if account menu item is invisible,
20384 // since the page will be auto redirected to the plugin's
20385 // main settings page, and the non-sticky message
20386 // will disappear.
20387 ! $is_menu_item_account_visible,
20388 'email_verified'
20389 );
20390 }
20391
20392 // Flush user details to DB.
20393 $this->_store_user();
20394
20395 $this->do_action( 'after_account_user_sync', $user );
20396
20397 /**
20398 * If account menu item is hidden, redirect to plugin's main settings page.
20399 *
20400 * @author Vova Feldman (@svovaf)
20401 * @since 1.1.6
20402 *
20403 * @link https://github.com/Freemius/wordpress-sdk/issues/6
20404 */
20405 if ( ! $is_menu_item_account_visible ) {
20406 fs_redirect( $this->_get_admin_page_url() );
20407 }
20408 }
20409 }
20410
20411 /**
20412 * @author Vova Feldman (@svovaf)
20413 * @since 1.0.9
20414 * @uses FS_Api
20415 *
20416 * @param number|bool $license_id
20417 *
20418 * @return FS_Subscription|object|bool
20419 */
20420 private function _fetch_site_license_subscription( $license_id = false ) {
20421 $this->_logger->entrance();
20422 $api = $this->get_api_site_scope();
20423
20424 if ( ! is_numeric( $license_id ) ) {
20425 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
20426 $this->_license->parent_license_id :
20427 $this->_license->id;
20428 }
20429
20430 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
20431
20432 return ! isset( $result->error ) ?
20433 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
20434 new FS_Subscription( $result->subscriptions[0] ) :
20435 false
20436 ) :
20437 $result;
20438 }
20439
20440 /**
20441 * @author Vova Feldman (@svovaf)
20442 * @since 1.0.4
20443 * @uses FS_Api
20444 *
20445 * @param number|bool $plan_id
20446 *
20447 * @return FS_Plugin_Plan|object
20448 */
20449 private function _fetch_site_plan( $plan_id = false ) {
20450 $this->_logger->entrance();
20451 $api = $this->get_api_site_scope();
20452
20453 if ( ! is_numeric( $plan_id ) ) {
20454 $plan_id = $this->_site->plan_id;
20455 }
20456
20457 $plan = $api->get( "/plans/{$plan_id}.json", true );
20458
20459 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
20460 }
20461
20462 /**
20463 * @author Vova Feldman (@svovaf)
20464 * @since 1.0.5
20465 * @uses FS_Api
20466 *
20467 * @return FS_Plugin_Plan[]|object
20468 */
20469 private function _fetch_plugin_plans() {
20470 $this->_logger->entrance();
20471 $api = $this->get_current_or_network_user_api_scope();
20472
20473 /**
20474 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
20475 */
20476 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
20477
20478 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
20479 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
20480 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
20481 }
20482
20483 $result = $result->plans;
20484 }
20485
20486 return $result;
20487 }
20488
20489 /**
20490 * @author Vova Feldman (@svovaf)
20491 * @since 2.0.0
20492 *
20493 * @param number $plan_id
20494 *
20495 * @return \FS_Plugin_Plan|object
20496 */
20497 private function fetch_plan_by_id( $plan_id ) {
20498 $this->_logger->entrance();
20499 $api = $this->get_current_or_network_user_api_scope();
20500
20501 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
20502
20503 return $this->is_api_result_entity( $result ) ?
20504 new FS_Plugin_Plan( $result ) :
20505 $result;
20506 }
20507
20508 /**
20509 * @author Vova Feldman (@svovaf)
20510 * @since 1.0.5
20511 * @uses FS_Api
20512 *
20513 * @param number|bool $plugin_id
20514 * @param number|bool $site_license_id
20515 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
20516 * @param number|null $blog_id
20517 *
20518 * @return FS_Plugin_License[]|object
20519 */
20520 private function _fetch_licenses(
20521 $plugin_id = false,
20522 $site_license_id = false,
20523 $foreign_licenses = array(),
20524 $blog_id = null
20525 ) {
20526 $this->_logger->entrance();
20527
20528 $api = $this->get_api_user_scope();
20529
20530 if ( ! is_numeric( $plugin_id ) ) {
20531 $plugin_id = $this->_plugin->id;
20532 }
20533
20534 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
20535 if ( ! empty ( $foreign_licenses ) ) {
20536 $foreign_licenses = array(
20537 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20538 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20539 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20540 );
20541
20542 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20543 }
20544
20545 $result = $api->get( $user_licenses_endpoint, true );
20546
20547 $is_site_license_synced = false;
20548
20549 $api_errors = array();
20550
20551 if ( $this->is_api_result_object( $result, 'licenses' ) &&
20552 is_array( $result->licenses )
20553 ) {
20554 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
20555 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
20556
20557 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
20558 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
20559 }
20560 }
20561
20562 $result = $result->licenses;
20563 } else {
20564 $api_errors[] = $result;
20565 $result = array();
20566 }
20567
20568 if ( ! $is_site_license_synced ) {
20569 if ( ! is_null( $blog_id ) ) {
20570 /**
20571 * If blog ID is not null, the request is for syncing of the license of a single site via the
20572 * network-level "Account" page.
20573 *
20574 * @author Leo Fajardo (@leorw)
20575 */
20576 $this->switch_to_blog( $blog_id );
20577 }
20578
20579 $api = $this->get_api_site_scope();
20580
20581 if ( is_numeric( $site_license_id ) ) {
20582 // Try to retrieve a foreign license that is linked to the install.
20583 $api_result = $api->call( '/licenses.json?is_enriched=true' );
20584
20585 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
20586 is_array( $api_result->licenses )
20587 ) {
20588 $licenses = $api_result->licenses;
20589
20590 if ( ! empty( $licenses ) ) {
20591 $result[] = new FS_Plugin_License( $licenses[0] );
20592 }
20593 } else {
20594 $api_errors[] = $api_result;
20595 }
20596 } else if (
20597 is_object( $this->_license ) &&
20598 /**
20599 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
20600 * the FS instance that does the syncing is the parent FS instance.
20601 *
20602 * @author Leo Fajardo (@leorw)
20603 * @since 2.3.0
20604 */
20605 $this->_license->plugin_id == $plugin_id
20606 ) {
20607 $is_license_in_result = false;
20608 if ( ! empty( $result ) ) {
20609 foreach ( $result as $license ) {
20610 if ( $license->id == $this->_license->id ) {
20611 $is_license_in_result = true;
20612 break;
20613 }
20614 }
20615 }
20616
20617 if ( ! $is_license_in_result ) {
20618 // Fetch foreign license by ID and license key.
20619 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
20620 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
20621
20622 if ( $this->is_api_result_entity( $license ) ) {
20623 $result[] = new FS_Plugin_License( $license );
20624 } else {
20625 $api_errors[] = $license;
20626 }
20627 }
20628 }
20629
20630 if ( ! is_null( $blog_id ) ) {
20631 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20632 }
20633 }
20634
20635 if ( is_array( $result ) && 0 < count( $result ) ) {
20636 // If found at least one license, return license collection even if there are errors.
20637 return $result;
20638 }
20639
20640 if ( ! empty( $api_errors ) ) {
20641 // If found any errors and no licenses, return first error.
20642 return $api_errors[0];
20643 }
20644
20645 // Fallback to empty licenses list.
20646 return $result;
20647 }
20648
20649 /**
20650 * @author Vova Feldman (@svovaf)
20651 * @since 2.0.0
20652 *
20653 * @param number $license_id
20654 * @param string $license_key
20655 *
20656 * @return \FS_Plugin_License|object
20657 */
20658 private function fetch_license_by_key( $license_id, $license_key ) {
20659 $this->_logger->entrance();
20660
20661 $api = $this->get_current_or_network_user_api_scope();
20662
20663 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
20664
20665 return $this->is_api_result_entity( $result ) ?
20666 new FS_Plugin_License( $result ) :
20667 $result;
20668 }
20669
20670 /**
20671 * @author Vova Feldman (@svovaf)
20672 * @since 1.2.0
20673 * @uses FS_Api
20674 *
20675 * @param number|bool $plugin_id
20676 * @param bool $flush
20677 *
20678 * @return FS_Payment[]|object
20679 */
20680 function _fetch_payments( $plugin_id = false, $flush = false ) {
20681 $this->_logger->entrance();
20682
20683 $api = $this->get_api_user_scope();
20684
20685 if ( ! is_numeric( $plugin_id ) ) {
20686 $plugin_id = $this->_plugin->id;
20687 }
20688
20689 $include_bundles = (
20690 is_object( $this->_plugin ) &&
20691 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
20692 );
20693
20694 $result = $api->get(
20695 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
20696 $flush
20697 );
20698
20699 if ( ! isset( $result->error ) ) {
20700 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
20701 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
20702 }
20703 $result = $result->payments;
20704 }
20705
20706 return $result;
20707 }
20708
20709 /**
20710 * @author Vova Feldman (@svovaf)
20711 * @since 1.2.1.5
20712 * @uses FS_Api
20713 *
20714 * @param bool $flush
20715 *
20716 * @return \FS_Billing|mixed
20717 */
20718 function _fetch_billing( $flush = false ) {
20719 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
20720
20721 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
20722
20723 if ( $this->is_api_result_entity( $billing ) ) {
20724 $billing = new FS_Billing( $billing );
20725 }
20726
20727 return $billing;
20728 }
20729
20730 /**
20731 * @author Vova Feldman (@svovaf)
20732 * @since 1.0.5
20733 *
20734 * @param FS_Plugin_License[] $licenses
20735 * @param number $module_id
20736 */
20737 private function _update_licenses( $licenses, $module_id ) {
20738 $this->_logger->entrance();
20739
20740 if ( is_array( $licenses ) ) {
20741 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
20742 $licenses[ $i ]->updated = time();
20743 }
20744 }
20745
20746 $this->_store_licenses( true, $module_id, $licenses );
20747 }
20748
20749 /**
20750 * @author Vova Feldman (@svovaf)
20751 * @since 1.0.4
20752 *
20753 * @param bool|number $plugin_id
20754 * @param bool $flush Since 1.1.7.3
20755 * @param int $expiration Since 1.2.2.7
20756 * @param bool|string $newer_than Since 2.2.1
20757 *
20758 * @return object|false New plugin tag info if exist.
20759 */
20760 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
20761 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
20762
20763 if ( ! is_object( $latest_tag ) ) {
20764 return false;
20765 }
20766
20767 $plugin_version = $this->get_plugin_version();
20768
20769 // Check if version is actually newer.
20770 $has_new_version =
20771 // If it's an non-installed add-on then always return latest.
20772 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
20773 // Compare versions.
20774 version_compare( $plugin_version, $latest_tag->version, '<' );
20775
20776 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
20777
20778 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
20779
20780 $this->_storage->beta_data = array(
20781 'is_beta' => $is_latest_version_beta,
20782 'version' => $latest_tag->version
20783 );
20784
20785 return $has_new_version ? $latest_tag : false;
20786 }
20787
20788 /**
20789 * @author Vova Feldman (@svovaf)
20790 * @since 1.0.5
20791 *
20792 * @param bool|number $plugin_id
20793 * @param bool $flush Since 1.1.7.3
20794 * @param int $expiration Since 1.2.2.7
20795 * @param bool|string $newer_than Since 2.2.1
20796 *
20797 * @return bool|FS_Plugin_Tag
20798 */
20799 function get_update( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
20800 $this->_logger->entrance();
20801
20802 if ( ! is_numeric( $plugin_id ) ) {
20803 $plugin_id = $this->_plugin->id;
20804 }
20805
20806 $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than );
20807 $updates = $this->get_all_updates();
20808
20809 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
20810 }
20811
20812 /**
20813 * Check if site assigned with active license.
20814 *
20815 * @author Vova Feldman (@svovaf)
20816 * @since 1.0.6
20817 *
20818 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
20819 */
20820 function has_active_license() {
20821 return (
20822 is_object( $this->_license ) &&
20823 is_numeric( $this->_license->id ) &&
20824 ! $this->_license->is_expired()
20825 );
20826 }
20827
20828 /**
20829 * Check if site assigned with active & valid (not expired) license.
20830 *
20831 * @author Vova Feldman (@svovaf)
20832 * @since 1.2.1
20833 *
20834 * @param bool $check_expiration
20835 */
20836 function has_active_valid_license( $check_expiration = true ) {
20837 return self::is_active_valid_license( $this->_license, $check_expiration );
20838 }
20839
20840 /**
20841 * @author Leo Fajardo (@leorw)
20842 * @since 2.3.1
20843 */
20844 function is_data_debug_mode() {
20845 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
20846 return false;
20847 }
20848
20849 $fs = $this->is_addon() ?
20850 $this->get_parent_instance() :
20851 $this;
20852
20853 if ( $fs->is_network_active() && fs_is_network_admin() ) {
20854 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
20855 } else {
20856 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
20857 }
20858
20859 return ( 'true' === $is_developer_license_debug_mode );
20860 }
20861
20862 /**
20863 * @author Leo Fajardo (@leorw)
20864 * @since 2.3.1
20865 */
20866 function _set_data_debug_mode() {
20867 if ( ! $this->is_whitelabeled( true ) ) {
20868 return;
20869 }
20870
20871 $license_or_user_key = fs_request_get_raw( 'license_or_user_key' );
20872
20873 $transient_value = ( ! empty( $license_or_user_key ) ) ?
20874 'true' :
20875 'false';
20876
20877 if ( 'true' === $transient_value ) {
20878 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
20879 'last_license_key' :
20880 'last_license_user_key'
20881 );
20882
20883 if ( md5( $license_or_user_key ) !== $stored_key ) {
20884 $this->shoot_ajax_failure( sprintf(
20885 '%s... %s',
20886 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
20887 $this->get_text_inline(
20888 'seems like the key you entered doesn\'t match our records.',
20889 'developer-or-license-not-found'
20890 )
20891 ) );
20892 }
20893 }
20894
20895 if ( $this->is_network_active() && fs_is_network_admin() ) {
20896 set_site_transient(
20897 "fs_{$this->get_id()}_data_debug_mode",
20898 $transient_value,
20899 WP_FS__TIME_24_HOURS_IN_SEC / 24
20900 );
20901 } else {
20902 set_transient(
20903 "fs_{$this->get_id()}_data_debug_mode",
20904 $transient_value,
20905 WP_FS__TIME_24_HOURS_IN_SEC / 24
20906 );
20907 }
20908
20909 if ( 'true' === $transient_value ) {
20910 $this->_admin_notices->add_sticky(
20911 $this->get_text_inline(
20912 '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.',
20913 'data_debug_mode_enabled'
20914 ),
20915 'data_debug_mode_enabled'
20916 );
20917 }
20918
20919 $this->shoot_ajax_success();
20920 }
20921
20922 /**
20923 * Check if a given license is active & valid (not expired).
20924 *
20925 * @author Vova Feldman (@svovaf)
20926 * @since 2.1.3
20927 *
20928 * @param FS_Plugin_License $license
20929 * @param bool $check_expiration
20930 *
20931 * @return bool
20932 */
20933 private static function is_active_valid_license( $license, $check_expiration = true ) {
20934 return (
20935 is_object( $license ) &&
20936 FS_Plugin_License::is_valid_id( $license->id ) &&
20937 $license->is_active() &&
20938 ( ! $check_expiration || $license->is_valid() )
20939 );
20940 }
20941
20942 /**
20943 * Checks if there's any site that is associated with an active & valid license.
20944 * This logic is used to determine if the admin can download the premium code base from a network level admin.
20945 *
20946 * @author Vova Feldman (@svovaf)
20947 * @since 2.1.3
20948 *
20949 * @return bool
20950 */
20951 function has_any_active_valid_license() {
20952 if ( ! fs_is_network_admin() ) {
20953 return $this->has_active_valid_license();
20954 }
20955
20956 $installs = $this->get_blog_install_map();
20957 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
20958
20959 foreach ( $installs as $blog_id => $install ) {
20960 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20961 continue;
20962 }
20963
20964 foreach ( $all_plugin_licenses as $license ) {
20965 if ( $license->id == $install->license_id ) {
20966 if ( self::is_active_valid_license( $license ) ) {
20967 return true;
20968 }
20969 }
20970 }
20971 }
20972
20973 return false;
20974 }
20975
20976 /**
20977 * Check if site assigned with license with enabled features.
20978 *
20979 * @author Vova Feldman (@svovaf)
20980 * @since 1.0.6
20981 *
20982 * @return bool
20983 */
20984 function has_features_enabled_license() {
20985 return (
20986 is_object( $this->_license ) &&
20987 is_numeric( $this->_license->id ) &&
20988 $this->_license->is_features_enabled()
20989 );
20990 }
20991
20992 /**
20993 * Checks if the product is activated with a bundle license.
20994 *
20995 * @author Leo Fajardo (@leorw)
20996 * @since 2.4.0
20997 *
20998 * @return bool
20999 */
21000 function is_activated_with_bundle_license() {
21001 if ( ! $this->has_features_enabled_license() ) {
21002 return false;
21003 }
21004
21005 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
21006 }
21007
21008 /**
21009 * Check if user is a trial or have feature enabled license.
21010 *
21011 * @author Vova Feldman (@svovaf)
21012 * @since 1.1.7
21013 *
21014 * @return bool
21015 */
21016 function can_use_premium_code() {
21017 return $this->is_trial() || $this->has_features_enabled_license();
21018 }
21019
21020 /**
21021 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
21022 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
21023 * the context user is authenticated.
21024 *
21025 * @author Leo Fajardo (@leorw)
21026 * @since 1.2.2
21027 *
21028 * @return bool
21029 */
21030 function is_user_admin() {
21031 /**
21032 * Require a super-admin when network activated, running from the network level OR if
21033 * running from the site level but not delegated the opt-in.
21034 *
21035 * @author Vova Feldman (@svovaf)
21036 * @since 2.0.0
21037 */
21038 if ( $this->_is_network_active &&
21039 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
21040 ) {
21041 return is_super_admin();
21042 }
21043
21044 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
21045 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
21046 }
21047
21048 /**
21049 * Sync site's plan.
21050 *
21051 * @author Vova Feldman (@svovaf)
21052 * @since 1.0.3
21053 *
21054 * @uses FS_Api
21055 *
21056 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
21057 * the admin.
21058 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
21059 * network-level "Account" page.
21060 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
21061 * `_sync_plugin_license` method in order to switch to the previous blog when sending
21062 * updates for a single site in case `execute_cron` has switched to a different blog.
21063 */
21064 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
21065 $this->_logger->entrance();
21066
21067 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
21068
21069 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
21070
21071 if ( $is_addon_sync ) {
21072 $this->_sync_addon_license( $plugin_id, $background );
21073 } else {
21074 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
21075 }
21076
21077 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
21078 }
21079
21080 /**
21081 * Sync plugin's add-on license.
21082 *
21083 * @author Vova Feldman (@svovaf)
21084 * @since 1.0.6
21085 * @uses FS_Api
21086 *
21087 * @param number $addon_id
21088 * @param bool $background
21089 */
21090 private function _sync_addon_license( $addon_id, $background ) {
21091 $this->_logger->entrance();
21092
21093 if ( $this->is_addon_activated( $addon_id ) ) {
21094 // If already installed, use add-on sync.
21095 $fs_addon = self::get_instance_by_id( $addon_id );
21096
21097 if (
21098 // Add-on is network activated and network integrated.
21099 $fs_addon->is_network_active() ||
21100 // Background sync cron.
21101 self::is_cron() ||
21102 // Add-on is not network activated or not network integrated.
21103 ! fs_is_network_admin()
21104 ) {
21105 $fs_addon->_sync_license( $background );
21106
21107 return;
21108 }
21109 }
21110
21111 // Validate add-on exists.
21112 $addon = $this->get_addon( $addon_id );
21113
21114 if ( ! is_object( $addon ) ) {
21115 return;
21116 }
21117
21118 // Add add-on into account add-ons.
21119 $account_addons = $this->get_account_addons();
21120 if ( ! is_array( $account_addons ) ) {
21121 $account_addons = array();
21122 }
21123 $account_addons[] = $addon->id;
21124 $account_addons = array_unique( $account_addons );
21125 $this->_store_account_addons( $account_addons );
21126
21127 // Load add-on licenses.
21128 $licenses = $this->_fetch_licenses( $addon->id );
21129
21130 // Sync add-on licenses.
21131 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
21132 $this->_update_licenses( $licenses, $addon->id );
21133
21134 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
21135 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
21136
21137 if ( ! isset( $plans_result->error ) ) {
21138 $plans = array();
21139 foreach ( $plans_result->plans as $plan ) {
21140 $plans[] = new FS_Plugin_Plan( $plan );
21141 }
21142
21143 $this->_admin_notices->add_sticky(
21144 sprintf(
21145 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
21146 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
21147 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
21148 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
21149 $addon->title
21150 ) . ' ' . $this->get_latest_download_link(
21151 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
21152 $addon_id
21153 ),
21154 'addon_plan_upgraded_' . $addon->slug,
21155 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
21156 );
21157 }
21158 }
21159 }
21160 }
21161
21162 /**
21163 * Sync site's plugin plan.
21164 *
21165 * @author Vova Feldman (@svovaf)
21166 * @since 1.0.6
21167 * @uses FS_Api
21168 *
21169 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
21170 * @param bool $send_installs_update Since 2.0.0
21171 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
21172 * syncing its license from the network-level "Account" page (e.g.: after
21173 * activating a license only for the single install).
21174 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
21175 * can be used here to switch to the previous blog in case `execute_cron`
21176 * has switched to a different blog.
21177 */
21178 private function _sync_plugin_license(
21179 $background = false,
21180 $send_installs_update = true,
21181 $is_context_single_site = false,
21182 $current_blog_id = null
21183 ) {
21184 $this->_logger->entrance();
21185
21186 $plan_change = 'none';
21187
21188 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
21189
21190 if ( ! $send_installs_update ) {
21191 $site = $this->_site;
21192 } else {
21193 /**
21194 * Sync site info.
21195 *
21196 * @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.
21197 */
21198 if ( $is_site_level_sync ) {
21199 /**
21200 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
21201 *
21202 * @author Leo Fajardo (@leorw)
21203 * @since 2.2.3
21204 */
21205 if ( is_numeric( $current_blog_id ) ) {
21206 $this->switch_to_blog( $current_blog_id );
21207 }
21208
21209 $result = $this->send_install_update( array(), true, true );
21210 $is_valid = $this->is_api_result_entity( $result );
21211 } else {
21212 $result = $this->send_installs_update( array(), true, true );
21213 $is_valid = $this->is_api_result_object( $result, 'installs' );
21214 }
21215
21216 if ( ! $is_valid ) {
21217 if ( $is_context_single_site ) {
21218 // Switch back to the main blog so that the following logic will have the right entities.
21219 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21220 }
21221
21222 // Show API message only if not background sync or if paying customer.
21223 if ( ! $background || $this->is_paying() ) {
21224 // Try to ping API to see if not blocked.
21225 if ( FS_Api::is_blocked( $result ) ) {
21226 /**
21227 * @author Vova Feldman (@svovaf)
21228 * @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.
21229 */
21230 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
21231 // Add notice immediately if not a background sync.
21232 $add_notice = ( ! $background );
21233
21234 if ( ! $add_notice ) {
21235 $counter = (int) get_transient( '_fs_api_connection_retry_counter' );
21236
21237 // We only want to add the notice after 3 consecutive failures.
21238 $add_notice = ( 3 <= $counter );
21239
21240 if ( ! $add_notice ) {
21241 /**
21242 * 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.
21243 *
21244 * 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.
21245 */
21246 set_transient( '_fs_api_connection_retry_counter', $counter + 1, WP_FS__TIME_WEEK_IN_SEC );
21247 }
21248 }
21249
21250 // Add notice instantly for not-background sync and only after 3 failed attempts for background sync.
21251 if ( $add_notice ) {
21252 self::$_global_admin_notices->add(
21253 $this->generate_api_blocked_notice_message_from_result( $result ),
21254 '',
21255 'error',
21256 $background,
21257 'api_blocked'
21258 );
21259
21260 add_action( 'admin_footer', array( 'Freemius', '_add_api_connectivity_notice_handler_js' ) );
21261
21262 // Notice was just shown, reset connectivity counter.
21263 delete_transient( '_fs_api_connection_retry_counter' );
21264 }
21265 }
21266 } else if ( is_object( $result ) ) {
21267 // Authentication params are broken.
21268 $this->_admin_notices->add(
21269 $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 ),
21270 '',
21271 'error'
21272 );
21273 }
21274 }
21275
21276 // No reason to continue with license sync while there are API issues.
21277 return;
21278 }
21279
21280 // API is working now. Delete the transient and start afresh.
21281 delete_transient('_fs_api_connection_retry_counter');
21282
21283 if ( $is_site_level_sync ) {
21284 $site = new FS_Site( $result );
21285 } else {
21286 // Map site addresses to their blog IDs.
21287 $address_to_blog_map = $this->get_address_to_blog_map();
21288
21289 // Find the current context install.
21290 $site = null;
21291 foreach ( $result->installs as $install ) {
21292 if ( $install->id == $this->_site->id ) {
21293 $site = new FS_Site( $install );
21294 } else {
21295 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
21296 $blog_id = $address_to_blog_map[ $address ];
21297
21298 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
21299 }
21300 }
21301 }
21302
21303 // Sync plans.
21304 $this->_sync_plans();
21305 }
21306
21307 // Remove sticky API connectivity message.
21308 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
21309
21310 if ( ! $this->has_paid_plan() ) {
21311 $this->_site = $site;
21312 $this->_store_site(
21313 true,
21314 $is_site_level_sync ?
21315 null :
21316 $this->get_network_install_blog_id()
21317 );
21318 } else {
21319 $context_blog_id = 0;
21320
21321 if ( $is_context_single_site ) {
21322 $context_blog_id = get_current_blog_id();
21323
21324 // Switch back to the main blog in order to properly sync the license.
21325 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21326 }
21327
21328 /**
21329 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
21330 * associated with that ID is not included in the user's licenses collection.
21331 */
21332 $this->_sync_licenses(
21333 $site->license_id,
21334 ( $is_context_single_site ?
21335 $context_blog_id :
21336 null
21337 )
21338 );
21339
21340 if ( $is_context_single_site ) {
21341 $this->switch_to_blog( $context_blog_id );
21342 }
21343
21344 // Check if plan / license changed.
21345 if ( $site->plan_id != $this->_site->plan_id ||
21346 // Check if trial started.
21347 $site->trial_plan_id != $this->_site->trial_plan_id ||
21348 $site->trial_ends != $this->_site->trial_ends ||
21349 // Check if license changed.
21350 $site->license_id != $this->_site->license_id
21351 ) {
21352 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
21353 // New trial started.
21354 $this->_site = $site;
21355 $plan_change = 'trial_started';
21356
21357 // For trial with subscription use-case.
21358 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
21359
21360 if ( is_object( $new_license ) && $new_license->is_valid() ) {
21361 $this->_site = $site;
21362 $this->_update_site_license( $new_license );
21363 $this->_store_licenses();
21364
21365 $this->_sync_site_subscription( $this->_license );
21366 }
21367 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
21368 // Was in trial, but now trial expired and no license ID.
21369 // New trial started.
21370 $this->_site = $site;
21371 $plan_change = 'trial_expired';
21372 } else {
21373 $is_free = $this->is_free_plan();
21374
21375 // Make sure license exist and not expired.
21376 $new_license = is_null( $site->license_id ) ?
21377 null :
21378 $this->_get_license_by_id( $site->license_id );
21379
21380 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
21381 // License cancelled.
21382 $this->_site = $site;
21383 $this->_update_site_license( $new_license );
21384 $this->_store_licenses();
21385
21386 $plan_change = 'cancelled';
21387 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
21388 // The license is expired, so ignore upgrade method.
21389 $this->_site = $site;
21390 } else {
21391 // License changed.
21392 $this->_site = $site;
21393
21394 /**
21395 * IMPORTANT:
21396 * 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.
21397 *
21398 * @author Vova Feldman (@svovaf)
21399 * @since 2.0.0
21400 */
21401 $this->_update_site_license( $new_license );
21402
21403 if ( ! $is_context_single_site &&
21404 fs_is_network_admin() &&
21405 $this->_is_network_active &&
21406 $new_license->quota > 1 &&
21407 get_blog_count() > 1
21408 ) {
21409 // See if license can activated on all sites.
21410 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
21411 if ( ! fs_request_get_bool( 'auto_install' ) ) {
21412 // Open the license activation dialog box on the account page.
21413 add_action( 'admin_footer', array(
21414 &$this,
21415 '_open_license_activation_dialog_box'
21416 ) );
21417 }
21418 }
21419 }
21420
21421 $this->_store_licenses();
21422
21423 $plan_change = $is_free ?
21424 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
21425 ( is_object( $new_license ) ?
21426 'changed' :
21427 'downgraded' );
21428 }
21429 }
21430
21431 // Store updated site info.
21432 $this->_store_site(
21433 true,
21434 $is_site_level_sync ?
21435 null :
21436 $this->get_network_install_blog_id()
21437 );
21438 } else {
21439 if ( ! is_object( $this->_license ) ) {
21440 $this->maybe_update_whitelabel_flag(
21441 FS_Plugin_License::is_valid_id( $site->license_id ) ?
21442 $this->get_license_by_id( $site->license_id ) :
21443 null
21444 );
21445 } else {
21446 $this->maybe_update_whitelabel_flag( $this->_license );
21447
21448 if ( $this->_license->is_expired() ) {
21449 if ( ! $this->has_features_enabled_license() ) {
21450 $this->_deactivate_license();
21451 $plan_change = 'downgraded';
21452 } else {
21453 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
21454
21455 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
21456 /**
21457 * Show the expired license notice every 14 days.
21458 *
21459 * @author Leo Fajardo (@leorw)
21460 * @since 2.3.1
21461 */
21462 $plan_change = 'expired';
21463 }
21464 }
21465 }
21466 }
21467
21468 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
21469 $this->_sync_site_subscription( $this->_license );
21470 }
21471 }
21472
21473 if ( ! $this->is_addon() &&
21474 $this->_site->is_beta() !== $site->is_beta()
21475 ) {
21476 // Beta flag updated.
21477 $this->_site = $site;
21478
21479 $this->_store_site(
21480 true,
21481 $is_site_level_sync ?
21482 null :
21483 $this->get_network_install_blog_id()
21484 );
21485 }
21486
21487 if ( $this->is_addon() || $this->has_addons() ) {
21488 /**
21489 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21490 * an updated valid user licenses collection will be fetched from the server which is used to also
21491 * update the account add-ons (add-ons the user has licenses for).
21492 *
21493 * @author Leo Fajardo (@leorw)
21494 * @since 2.2.4
21495 */
21496 $this->purge_valid_user_licenses_cache();
21497 }
21498 }
21499
21500 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21501
21502 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
21503 switch ( $plan_change ) {
21504 case 'none':
21505 if ( ! $background && is_admin() ) {
21506 $plan = $this->is_trial() ?
21507 $this->get_trial_plan() :
21508 $this->get_plan();
21509
21510 if ( $plan->is_free() ) {
21511 $this->_admin_notices->add(
21512 sprintf(
21513 $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' ),
21514 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
21515 ) . ' ' . sprintf(
21516 '<a href="%s">%s</a>',
21517 $this->contact_url(
21518 'bug',
21519 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' ),
21520 strtoupper( $plan->name )
21521 )
21522 ),
21523 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21524 ),
21525 $hmm_text
21526 );
21527 }
21528 }
21529 break;
21530 case 'upgraded':
21531 case 'activated':
21532 $this->add_after_plan_activation_or_upgrade_instructions_notice( 'upgraded' === $plan_change );
21533
21534 $this->_admin_notices->remove_sticky( array(
21535 'trial_started',
21536 'trial_promotion',
21537 'trial_expired',
21538 'activation_complete',
21539 'license_expired',
21540 ) );
21541 break;
21542 case 'changed':
21543 $this->_admin_notices->add_sticky(
21544 sprintf(
21545 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
21546 $this->get_plan_title()
21547 ),
21548 'plan_changed'
21549 );
21550
21551 $this->_admin_notices->remove_sticky( array(
21552 'trial_started',
21553 'trial_promotion',
21554 'trial_expired',
21555 'activation_complete',
21556 ) );
21557 break;
21558 case 'downgraded':
21559 $this->_admin_notices->add_sticky(
21560 ($this->has_free_plan() ?
21561 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 ) :
21562 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21563 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) ) ),
21564 'license_expired',
21565 $hmm_text
21566 );
21567 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21568 break;
21569 case 'cancelled':
21570 $this->_admin_notices->add(
21571 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
21572 sprintf(
21573 '<a href="%s">%s</a>',
21574 $this->contact_url( 'bug' ),
21575 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21576 ),
21577 $hmm_text,
21578 'error'
21579 );
21580 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21581 break;
21582 case 'expired':
21583 $this->_admin_notices->add_sticky(
21584 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 ),
21585 'license_expired',
21586 $hmm_text
21587 );
21588
21589 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
21590
21591 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21592 break;
21593 case 'trial_started':
21594 $this->add_complete_upgrade_instructions_notice(
21595 sprintf(
21596 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
21597 '<i>' . $this->get_plugin_name() . '</i>'
21598 ),
21599 'trial_started',
21600 $this->get_trial_plan()->title
21601 );
21602
21603 $this->_admin_notices->remove_sticky( array(
21604 'trial_promotion',
21605 ) );
21606 break;
21607 case 'trial_expired':
21608 $this->_admin_notices->add_sticky(
21609 ($this->has_free_plan() ?
21610 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
21611 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21612 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))),
21613 'trial_expired',
21614 $hmm_text
21615 );
21616 $this->_admin_notices->remove_sticky( array(
21617 'trial_started',
21618 'trial_promotion',
21619 'plan_upgraded',
21620 ) );
21621 break;
21622 }
21623 }
21624
21625 if ( 'none' !== $plan_change ) {
21626 if (
21627 ! is_object( $this->_license ) ||
21628 ! $this->_license->is_whitelabeled
21629 ) {
21630 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
21631 }
21632
21633 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
21634 }
21635 }
21636
21637 /**
21638 * @author Leo Fajardo (@leorw)
21639 * @since 2.5.4
21640 *
21641 * @param mixed $result
21642 *
21643 * @return string
21644 */
21645 private function generate_api_blocked_notice_message_from_result( $result ) {
21646 $api_domains = $this->apply_filters( 'api_domains', array(
21647 'api.freemius.com',
21648 'wp.freemius.com',
21649 ) );
21650
21651 $api_domains_list_items = '';
21652
21653 foreach( $api_domains as $api_domain ) {
21654 $api_domains_list_items .= "<li>{$api_domain}</li>";
21655 }
21656
21657 $error_message = sprintf(
21658 $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' ),
21659 $this->get_plugin_name(),
21660 "<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>"
21661 );
21662
21663 $error_message =
21664 "<div>{$error_message}</div>" .
21665 '<div class="fs-api-request-error-details" style="display: none">' .
21666 '<strong>' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . '</strong><br>' .
21667 $result->error->message .
21668 '</div>';
21669
21670 return $error_message;
21671 }
21672
21673 /**
21674 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
21675 *
21676 * @author Vova Feldman (@svovaf)
21677 * @since 2.0.0
21678 */
21679 public function _open_license_activation_dialog_box() {
21680 $vars = array( 'license_id' => $this->_site->license_id );
21681 fs_require_once_template( 'js/open-license-activation.php', $vars );
21682 }
21683
21684 /**
21685 * @author Vova Feldman (@svovaf)
21686 * @since 1.0.5
21687 *
21688 * @param bool $background
21689 * @param FS_Plugin_License|null $premium_license
21690 */
21691 protected function _activate_license( $background = false, $premium_license = null ) {
21692 $this->_logger->entrance();
21693
21694 if ( is_null( $premium_license ) ) {
21695 $license_id = fs_request_get( 'license_id' );
21696
21697 if ( is_object( $this->_site ) &&
21698 FS_Plugin_License::is_valid_id( $license_id ) &&
21699 $license_id == $this->_site->license_id
21700 ) {
21701 // License is already activated.
21702 return;
21703 }
21704
21705 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
21706 $this->_get_license_by_id( $license_id ) :
21707 $this->_get_available_premium_license();
21708 }
21709
21710 if ( ! is_object( $premium_license ) ) {
21711 return;
21712 }
21713
21714 if ( ! is_object( $this->_site ) ) {
21715 // Not yet opted-in.
21716 $user = $this->get_current_or_network_user();
21717 if ( ! is_object( $user ) ) {
21718 $user = self::_get_user_by_id( $premium_license->user_id );
21719 }
21720
21721 if ( is_object( $user ) ) {
21722 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
21723 } else {
21724 $this->opt_in(
21725 false,
21726 false,
21727 false,
21728 $premium_license->secret_key
21729 );
21730
21731 return;
21732 }
21733 }
21734
21735
21736 /**
21737 * If the premium license is already associated with the install, just
21738 * update the license reference (activation is not required).
21739 *
21740 * @since 1.1.9
21741 */
21742 if ( $premium_license->id == $this->_site->license_id ) {
21743 // License is already activated.
21744 $this->_update_site_license( $premium_license );
21745 $this->_store_account();
21746
21747 return;
21748 }
21749
21750 if ( $this->_site->user_id != $premium_license->user_id ) {
21751 $api_request_params = array( 'license_key' => $premium_license->secret_key );
21752 } else {
21753 $api_request_params = array();
21754 }
21755
21756 $api = $this->get_api_site_scope();
21757 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
21758
21759 if ( ! $this->is_api_result_entity( $license ) ) {
21760 if ( ! $background ) {
21761 $this->_admin_notices->add( sprintf(
21762 '%s %s',
21763 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
21764 ( is_object( $license ) && isset( $license->error ) ?
21765 $license->error->message :
21766 sprintf( '%s<br><code>%s</code>',
21767 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
21768 var_export( $license, true )
21769 )
21770 )
21771 ),
21772 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
21773 'error'
21774 );
21775 }
21776
21777 return;
21778 }
21779
21780 $premium_license = new FS_Plugin_License( $license );
21781
21782 // Updated site plan.
21783 $site = $this->get_api_site_scope()->get( '/', true );
21784 if ( $this->is_api_result_entity( $site ) ) {
21785 $this->_site = new FS_Site( $site );
21786 }
21787 $this->_update_site_license( $premium_license );
21788
21789 $this->_store_account();
21790
21791 if ( $this->is_addon() || $this->has_addons() ) {
21792 /**
21793 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21794 * an updated valid user licenses collection will be fetched from the server which is used to also
21795 * update the account add-ons (add-ons the user has licenses for).
21796 *
21797 * @author Leo Fajardo (@leorw)
21798 * @since 2.2.4
21799 */
21800 $this->purge_valid_user_licenses_cache();
21801 }
21802
21803 if ( ! $background ) {
21804 $this->add_complete_upgrade_instructions_notice(
21805 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ),
21806 'license_activated'
21807 );
21808 }
21809
21810 $this->_admin_notices->remove_sticky( array(
21811 'trial_promotion',
21812 'license_expired',
21813 ) );
21814 }
21815
21816 /**
21817 * @author Vova Feldman (@svovaf)
21818 * @since 1.0.5
21819 *
21820 * @param bool $show_notice
21821 */
21822 protected function _deactivate_license( $show_notice = true ) {
21823 $this->_logger->entrance();
21824
21825 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21826
21827 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
21828 $this->_admin_notices->add(
21829 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() ),
21830 $hmm_text
21831 );
21832
21833 return;
21834 }
21835
21836 $api = $this->get_api_site_scope();
21837 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
21838
21839 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
21840 }
21841
21842 /**
21843 * @author Leo Fajardo (@leorw)
21844 * @since 2.2.1
21845 *
21846 * @param FS_Plugin_License $license
21847 * @param bool|string $hmm_text
21848 * @param bool $show_notice
21849 */
21850 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
21851 if ( isset( $license->error ) ) {
21852 $this->_admin_notices->add(
21853 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
21854 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
21855 $hmm_text,
21856 'error'
21857 );
21858
21859 return;
21860 }
21861
21862 // Update license cache.
21863 if ( is_array( $this->_licenses ) ) {
21864 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
21865 if ( $license->id == $this->_licenses[ $i ]->id ) {
21866 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
21867 }
21868 }
21869 }
21870
21871 // Update site plan to default.
21872 $this->_sync_plans();
21873 $this->_site->plan_id = $this->_plans[0]->id;
21874 // Unlink license from site.
21875 $this->_update_site_license( null );
21876
21877 $this->_store_account();
21878
21879 if ( $show_notice ) {
21880 $this->_admin_notices->add(
21881 sprintf( $this->is_only_premium() ?
21882 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
21883 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
21884 $this->get_plan_title()
21885 ),
21886 $this->get_text_inline( 'O.K', 'ok' )
21887 );
21888 }
21889
21890 $this->_admin_notices->remove_sticky( array(
21891 'plan_upgraded',
21892 'license_activated',
21893 ) );
21894 }
21895
21896 /**
21897 * Site plan downgrade.
21898 *
21899 * @author Vova Feldman (@svovaf)
21900 * @since 1.0.4
21901 *
21902 * @return object
21903 *
21904 * @uses FS_Api
21905 */
21906 private function _downgrade_site() {
21907 $this->_logger->entrance();
21908
21909 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
21910
21911 $api = $this->get_api_site_scope();
21912 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
21913
21914 $plan_downgraded = false;
21915 $plan = false;
21916 if ( $this->is_api_result_entity( $site ) ) {
21917 $prev_plan_id = $this->_site->plan_id;
21918
21919 // Update new site plan id.
21920 $this->_site->plan_id = $site->plan_id;
21921
21922 $plan = $this->get_plan();
21923 $subscription = $this->_sync_site_subscription( $this->_license );
21924
21925 // Plan downgraded if plan was changed or subscription was cancelled.
21926 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
21927 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
21928 } else {
21929 // handle different error cases.
21930 $this->handle_license_deactivation_result(
21931 $site,
21932 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
21933 );
21934 }
21935
21936 if ( ! $plan_downgraded ) {
21937 return (object) array(
21938 'error' => (object) array(
21939 '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' )
21940 )
21941 );
21942 }
21943
21944 // Remove previous sticky message about upgrade (if exist).
21945 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21946
21947 $this->_admin_notices->add(
21948 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
21949 $plan->title,
21950 human_time_diff( time(), strtotime( $this->_license->expiration ) )
21951 )
21952 );
21953
21954 // Store site updates.
21955 $this->_store_site();
21956
21957 if ( $deactivate_license &&
21958 ! FS_Plugin_License::is_valid_id( $site->license_id )
21959 ) {
21960 if ( $this->_site->is_localhost() ) {
21961 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
21962 } else {
21963 $this->_license->activated = max( 0, $this->_license->activated - 1 );
21964 }
21965
21966 // Handle successful license deactivation result.
21967 $this->handle_license_deactivation_result( $this->_license );
21968 }
21969
21970 return $site;
21971 }
21972
21973 /**
21974 * @author Vova Feldman (@svovaf)
21975 * @since 1.1.8.1
21976 *
21977 * @param bool|string $plan_name
21978 *
21979 * @return bool If trial was successfully started.
21980 */
21981 function start_trial( $plan_name = false ) {
21982 $this->_logger->entrance();
21983
21984 // Alias.
21985 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
21986
21987 if ( $this->is_trial() ) {
21988 // Already in trial mode.
21989 $this->_admin_notices->add(
21990 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
21991 $oops_text,
21992 'error'
21993 );
21994
21995 return false;
21996 }
21997
21998 if ( $this->_site->is_trial_utilized() ) {
21999 // Trial was already utilized.
22000 $this->_admin_notices->add(
22001 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
22002 $oops_text,
22003 'error'
22004 );
22005
22006 return false;
22007 }
22008
22009 if ( false !== $plan_name ) {
22010 $plan = $this->get_plan_by_name( $plan_name );
22011
22012 if ( false === $plan ) {
22013 // Plan doesn't exist.
22014 $this->_admin_notices->add(
22015 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
22016 $oops_text,
22017 'error'
22018 );
22019
22020 return false;
22021 }
22022
22023 if ( ! $plan->has_trial() ) {
22024 // Plan doesn't exist.
22025 $this->_admin_notices->add(
22026 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
22027 $oops_text,
22028 'error'
22029 );
22030
22031 return false;
22032 }
22033 } else {
22034 if ( ! $this->has_trial_plan() ) {
22035 // None of the plans have a trial.
22036 $this->_admin_notices->add(
22037 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
22038 $oops_text,
22039 'error'
22040 );
22041
22042 return false;
22043 }
22044
22045 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
22046
22047 $plan = $plans_with_trial[0];
22048 }
22049
22050 $api = $this->get_api_site_scope();
22051 $plan = $api->call( "plans/{$plan->id}/trials.json", 'post' );
22052
22053 if ( ! $this->is_api_result_entity( $plan ) ) {
22054 // Some API error while trying to start the trial.
22055 $this->_admin_notices->add(
22056 $this->get_api_error_message( $plan ),
22057 $oops_text,
22058 'error'
22059 );
22060
22061 return false;
22062 }
22063
22064 // Sync license.
22065 $this->_sync_license();
22066
22067 return $this->is_trial();
22068 }
22069
22070 /**
22071 * Cancel site trial.
22072 *
22073 * @author Vova Feldman (@svovaf)
22074 * @since 1.0.9
22075 *
22076 * @return object
22077 *
22078 * @uses FS_Api
22079 */
22080 private function _cancel_trial() {
22081 $this->_logger->entrance();
22082
22083 if ( ! $this->is_trial() ) {
22084 return (object) array(
22085 'error' => (object) array(
22086 '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' )
22087 )
22088 );
22089 }
22090
22091 $trial_plan = $this->get_trial_plan();
22092
22093 $api = $this->get_api_site_scope();
22094 $site = $api->call( 'trials.json', 'delete' );
22095
22096 $trial_cancelled = false;
22097
22098 if ( $this->is_api_result_entity( $site ) ) {
22099 $prev_trial_ends = $this->_site->trial_ends;
22100
22101 if ( $this->is_paid_trial() ) {
22102 $this->_license->expiration = $site->trial_ends;
22103 $this->_license->is_cancelled = true;
22104 $this->_update_site_license( $this->_license );
22105 $this->_store_licenses();
22106
22107 // Clear subscription reference.
22108 $this->_sync_site_subscription( null );
22109 }
22110
22111 // Update site info.
22112 $this->_site = new FS_Site( $site );
22113
22114 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
22115 } else {
22116 // @todo handle different error cases.
22117 }
22118
22119 if ( ! $trial_cancelled ) {
22120 return (object) array(
22121 'error' => (object) array(
22122 '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' )
22123 )
22124 );
22125 }
22126
22127 // Remove previous sticky messages about upgrade or trial (if exist).
22128 $this->_admin_notices->remove_sticky( array(
22129 'trial_started',
22130 'trial_promotion',
22131 'plan_upgraded',
22132 ) );
22133
22134 // Store site updates.
22135 $this->_store_site();
22136
22137 if ( ! $this->is_addon() ||
22138 ! $this->deactivate_premium_only_addon_without_license( true )
22139 ) {
22140 $this->_admin_notices->add(
22141 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
22142 );
22143 }
22144
22145 return $site;
22146 }
22147
22148 /**
22149 * @author Vova Feldman (@svovaf)
22150 * @since 1.0.6
22151 *
22152 * @param bool|number $plugin_id
22153 *
22154 * @return bool
22155 */
22156 private function _is_addon_id( $plugin_id ) {
22157 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
22158 }
22159
22160 /**
22161 * Check if user eligible to download premium version updates.
22162 *
22163 * @author Vova Feldman (@svovaf)
22164 * @since 1.0.6
22165 *
22166 * @return bool
22167 */
22168 private function _can_download_premium() {
22169 return $this->has_any_active_valid_license() ||
22170 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
22171 }
22172
22173 /**
22174 *
22175 * @author Vova Feldman (@svovaf)
22176 * @since 1.0.6
22177 *
22178 * @param bool|number $addon_id
22179 * @param string $type "json" or "zip"
22180 *
22181 * @return string
22182 */
22183 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
22184
22185 $is_addon = $this->_is_addon_id( $addon_id );
22186
22187 $is_premium = null;
22188 if ( ! $is_addon ) {
22189 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
22190 } else if ( $this->is_addon_activated( $addon_id ) ) {
22191 $fs_addon = self::get_instance_by_id( $addon_id );
22192 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
22193 }
22194
22195 // If add-on, then append add-on ID.
22196 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
22197 '/updates/latest.' . $type;
22198
22199 // If add-on and not yet activated, try to fetch based on server licensing.
22200 if ( is_bool( $is_premium ) ) {
22201 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
22202 }
22203
22204 if ( $this->has_secret_key() ) {
22205 $endpoint = add_query_arg( 'type', 'all', $endpoint );
22206 } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) {
22207 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
22208 }
22209
22210 return $endpoint;
22211 }
22212
22213 /**
22214 * @author Vova Feldman (@svovaf)
22215 * @since 1.0.4
22216 *
22217 * @param bool|number $addon_id
22218 * @param bool $flush Since 1.1.7.3
22219 * @param int $expiration Since 1.2.2.7
22220 * @param bool|string $newer_than Since 2.2.1
22221 * @param bool|string $fetch_readme Since 2.2.1
22222 *
22223 * @return object|false Plugin latest tag info.
22224 */
22225 function _fetch_latest_version(
22226 $addon_id = false,
22227 $flush = true,
22228 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22229 $newer_than = false,
22230 $fetch_readme = true
22231 ) {
22232 $this->_logger->entrance();
22233
22234 if ( $this->is_unresolved_clone( true ) ) {
22235 return false;
22236 }
22237
22238 $switch_to_blog_id = null;
22239
22240 /**
22241 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
22242 * @since 1.1.7.4 Also check updates for add-ons.
22243 */
22244 if (
22245 ( ! $this->is_registered() || ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed() ) &&
22246 ! $this->_is_addon_id( $addon_id )
22247 ) {
22248 if ( ! is_multisite() ) {
22249 return false;
22250 }
22251
22252 $installs_map = $this->get_blog_install_map();
22253
22254 foreach ( $installs_map as $blog_id => $install ) {
22255 if ( ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed( $blog_id ) ) {
22256 continue;
22257 }
22258
22259 /**
22260 * @var FS_Site $install
22261 */
22262 if ( $install->is_trial() ) {
22263 $switch_to_blog_id = $blog_id;
22264 break;
22265 }
22266
22267 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
22268 $license = $this->get_license_by_id( $install->license_id );
22269
22270 if ( is_object( $license ) && $license->is_features_enabled() ) {
22271 $switch_to_blog_id = $blog_id;
22272 break;
22273 }
22274 }
22275 }
22276
22277 if ( is_null( $switch_to_blog_id ) ) {
22278 return false;
22279 }
22280 }
22281
22282 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
22283 get_current_blog_id() :
22284 0;
22285
22286 if ( is_numeric( $switch_to_blog_id ) ) {
22287 $this->switch_to_blog( $switch_to_blog_id );
22288 }
22289
22290 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
22291
22292 if ( ! empty( $newer_than ) ) {
22293 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
22294 }
22295
22296 if ( true === $fetch_readme ) {
22297 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
22298 }
22299
22300 $tag = $this->get_api_site_or_plugin_scope()->get(
22301 $latest_version_endpoint,
22302 $flush,
22303 $expiration
22304 );
22305
22306 if ( is_numeric( $switch_to_blog_id ) ) {
22307 $this->switch_to_blog( $current_blog_id );
22308 }
22309
22310 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
22311
22312 $this->_logger->departure( 'Latest version ' . $latest_version );
22313
22314 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
22315 }
22316
22317 #----------------------------------------------------------------------------------
22318 #region Download Plugin
22319 #----------------------------------------------------------------------------------
22320
22321 /**
22322 * Download latest plugin version, based on plan.
22323 *
22324 * Not like _download_latest(), this will redirect the page
22325 * to secure download url to prevent dual download (from FS to WP server,
22326 * and then from WP server to the client / browser).
22327 *
22328 * @author Vova Feldman (@svovaf)
22329 * @since 1.0.9
22330 *
22331 * @param bool|number $plugin_id
22332 *
22333 * @uses FS_Api
22334 * @uses wp_redirect()
22335 */
22336 private function download_latest_directly( $plugin_id = false ) {
22337 $this->_logger->entrance();
22338
22339 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
22340 }
22341
22342 /**
22343 * Get latest plugin FS API download URL.
22344 *
22345 * @author Vova Feldman (@svovaf)
22346 * @since 1.0.9
22347 *
22348 * @param bool|number $plugin_id
22349 *
22350 * @return string
22351 */
22352 private function get_latest_download_api_url( $plugin_id = false ) {
22353 $this->_logger->entrance();
22354
22355 $download_api_url = $this->get_api_site_scope()->get_signed_url(
22356 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
22357 );
22358
22359 return str_replace( 'http:', 'https:', $download_api_url );
22360 }
22361
22362 /**
22363 * Get payment invoice URL.
22364 *
22365 * @author Vova Feldman (@svovaf)
22366 * @since 1.2.0
22367 *
22368 * @param bool|number $payment_id
22369 *
22370 * @return string
22371 */
22372 function _get_invoice_api_url( $payment_id = false ) {
22373 $this->_logger->entrance();
22374
22375 $url = $this->get_api_user_scope()->get_signed_url(
22376 "/payments/{$payment_id}/invoice.pdf"
22377 );
22378
22379 if ( ! fs_starts_with( $url, 'https://' ) ) {
22380 // Always use HTTPS for invoices.
22381 $url = 'https' . substr( $url, 4 );
22382 }
22383
22384 return $url;
22385 }
22386
22387 /**
22388 * Get latest plugin download link.
22389 *
22390 * @author Vova Feldman (@svovaf)
22391 * @since 1.0.9
22392 *
22393 * @param string $label
22394 * @param bool|number $plugin_id
22395 *
22396 * @return string
22397 */
22398 private function get_latest_download_link( $label, $plugin_id = false ) {
22399 return sprintf(
22400 '<a target="_blank" rel="noopener" href="%s">%s</a>',
22401 $this->_get_latest_download_local_url( $plugin_id ),
22402 $label
22403 );
22404 }
22405
22406 /**
22407 * Get latest plugin download local URL.
22408 *
22409 * @author Vova Feldman (@svovaf)
22410 * @since 1.0.9
22411 *
22412 * @param bool|number $plugin_id
22413 *
22414 * @return string
22415 */
22416 function _get_latest_download_local_url( $plugin_id = false ) {
22417 // Add timestamp to protect from caching.
22418 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
22419
22420 if ( ! empty( $plugin_id ) ) {
22421 $params['plugin_id'] = $plugin_id;
22422 } else if ( $this->is_addon() ) {
22423 $params['plugin_id'] = $this->get_id();
22424 }
22425
22426 $fs = $this->is_addon() ?
22427 $this->get_parent_instance() :
22428 $this;
22429
22430 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
22431 }
22432
22433 #endregion Download Plugin ------------------------------------------------------------------
22434
22435 /**
22436 * @author Vova Feldman (@svovaf)
22437 * @since 1.0.4
22438 *
22439 * @uses FS_Api
22440 *
22441 * @param bool $background Hints the method if it's a background updates check. If false, it means that
22442 * was initiated by the admin.
22443 * @param bool|number $plugin_id
22444 * @param bool $flush Since 1.1.7.3
22445 * @param int $expiration Since 1.2.2.7
22446 * @param bool|string $newer_than Since 2.2.1
22447 */
22448 private function check_updates(
22449 $background = false,
22450 $plugin_id = false,
22451 $flush = true,
22452 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22453 $newer_than = false
22454 ) {
22455 $this->_logger->entrance();
22456
22457 // Check if there's a newer version for download.
22458 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than );
22459
22460 $update = null;
22461 if ( is_object( $new_version ) ) {
22462 $update = new FS_Plugin_Tag( $new_version );
22463
22464 if ( ! $background ) {
22465 $this->_admin_notices->add(
22466 sprintf(
22467 /* translators: %s: Numeric version number (e.g. '2.1.9' */
22468 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
22469 $update->version,
22470 sprintf(
22471 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22472 $this->get_account_url( 'download_latest' ),
22473 sprintf(
22474 /* translators: %s: plan name (e.g. latest "Professional" version) */
22475 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
22476 $this->get_plan_title()
22477 )
22478 )
22479 ),
22480 $this->get_text_inline( 'New', 'new' ) . '!'
22481 );
22482 }
22483 } else if ( false === $new_version && ! $background ) {
22484 $this->_admin_notices->add(
22485 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
22486 $this->get_text_inline( 'You are all good!', 'you-are-good' )
22487 );
22488 }
22489
22490 $this->_store_update( $update, true, $plugin_id );
22491 }
22492
22493 /**
22494 * @author Vova Feldman (@svovaf)
22495 * @since 1.0.4
22496 *
22497 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
22498 *
22499 * @return FS_Plugin[]
22500 *
22501 * @uses FS_Api
22502 */
22503 private function sync_addons( $flush = false ) {
22504 $this->_logger->entrance();
22505
22506 $api = $this->get_api_site_or_plugin_scope();
22507
22508 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
22509
22510 /**
22511 * @since 1.2.1
22512 *
22513 * If there's a cached version of the add-ons and not asking
22514 * for a flush, just use the currently stored add-ons.
22515 */
22516 if ( ! $flush && $api->is_cached( $path ) ) {
22517 $addons = self::get_all_addons();
22518
22519 return isset( $addons[ $this->_plugin->id ] ) ?
22520 $addons[ $this->_plugin->id ] :
22521 array();
22522 }
22523
22524 $result = $api->get( $path, $flush );
22525
22526 $addons = array();
22527 if ( $this->is_api_result_object( $result, 'plugins' ) &&
22528 is_array( $result->plugins )
22529 ) {
22530 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
22531 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
22532 }
22533
22534 $this->_store_addons( $addons, true );
22535 }
22536
22537 return $addons;
22538 }
22539
22540 /**
22541 * Handle user email update.
22542 *
22543 * @author Vova Feldman (@svovaf)
22544 * @since 1.0.3
22545 * @uses FS_Api
22546 *
22547 * @param string $new_email
22548 *
22549 * @return object
22550 */
22551 private function update_email( $new_email ) {
22552 $this->_logger->entrance();
22553
22554 $api = $this->get_api_user_scope();
22555 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
22556 'email' => $new_email,
22557 'after_email_confirm_url' => $this->_get_admin_page_url(
22558 'account',
22559 array( 'fs_action' => 'sync_user' )
22560 ),
22561 ) );
22562
22563 if ( ! isset( $user->error ) ) {
22564 $this->_user->email = $user->email;
22565 $this->_user->is_verified = $user->is_verified;
22566 $this->_store_user();
22567 } else {
22568 // handle different error cases.
22569 }
22570
22571 return $user;
22572 }
22573
22574 #----------------------------------------------------------------------------------
22575 #region API Error Handling
22576 #----------------------------------------------------------------------------------
22577
22578 /**
22579 * @author Vova Feldman (@svovaf)
22580 * @since 1.1.1
22581 *
22582 * @param mixed $result
22583 *
22584 * @return bool Is API result contains an error.
22585 */
22586 private function is_api_error( $result ) {
22587 return FS_Api::is_api_error( $result );
22588 }
22589
22590 /**
22591 * Checks if given API result is a non-empty and not an error object.
22592 *
22593 * @author Vova Feldman (@svovaf)
22594 * @since 1.2.1.5
22595 *
22596 * @param mixed $result
22597 * @param string|null $required_property Optional property we want to verify that is set.
22598 *
22599 * @return bool
22600 */
22601 function is_api_result_object( $result, $required_property = null ) {
22602 return FS_Api::is_api_result_object( $result, $required_property );
22603 }
22604
22605 /**
22606 * Checks if given API result is a non-empty entity object with non-empty ID.
22607 *
22608 * @author Vova Feldman (@svovaf)
22609 * @since 1.2.1.5
22610 *
22611 * @param mixed $result
22612 *
22613 * @return bool
22614 */
22615 private function is_api_result_entity( $result ) {
22616 return FS_Api::is_api_result_entity( $result );
22617 }
22618
22619 #endregion
22620
22621 /**
22622 * Make sure a given argument is an array of a specific type.
22623 *
22624 * @author Vova Feldman (@svovaf)
22625 * @since 1.2.1.5
22626 *
22627 * @param mixed $array
22628 * @param string $class
22629 *
22630 * @return bool
22631 */
22632 private function is_array_instanceof( $array, $class ) {
22633 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
22634 }
22635
22636 /**
22637 * Start install ownership change.
22638 *
22639 * @author Vova Feldman (@svovaf)
22640 * @since 1.1.1
22641 * @uses FS_Api
22642 *
22643 * @param string $candidate_email
22644 * @param string $transfer_type
22645 *
22646 * @return bool Is ownership change successfully initiated.
22647 */
22648 private function init_change_owner( $candidate_email, $transfer_type ) {
22649 $this->_logger->entrance();
22650
22651 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
22652 $install_ids = array();
22653
22654 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
22655 $install = $install_info['install'];
22656
22657 if ( $this->_user->id != $install->user_id ) {
22658 // Skip add-on installs that are not owned by the parent product's install's owner.
22659 continue;
22660 }
22661
22662 $install_ids[ $slug ] = $install->id;
22663 }
22664
22665 $api = $this->get_api_site_scope();
22666 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
22667 'email' => $candidate_email,
22668 'transfer_type' => $transfer_type,
22669 'install_ids' => implode( ',', array_values( $install_ids ) ),
22670 'after_confirm_url' => $this->_get_admin_page_url(
22671 'account',
22672 array( 'fs_action' => 'change_owner' )
22673 ),
22674 ) );
22675
22676 return ! $this->is_api_error( $result );
22677 }
22678
22679 /**
22680 * Handle install ownership change.
22681 *
22682 * @author Vova Feldman (@svovaf)
22683 * @since 1.1.1
22684 * @uses FS_Api
22685 *
22686 * @return bool Was ownership change successfully complete.
22687 */
22688 private function complete_change_owner() {
22689 $this->_logger->entrance();
22690
22691 $install_ids = fs_request_get( 'install_ids' );
22692
22693 if ( ! empty( $install_ids ) ) {
22694 $install_ids = explode( ',', $install_ids );
22695
22696 foreach ( $install_ids as $key => $install_id ) {
22697 if ( ! FS_Site::is_valid_id( $install_id ) ) {
22698 unset( $install_ids[ $key ] );
22699 }
22700 }
22701 }
22702
22703 if ( ! is_array( $install_ids ) ) {
22704 $install_ids = array();
22705 }
22706
22707 $user = new FS_User();
22708 $user->id = fs_request_get( 'user_id' );
22709 $user->public_key = fs_request_get_raw( 'user_public_key' );
22710 $user->secret_key = fs_request_get_raw( 'user_secret_key' );
22711
22712 $prev_user = $this->_user;
22713 $this->_user = $user;
22714
22715 $result = $this->get_api_user_scope( true )->get(
22716 "/installs.json?install_ids=" . implode( ',', $install_ids )
22717 );
22718
22719 $current_blog_sites = self::get_all_sites( $this->get_module_type() );
22720
22721 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22722 $site_id_slug_map = array();
22723
22724 foreach ( $current_blog_sites as $slug => $site ) {
22725 $site_id_slug_map[ $site->id ] = $slug;
22726 }
22727
22728 foreach ( $result->installs as $install ) {
22729 $site = new FS_Site( $install );
22730
22731 if ( ! isset( $site_id_slug_map[ $install->id ] ) ) {
22732 continue;
22733 }
22734
22735 $current_blog_sites[ $site_id_slug_map[ $install->id ] ] = clone $site;
22736
22737 if ( $this->_site->id == $site->id ) {
22738 $this->_site = $site;
22739 }
22740 }
22741 }
22742
22743 // Validate install's user and given user.
22744 if ( $user->id != $this->_site->user_id ) {
22745 $this->_user = $prev_user;
22746
22747 return false;
22748 }
22749
22750 $this->set_account_option( 'sites', $current_blog_sites, true );
22751
22752 // Fetch new user information.
22753 $user_result = $this->get_api_user_scope( true )->get();
22754 $user = new FS_User( $user_result );
22755 $this->_user = $user;
22756
22757 $this->_set_account( $user, $this->_site );
22758
22759 $remove_user = true;
22760 $all_modules_sites = self::get_all_modules_sites();
22761
22762 foreach ( $all_modules_sites as $sites_by_module_type ) {
22763 foreach ( $sites_by_module_type as $sites_by_slug ) {
22764 foreach ( $sites_by_slug as $site ) {
22765 if ( $prev_user->id == $site->user_id ) {
22766 $remove_user = false;
22767 break;
22768 }
22769 }
22770
22771 if ( ! $remove_user ) {
22772 break;
22773 }
22774 }
22775
22776 if ( ! $remove_user ) {
22777 break;
22778 }
22779 }
22780
22781 if ( $remove_user ) {
22782 $users = self::get_all_users();
22783
22784 if ( isset( $users[ $prev_user->id ] ) ) {
22785 unset( $users[ $prev_user->id ] );
22786 } else {
22787 // If the prev user wasn't found by the key, iterate over the users collection.
22788 foreach ( $users as $key => $user ) {
22789 if ( $user->id == $prev_user->id ) {
22790 unset( $users[ $key ] );
22791 break;
22792 }
22793 }
22794 }
22795
22796 $this->set_account_option( 'users', $users, true );
22797 }
22798
22799 return true;
22800 }
22801
22802 /**
22803 * Completes ownership change by license.
22804 *
22805 * @author Leo Fajardo (@leorw)
22806 * @since 2.3.2
22807 *
22808 * @param number $user_id
22809 * @param array[string]number $install_ids_by_slug_map
22810 *
22811 */
22812 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
22813 $this->_logger->entrance();
22814
22815 $this->sync_user_by_current_install( $user_id );
22816
22817 $result = $this->get_api_user_scope( true )->get(
22818 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
22819 );
22820
22821 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22822 $sites = self::get_all_sites( $this->get_module_type() );
22823 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
22824
22825 foreach ( $result->installs as $install ) {
22826 $site = new FS_Site( $install );
22827
22828 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
22829 }
22830
22831 $this->set_account_option( 'sites', $sites, true );
22832 }
22833 }
22834
22835 /**
22836 * Handle user name update.
22837 *
22838 * @author Vova Feldman (@svovaf)
22839 * @since 1.0.9
22840 * @uses FS_Api
22841 *
22842 * @return object
22843 */
22844 private function update_user_name() {
22845 $this->_logger->entrance();
22846 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
22847
22848 $api = $this->get_api_user_scope();
22849 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
22850 'name' => $name,
22851 ) );
22852
22853 if ( ! isset( $user->error ) ) {
22854 $this->_user->first = $user->first;
22855 $this->_user->last = $user->last;
22856 $this->_store_user();
22857 } else {
22858 // handle different error cases.
22859
22860 }
22861
22862 return $user;
22863 }
22864
22865 /**
22866 * Verify user email.
22867 *
22868 * @author Vova Feldman (@svovaf)
22869 * @since 1.0.3
22870 * @uses FS_Api
22871 */
22872 private function verify_email() {
22873 $this->_handle_account_user_sync();
22874
22875 if ( $this->_user->is_verified() ) {
22876 return;
22877 }
22878
22879 $api = $this->get_api_site_scope();
22880 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
22881 'after_email_confirm_url' => $this->_get_admin_page_url(
22882 'account',
22883 array( 'fs_action' => 'sync_user' )
22884 )
22885 ) );
22886
22887 if ( ! isset( $result->error ) ) {
22888 $this->_admin_notices->add( sprintf(
22889 $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' ),
22890 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
22891 ) );
22892 } else {
22893 // handle different error cases.
22894
22895 }
22896 }
22897
22898 /**
22899 * @author Vova Feldman (@svovaf)
22900 * @since 1.1.2
22901 *
22902 * @param array $params
22903 * @param bool|null $network
22904 *
22905 * @return string
22906 */
22907 function get_activation_url( $params = array(), $network = null ) {
22908 if ( $this->is_addon() && $this->has_free_plan() ) {
22909 /**
22910 * @author Vova Feldman (@svovaf)
22911 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
22912 */
22913 return $this->get_parent_instance()->get_activation_url( $params );
22914 }
22915
22916 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
22917 }
22918
22919 /**
22920 * @author Vova Feldman (@svovaf)
22921 * @since 1.2.1.5
22922 *
22923 * @param array $params
22924 *
22925 * @return string
22926 */
22927 function get_reconnect_url( $params = array() ) {
22928 $params['fs_action'] = 'reset_anonymous_mode';
22929 $params['fs_unique_affix'] = $this->get_unique_affix();
22930
22931 return $this->get_activation_url( $params );
22932 }
22933
22934 /**
22935 * Get the URL of the page that should be loaded after the user connect
22936 * or skip in the opt-in screen.
22937 *
22938 * @author Vova Feldman (@svovaf)
22939 * @since 1.1.3
22940 *
22941 * @param string $filter Filter name.
22942 * @param array $params Since 1.2.2.7
22943 * @param bool|null $network
22944 *
22945 * @return string
22946 */
22947 function get_after_activation_url( $filter, $params = array(), $network = null ) {
22948 if ( $this->show_opt_in_on_themes_page() &&
22949 ( fs_request_has( 'pending_activation' ) ||
22950 // For cases when the first time path is set, even though it's a WP.org theme.
22951 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
22952 ) {
22953 $first_time_path = '';
22954 } else {
22955 $first_time_path = $this->_menu->get_first_time_path(
22956 fs_is_network_admin() && $this->_is_network_active
22957 );
22958 }
22959
22960 if ( $this->_is_network_active &&
22961 fs_is_network_admin() &&
22962 ! $this->_menu->has_network_menu() &&
22963 $this->is_network_registered()
22964 ) {
22965 $target_url = $this->get_account_url();
22966 } else {
22967 // Default plugin's page.
22968 $target_url = $this->_get_admin_page_url( '', array(), $network );
22969 }
22970
22971 return add_query_arg( $params, $this->apply_filters(
22972 $filter,
22973 empty( $first_time_path ) ?
22974 $target_url :
22975 $first_time_path
22976 ) );
22977 }
22978
22979 /**
22980 * Handle account page updates / edits / actions.
22981 *
22982 * @author Vova Feldman (@svovaf)
22983 * @since 1.0.2
22984 *
22985 */
22986 private function _handle_account_edits() {
22987 if ( ! $this->is_user_admin() ) {
22988 return;
22989 }
22990
22991 $action = fs_get_action();
22992
22993 if ( empty( $action ) ) {
22994 return;
22995 }
22996
22997 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
22998 $install_id = fs_request_get( 'install_id', '' );
22999
23000 // Alias.
23001 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
23002
23003 $is_network_action = $this->is_network_level_action();
23004 $blog_id = $this->is_network_level_site_specific_action();
23005 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
23006
23007 if ( is_numeric( $blog_id ) ) {
23008 $this->switch_to_blog( $blog_id );
23009 } else {
23010 $blog_id = '';
23011 }
23012
23013 switch ( $action ) {
23014 case 'opt_in':
23015 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23016
23017 if ( $is_parent_plugin_action ) {
23018 if ( $is_network_action && ! empty( $blog_id ) ) {
23019 if ( ! $this->is_registered() ) {
23020 $this->install_with_user(
23021 $this->get_network_user(),
23022 false,
23023 false,
23024 false,
23025 false
23026 );
23027
23028 $this->_admin_notices->add(
23029 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
23030 $this->get_text_inline( 'Awesome', 'awesome' )
23031 );
23032 }
23033 }
23034 }
23035 break;
23036
23037 case 'toggle_tracking':
23038 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23039
23040 if ( $is_parent_plugin_action ) {
23041 if ( $is_network_action && ! empty( $blog_id ) ) {
23042 if ( $this->is_registered( true ) ) {
23043 if ( $this->is_tracking_prohibited( $blog_id ) ) {
23044 if ( $this->toggle_site_tracking( true, $blog_id ) ) {
23045 $this->_admin_notices->add(
23046 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>" ),
23047 $this->get_text_inline( 'Thank you!', 'thank-you' )
23048 );
23049 }
23050 } else {
23051 if ( $this->toggle_site_tracking( false, $blog_id ) ) {
23052 $install = $this->get_install_by_blog_id( $blog_id );
23053
23054 $this->_admin_notices->add(
23055 sprintf(
23056 $this->get_text_inline( 'Diagnostic data will no longer be sent from %s to %s.', 'opted-out-successfully' ),
23057 self::get_unfiltered_site_url( $blog_id, true ),
23058 "<b>{$this->get_plugin_title()}</b>"
23059 )
23060 );
23061 }
23062 }
23063 }
23064 }
23065 }
23066
23067 break;
23068
23069 case 'delete_account':
23070 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23071
23072 $is_network_deletion = $is_network_action && empty( $blog_id );
23073
23074 if ( $is_parent_plugin_action ) {
23075 // Delete add-on installs if have any.
23076 $installed_addons = $this->get_installed_addons();
23077 foreach ( $installed_addons as $fs_addon ) {
23078 if ( $is_network_deletion ) {
23079 $fs_addon->delete_network_account_event();
23080 } else {
23081 $fs_addon->delete_account_event();
23082 }
23083 }
23084
23085 if ( $is_network_deletion ) {
23086 $this->delete_network_account_event();
23087 } else {
23088 $this->delete_account_event();
23089 }
23090
23091 // Clear user and site.
23092 $this->_site = null;
23093 $this->_user = null;
23094
23095 $this->maybe_set_slug_and_network_menu_exists_flag();
23096
23097 fs_redirect( $this->get_activation_url() );
23098 } else {
23099 if ( $this->is_addon_activated( $plugin_id ) ) {
23100 $fs_addon = self::get_instance_by_id( $plugin_id );
23101
23102 if ( $is_network_deletion ) {
23103 $fs_addon->delete_network_account_event();
23104 } else {
23105 $fs_addon->delete_account_event();
23106 }
23107
23108 fs_redirect( $this->_get_admin_page_url( 'account' ) );
23109 }
23110 }
23111
23112 return;
23113
23114 case 'downgrade_account':
23115 if ( is_numeric( $blog_id ) ) {
23116 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23117 } else {
23118 check_admin_referer( $action );
23119 }
23120
23121 $switch_to_network_install_blog_after_cancellation = (
23122 is_numeric( $blog_id ) &&
23123 $plugin_id == $this->get_id() &&
23124 ! $this->is_trial()
23125 );
23126
23127 $result = $this->cancel_subscription_or_trial( $plugin_id );
23128 if ( $this->is_api_error( $result ) ) {
23129 $this->_admin_notices->add(
23130 $result->error->message,
23131 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23132 'error'
23133 );
23134 }
23135
23136 if ( $switch_to_network_install_blog_after_cancellation ) {
23137 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23138 }
23139
23140 return;
23141
23142 case 'activate_license':
23143 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23144
23145 $fs = $this;
23146 if ( $plugin_id != $this->get_id() ) {
23147 $fs = $this->is_addon_activated( $plugin_id ) ?
23148 self::get_instance_by_id( $plugin_id ) :
23149 null;
23150 }
23151
23152 if ( is_object( $fs ) ) {
23153 $fs->_activate_license();
23154
23155 /**
23156 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
23157 *
23158 * @author Leo Fajardo (@leorw)
23159 * @since 2.4.0
23160 */
23161 unset( $_REQUEST['plugin_id'] );
23162
23163 if ( $this->is_bundle_license_auto_activation_enabled() ) {
23164 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
23165 }
23166 }
23167
23168 return;
23169
23170 case 'deactivate_license':
23171 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23172
23173 if ( $plugin_id == $this->get_id() ) {
23174 $this->_deactivate_license();
23175
23176 if ( $this->is_only_premium() ) {
23177 // Clear user and site.
23178 $this->_site = null;
23179 $this->_user = null;
23180
23181 if ( ! $is_network_action ) {
23182 fs_redirect( $this->get_activation_url() );
23183 } else if ( is_numeric( $blog_id ) ) {
23184 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23185 }
23186 }
23187 } else {
23188 if ( $this->is_addon_activated( $plugin_id ) ) {
23189 $fs_addon = self::get_instance_by_id( $plugin_id );
23190 $fs_addon->_deactivate_license();
23191 }
23192 }
23193
23194 return;
23195
23196 case 'check_updates':
23197 check_admin_referer( $action );
23198 $this->check_updates();
23199
23200 return;
23201
23202 case 'change_owner':
23203 $state = fs_request_get( 'state', 'init' );
23204 switch ( $state ) {
23205 case 'init':
23206 // The nonce is injected by the error handler in `_email_address_update_ajax_handler` function.
23207 check_admin_referer( 'change_owner' );
23208
23209 $candidate_email = fs_request_get( 'candidate_email' );
23210 $transfer_type = fs_request_get( 'transfer_type' );
23211
23212 if ( $this->init_change_owner( $candidate_email, $transfer_type ) ) {
23213 if ( 'transfer' === $transfer_type ) {
23214 $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>' ) );
23215 } else {
23216 $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>' ) );
23217 }
23218 }
23219 break;
23220 case 'owner_confirmed':
23221 // We cannot (or need not to) check the nonce and referer here, because the link comes from the email sent by our API.
23222 $candidate_email = fs_request_get( 'candidate_email', '' );
23223
23224 if ( ! is_email($candidate_email ) ) {
23225 return;
23226 }
23227
23228 $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>' ) );
23229 break;
23230 case 'candidate_confirmed':
23231 // We do not need to validate the authenticity of this request here, because the `complete_change_owner` does that for us through API calls.
23232 if ( $this->complete_change_owner() ) {
23233 $this->_admin_notices->add_sticky(
23234 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
23235 'ownership_changed',
23236 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
23237 );
23238 } else {
23239 // @todo Handle failed ownership change message.
23240 }
23241 break;
23242 }
23243
23244 return;
23245
23246 case 'update_user_name':
23247 check_admin_referer( 'update_user_name' );
23248
23249 $result = $this->update_user_name();
23250
23251 if ( isset( $result->error ) ) {
23252 $this->_admin_notices->add(
23253 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
23254 $oops_text,
23255 'error'
23256 );
23257 } else {
23258 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
23259 }
23260
23261 return;
23262
23263 #region Actions that might be called from external links (e.g. email)
23264
23265 /**
23266 * !!IMPORTANT!!: We cannot check for a valid nonce in this region, because the links could be coming from emails.
23267 */
23268
23269 case 'cancel_trial':
23270 $result = $this->cancel_subscription_or_trial( $plugin_id );
23271 if ( $this->is_api_error( $result ) ) {
23272 $this->_admin_notices->add(
23273 $result->error->message,
23274 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23275 'error'
23276 );
23277 }
23278
23279 return;
23280
23281 case 'verify_email':
23282 $this->verify_email();
23283
23284 return;
23285
23286 case 'sync_user':
23287 $this->_handle_account_user_sync();
23288
23289 return;
23290
23291 case $this->get_unique_affix() . '_sync_license':
23292 $this->_sync_license();
23293
23294 return;
23295
23296 case 'download_latest':
23297 $this->download_latest_directly( $plugin_id );
23298
23299 return;
23300
23301 #endregion
23302 }
23303
23304 if ( WP_FS__IS_POST_REQUEST ) {
23305 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
23306 foreach ( $properties as $p ) {
23307 if ( 'update_' . $p === $action ) {
23308 check_admin_referer( $action );
23309
23310 $this->_logger->log( $action );
23311
23312 $site_property = substr( $p, strlen( 'site_' ) );
23313 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
23314 $this->get_site()->{$site_property} = $site_property_value;
23315
23316 // Store account after modification.
23317 $this->_store_site();
23318
23319 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
23320
23321 $this->_admin_notices->add( sprintf(
23322 /* translators: %s: User's account property (e.g. email address, name) */
23323 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
23324 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
23325 ) );
23326
23327 return;
23328 }
23329 }
23330 }
23331 }
23332
23333 /**
23334 * Adds CSS classes for the body tag in the admin.
23335 *
23336 * @param string $classes Space-separated string of class names.
23337 *
23338 * @return string $classes FS Admin body tag class names.
23339 */
23340 public function fs_addons_body_class( $classes ) {
23341 $classes .= ' plugins-php';
23342 return $classes;
23343 }
23344
23345 /**
23346 * Account page resources load.
23347 *
23348 * @author Vova Feldman (@svovaf)
23349 * @since 1.0.6
23350 */
23351 function _account_page_load() {
23352 $this->_logger->entrance();
23353
23354 $this->_logger->info( var_export( $_REQUEST, true ) );
23355
23356 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
23357
23358 if ( $this->has_addons() ) {
23359 wp_enqueue_script( 'plugin-install' );
23360 add_thickbox();
23361 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23362 }
23363
23364 if ( $this->has_paid_plan() &&
23365 ! $this->has_any_license() &&
23366 ! $this->is_sync_executed() &&
23367 $this->is_tracking_allowed()
23368 ) {
23369 /**
23370 * If no licenses found and no sync job was executed during the last 24 hours,
23371 * just execute the sync job right away (blocking execution).
23372 *
23373 * @since 1.1.7.3
23374 */
23375 $this->run_manual_sync();
23376 }
23377
23378 $this->_handle_account_edits();
23379
23380 if (
23381 is_object( $this->_license ) &&
23382 $this->_license->user_id == $this->_user->id &&
23383 ! $this->is_whitelabeled( true )
23384 ) {
23385 $this->_admin_notices->add(
23386 sprintf(
23387 $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' ),
23388 sprintf(
23389 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
23390 $this->get_text_inline( 'Click here', 'click-here' )
23391 )
23392 ),
23393 '',
23394 'success',
23395 false,
23396 'license_not_whitelabeled'
23397 );
23398 }
23399
23400 $this->do_action( 'account_page_load_before_departure' );
23401 }
23402
23403 /**
23404 * Renders the "Affiliation" page.
23405 *
23406 * @author Leo Fajardo (@leorw)
23407 * @since 1.2.3
23408 */
23409 function _affiliation_page_render() {
23410 $this->_logger->entrance();
23411
23412 $this->fetch_affiliate_and_terms();
23413
23414 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
23415
23416 $is_bundle_context = $this->has_bundle_context();
23417
23418 $plugin_title = $this->get_plugin_title();
23419
23420 if ( $is_bundle_context ) {
23421 $plugin_title = $this->plugin_affiliate_terms->plugin_title;
23422
23423 // Add the suffix "Bundle" only if the word is not present in the title itself.
23424 if ( false === mb_stripos( $plugin_title, fs_text_inline( 'Bundle', 'bundle' ) ) ) {
23425 $plugin_title = $this->apply_filters(
23426 'formatted_bundle_title',
23427 $plugin_title . ' ' . fs_text_inline( 'Bundle', 'bundle' )
23428 );
23429 }
23430 }
23431
23432 $vars = array(
23433 'id' => $this->_module_id,
23434 'plugin_title' => $plugin_title,
23435 );
23436 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
23437 }
23438
23439
23440 /**
23441 * Render account page.
23442 *
23443 * @author Vova Feldman (@svovaf)
23444 * @since 1.0.0
23445 */
23446 function _account_page_render() {
23447 $this->_logger->entrance();
23448
23449 $template = 'account.php';
23450 $vars = array( 'id' => $this->_module_id );
23451
23452 /**
23453 * Added filter to the template to allow developers wrapping the template
23454 * in custom HTML (e.g. within a wizard/tabs).
23455 *
23456 * @author Vova Feldman (@svovaf)
23457 * @since 1.2.1.6
23458 */
23459 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
23460 }
23461
23462 /**
23463 * Render account connect page.
23464 *
23465 * @author Vova Feldman (@svovaf)
23466 * @since 1.0.7
23467 */
23468 function _connect_page_render() {
23469 $this->_logger->entrance();
23470
23471 $vars = array( 'id' => $this->_module_id );
23472
23473 /**
23474 * Added filter to the template to allow developers wrapping the template
23475 * in custom HTML (e.g. within a wizard/tabs).
23476 *
23477 * @author Vova Feldman (@svovaf)
23478 * @since 1.2.1.6
23479 */
23480 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
23481 }
23482
23483 /**
23484 * Load required resources before add-ons page render.
23485 *
23486 * @author Vova Feldman (@svovaf)
23487 * @since 1.0.6
23488 */
23489 function _addons_page_load() {
23490 $this->_logger->entrance();
23491
23492 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
23493
23494 wp_enqueue_script( 'plugin-install' );
23495 add_thickbox();
23496 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23497
23498 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
23499 $this->_admin_notices->add(
23500 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>' ),
23501 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
23502 'update-nag'
23503 );
23504 }
23505 }
23506
23507 /**
23508 * Render add-ons page.
23509 *
23510 * @author Vova Feldman (@svovaf)
23511 * @since 1.0.6
23512 */
23513 function _addons_page_render() {
23514 $this->_logger->entrance();
23515
23516 $vars = array( 'id' => $this->_module_id );
23517
23518 /**
23519 * Added filter to the template to allow developers wrapping the template
23520 * in custom HTML (e.g. within a wizard/tabs).
23521 *
23522 * @author Vova Feldman (@svovaf)
23523 * @since 1.2.1.6
23524 */
23525 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
23526 }
23527
23528 /* Pricing & Upgrade
23529 ------------------------------------------------------------------------------------------------------------------*/
23530 /**
23531 * Render pricing page.
23532 *
23533 * @author Vova Feldman (@svovaf)
23534 * @since 1.0.0
23535 */
23536 function _pricing_page_render() {
23537 $this->_logger->entrance();
23538
23539 $vars = array( 'id' => $this->_module_id );
23540
23541 if ( 'true' === fs_request_get( 'checkout', false ) ) {
23542 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
23543 } else {
23544 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
23545 }
23546 }
23547
23548 /**
23549 * @author Leo Fajardo (@leorw)
23550 * @since 2.3.1
23551 */
23552 function _maybe_add_pricing_ajax_handler() {
23553 if ( ! $this->should_use_external_pricing() ) {
23554 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
23555 }
23556 }
23557
23558 /**
23559 * @author Leo Fajardo (@leorw)
23560 * @since 2.3.1
23561 */
23562 function _fs_pricing_ajax_action_handler() {
23563 $this->check_ajax_referer( 'pricing_ajax_action' );
23564
23565 $result = null;
23566 $pricing_action = fs_request_get( 'pricing_action' );
23567
23568 switch ( $pricing_action ) {
23569 case 'fetch_pricing_data':
23570 $params = array(
23571 'is_enriched' => true,
23572 'trial' => fs_request_get_bool( 'trial' ),
23573 'sandbox' => fs_request_get_raw( 'sandbox' ),
23574 's_ctx_type' => fs_request_get_raw( 's_ctx_type' ),
23575 's_ctx_id' => fs_request_get_raw( 's_ctx_id' ),
23576 's_ctx_ts' => fs_request_get_raw( 's_ctx_ts' ),
23577 's_ctx_secure' => fs_request_get_raw( 's_ctx_secure' ),
23578 );
23579
23580 $bundle_id = $this->get_bundle_id();
23581 $bundle_public_key = $this->get_bundle_public_key();
23582
23583 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
23584
23585 if ( ! $has_bundle_context ) {
23586 $api = $this->get_api_plugin_scope();
23587 } else {
23588 $api = FS_Api::instance(
23589 $bundle_id,
23590 'plugin',
23591 $bundle_id,
23592 $bundle_public_key,
23593 ! $this->is_live(),
23594 false,
23595 $this->get_sdk_version()
23596 );
23597
23598 $params['plugin_id'] = $this->get_id();
23599 $params['plugin_public_key'] = $this->get_public_key();
23600 }
23601
23602 $result = $api->get( 'pricing.json?' . http_build_query( $params ) );
23603 break;
23604 case 'start_trial':
23605 $result = $this->opt_in(
23606 false,
23607 false,
23608 false,
23609 false,
23610 false,
23611 fs_request_get( 'plan_id' )
23612 );
23613 }
23614
23615 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
23616 $this->_logger->api_error( $result );
23617
23618 self::shoot_ajax_failure(
23619 isset( $result->error ) ?
23620 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
23621 var_export( $result, true )
23622 );
23623 }
23624
23625 $this->shoot_ajax_success( $result );
23626 }
23627
23628 #----------------------------------------------------------------------------------
23629 #region Contact Us
23630 #----------------------------------------------------------------------------------
23631
23632 /**
23633 * Render contact-us page.
23634 *
23635 * @author Vova Feldman (@svovaf)
23636 * @since 1.0.3
23637 */
23638 function _contact_page_render() {
23639 $this->_logger->entrance();
23640
23641 $vars = array( 'id' => $this->_module_id );
23642
23643 /**
23644 * Added filter to the template to allow developers wrapping the template
23645 * in custom HTML (e.g. within a wizard/tabs).
23646 *
23647 * @author Vova Feldman (@svovaf)
23648 * @since 2.1.3
23649 */
23650 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
23651 }
23652
23653 #endregion ------------------------------------------------------------------------
23654
23655 /**
23656 * Hide all admin notices to prevent distractions.
23657 *
23658 * @author Vova Feldman (@svovaf)
23659 * @since 1.0.3
23660 *
23661 * @uses remove_all_actions()
23662 */
23663 private static function _hide_admin_notices() {
23664 remove_all_actions( 'admin_notices' );
23665 remove_all_actions( 'network_admin_notices' );
23666 remove_all_actions( 'all_admin_notices' );
23667 remove_all_actions( 'user_admin_notices' );
23668 }
23669
23670 static function _clean_admin_content_section_hook() {
23671 $hide_admin_notices = true;
23672
23673 if ( fs_request_is_action( 'allow_clone_resolution_notice' ) ) {
23674 check_admin_referer( 'fs_allow_clone_resolution_notice' );
23675
23676 $hide_admin_notices = false;
23677 }
23678
23679 if ( $hide_admin_notices ) {
23680 self::_hide_admin_notices();
23681 }
23682
23683 // Hide footer.
23684 echo '<style>#wpfooter { display: none !important; }</style>';
23685 }
23686
23687 /**
23688 * Attach to admin_head hook to hide all admin notices.
23689 *
23690 * @author Vova Feldman (@svovaf)
23691 * @since 1.0.3
23692 */
23693 static function _clean_admin_content_section() {
23694 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
23695 }
23696
23697 /* CSS & JavaScript
23698 ------------------------------------------------------------------------------------------------------------------*/
23699 /* function _enqueue_script($handle, $src) {
23700 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
23701
23702 $this->_logger->entrance( 'script = ' . $url );
23703
23704 wp_enqueue_script( $handle, $url );
23705 }*/
23706
23707 /* SDK
23708 ------------------------------------------------------------------------------------------------------------------*/
23709 private $_user_api;
23710
23711 /**
23712 *
23713 * @author Vova Feldman (@svovaf)
23714 * @since 1.0.2
23715 *
23716 * @param bool $flush
23717 *
23718 * @return FS_Api
23719 */
23720 function get_api_user_scope( $flush = false ) {
23721 if ( ! isset( $this->_user_api ) || $flush ) {
23722 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
23723 }
23724
23725 return $this->_user_api;
23726 }
23727
23728 /**
23729 * @author Vova Feldman (@svovaf)
23730 * @since 2.0.0
23731 *
23732 * @param \FS_User $user
23733 *
23734 * @return \FS_Api
23735 */
23736 private function get_api_user_scope_by_user( FS_User $user ) {
23737 return FS_Api::instance(
23738 $this->_module_id,
23739 'user',
23740 $user->id,
23741 $user->public_key,
23742 ! $this->is_live(),
23743 $user->secret_key,
23744 $this->get_sdk_version()
23745 );
23746 }
23747
23748 /**
23749 *
23750 * @author Leo Fajardo (@leorw)
23751 * @since 2.0.0
23752 *
23753 * @param bool $flush
23754 *
23755 * @return FS_Api
23756 */
23757 private function get_current_or_network_user_api_scope( $flush = false ) {
23758 if ( ! $this->_is_network_active ||
23759 ( isset( $this->_user ) && $this->_user instanceof FS_User )
23760 ) {
23761 return $this->get_api_user_scope( $flush );
23762 }
23763
23764 $user = $this->get_current_or_network_user();
23765
23766 $this->_user_api = FS_Api::instance(
23767 $this->_module_id,
23768 'user',
23769 $user->id,
23770 $user->public_key,
23771 ! $this->is_live(),
23772 $user->secret_key,
23773 $this->get_sdk_version()
23774 );
23775
23776 return $this->_user_api;
23777 }
23778
23779 private $_site_api;
23780
23781 /**
23782 *
23783 * @author Vova Feldman (@svovaf)
23784 * @since 1.0.2
23785 *
23786 * @param bool $flush
23787 *
23788 * @return FS_Api
23789 */
23790 private function get_api_site_scope( $flush = false ) {
23791 if ( ! isset( $this->_site_api ) || $flush ) {
23792 $this->_site_api = FS_Api::instance(
23793 $this->_module_id,
23794 'install',
23795 $this->_site->id,
23796 $this->_site->public_key,
23797 ! $this->is_live(),
23798 $this->_site->secret_key,
23799 $this->get_sdk_version(),
23800 self::get_unfiltered_site_url()
23801 );
23802 }
23803
23804 return $this->_site_api;
23805 }
23806
23807 /**
23808 * @author Leo Fajardo (@leorw)
23809 * @since 2.5.0
23810 *
23811 * @param string $path
23812 * @param string $method
23813 * @param array $params
23814 * @param bool $flush_instance
23815 *
23816 * @return array|mixed|string|void
23817 * @throws Freemius_Exception
23818 */
23819 private function api_site_call( $path, $method = 'GET', $params = array(), $flush_instance = false ) {
23820 $result = $this->get_api_site_scope( $flush_instance )->call( $path, $method, $params );
23821
23822 /**
23823 * 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.
23824 *
23825 * @author Leo Fajardo (@leorw)
23826 * @since 2.5.0
23827 */
23828 if (
23829 $this->is_registered() &&
23830 FS_Api::is_api_result_entity( $result ) &&
23831 isset( $result->url )
23832 ) {
23833 $stored_local_url = trailingslashit( $this->_site->url );
23834 $stored_remote_url = trailingslashit( $result->url );
23835
23836 if ( $stored_local_url !== $stored_remote_url ) {
23837 $this->_site->url = $result->url;
23838 $this->_store_site();
23839 }
23840
23841 if ( fs_strip_url_protocol( $stored_remote_url ) !== self::get_unfiltered_site_url( null, true, true ) ) {
23842 FS_Clone_Manager::instance()->maybe_run_clone_resolution();
23843 }
23844 }
23845
23846 return $result;
23847 }
23848
23849 private $_plugin_api;
23850
23851 /**
23852 * Get plugin public API scope.
23853 *
23854 * @author Vova Feldman (@svovaf)
23855 * @since 1.0.7
23856 *
23857 * @return FS_Api
23858 */
23859 function get_api_plugin_scope() {
23860 if ( ! isset( $this->_plugin_api ) ) {
23861 $this->_plugin_api = FS_Api::instance(
23862 $this->_module_id,
23863 'plugin',
23864 $this->_plugin->id,
23865 $this->_plugin->public_key,
23866 ! $this->is_live(),
23867 false,
23868 $this->get_sdk_version()
23869 );
23870 }
23871
23872 return $this->_plugin_api;
23873 }
23874
23875 /**
23876 * Get bundle public API scope.
23877 *
23878 * @author Vova Feldman (@svovaf)
23879 * @since 2.3.1
23880 *
23881 * @return FS_Api
23882 */
23883 function get_api_bundle_scope() {
23884 return FS_Api::instance(
23885 $this->get_bundle_id(),
23886 'plugin',
23887 $this->get_bundle_id(),
23888 $this->get_bundle_public_key(),
23889 ! $this->is_live(),
23890 false,
23891 $this->get_sdk_version()
23892 );
23893 }
23894
23895 /**
23896 * Get site API scope object (fallback to public plugin scope when not registered).
23897 *
23898 * @author Vova Feldman (@svovaf)
23899 * @since 1.0.7
23900 *
23901 * @return FS_Api
23902 */
23903 function get_api_site_or_plugin_scope() {
23904 return $this->is_registered() ?
23905 $this->get_api_site_scope() :
23906 $this->get_api_plugin_scope();
23907 }
23908
23909 /**
23910 * @author Leo Fajardo (@leorw)
23911 * @since 2.2.3.1
23912 *
23913 * @param object $result
23914 */
23915 private function maybe_modify_api_curl_error_message( $result ) {
23916 if (
23917 'cUrlMissing' !== $result->error->type &&
23918 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
23919 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
23920 ) {
23921 return;
23922 }
23923
23924 $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' ) .
23925 ' ' .
23926 $this->esc_html_inline(
23927 sprintf(
23928 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
23929 implode(
23930 ', ',
23931 $this->apply_filters( 'api_domains', array(
23932 'api.freemius.com',
23933 'wp.freemius.com'
23934 ) )
23935 )
23936 ),
23937 'connectivity-whitelist'
23938 ) .
23939 ' ' .
23940 sprintf(
23941 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
23942 $this->get_module_type()
23943 );
23944 }
23945
23946 /**
23947 * Show trial promotional notice (if any trial exist).
23948 *
23949 * @author Vova Feldman (@svovaf)
23950 * @since 1.0.9
23951 *
23952 * @param FS_Plugin_Plan[] $plans
23953 */
23954 function _check_for_trial_plans( $plans ) {
23955 /**
23956 * 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.
23957 *
23958 * @author Vova Feldman (@svovaf)
23959 * @since 2.1.2
23960 */
23961 if ( ! is_array( $plans ) && is_object( $plans ) ) {
23962 $plans = array( $plans );
23963 }
23964
23965 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
23966 $plans = array();
23967 }
23968
23969 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
23970 }
23971
23972 /**
23973 * During trial promotion the "upgrade" submenu item turns to
23974 * "start trial" to encourage the trial. Since we want to keep
23975 * the same menu item handler and there's no robust way to
23976 * add new arguments to the menu item link's querystring,
23977 * use JavaScript to find the menu item and update the href of
23978 * the link.
23979 *
23980 * @author Vova Feldman (@svovaf)
23981 * @since 1.2.1.5
23982 */
23983 function _fix_start_trial_menu_item_url() {
23984 $template_args = array( 'id' => $this->_module_id );
23985 fs_require_template( 'add-trial-to-pricing.php', $template_args );
23986 }
23987
23988 /**
23989 * Check if module is currently in a trial promotion mode.
23990 *
23991 * @author Vova Feldman (@svovaf)
23992 * @since 1.2.2.7
23993 *
23994 * @return bool
23995 */
23996 function is_in_trial_promotion() {
23997 return $this->_admin_notices->has_sticky( 'trial_promotion' );
23998 }
23999
24000 /**
24001 * Show trial promotional notice (if any trial exist).
24002 *
24003 * @author Vova Feldman (@svovaf)
24004 * @since 1.0.9
24005 *
24006 * @return bool If trial notice added.
24007 */
24008 function _add_trial_notice() {
24009 if ( ! $this->is_user_admin() ) {
24010 return false;
24011 }
24012
24013 if ( ! $this->is_user_in_admin() ) {
24014 return false;
24015 }
24016
24017 if ( $this->_is_network_active ) {
24018 if ( fs_is_network_admin() ) {
24019 // Network level trial is disabled at the moment.
24020 return false;
24021 }
24022
24023 if ( ! $this->is_delegated_connection() ) {
24024 // Only delegated sites should support trials.
24025 return false;
24026 }
24027 }
24028
24029 // Check if trial message is already shown.
24030 if ( $this->is_in_trial_promotion() ) {
24031 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
24032
24033 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
24034
24035 return false;
24036 }
24037
24038 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
24039 // Don't show trial if running the premium code, unless running in DEV mode.
24040 return false;
24041 }
24042
24043 if ( ! $this->has_trial_plan() ) {
24044 // No plans with trial.
24045 return false;
24046 }
24047
24048 if ( ! $this->apply_filters( 'show_trial', true ) ) {
24049 // Developer explicitly asked not to show the trial promo.
24050 return false;
24051 }
24052
24053 if ( $this->is_registered() ) {
24054 // Check if trial already utilized.
24055 if ( $this->_site->is_trial_utilized() ) {
24056 return false;
24057 }
24058
24059 if ( $this->is_paying_or_trial() ) {
24060 // Don't show trial if paying or already in trial.
24061 return false;
24062 }
24063 }
24064
24065 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24066 // If not yet opted-in/skipped, or pending activation, don't show trial.
24067 return false;
24068 }
24069
24070 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
24071 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
24072
24073 // Show promotion if never shown before and 24 hours after initial activation with FS.
24074 if ( ! $was_promotion_shown_before &&
24075 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
24076 ) {
24077 return false;
24078 }
24079
24080 // OR if promotion was shown before, try showing it every 30 days.
24081 if ( $was_promotion_shown_before &&
24082 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
24083 ) {
24084 return false;
24085 }
24086
24087 $trial_period = $this->_trial_days;
24088 $require_payment = $this->_is_trial_require_payment;
24089 $trial_url = $this->get_trial_url();
24090 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
24091
24092 if ( $this->is_registered() ) {
24093 // If opted-in, override trial with up to date data from API.
24094 $trial_plans = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
24095 $trial_plans_count = count( $trial_plans );
24096
24097 if ( 0 === $trial_plans_count ) {
24098 // If there's no plans with a trial just exit.
24099 return false;
24100 }
24101
24102 /**
24103 * @var FS_Plugin_Plan $paid_plan
24104 */
24105 $paid_plan = $trial_plans[0];
24106 $require_payment = $paid_plan->is_require_subscription;
24107 $trial_period = $paid_plan->trial_period;
24108
24109 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
24110
24111 if ( $total_paid_plans !== $trial_plans_count ) {
24112 // Not all paid plans have a trial - generate a string of those that have it.
24113 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
24114 $plans_string .= sprintf(
24115 ' <a href="%s">%s</a>',
24116 $trial_url,
24117 $trial_plans[ $i ]->title
24118 );
24119
24120 if ( $i < $trial_plans_count - 2 ) {
24121 $plans_string .= ', ';
24122 } else if ( $i == $trial_plans_count - 2 ) {
24123 $plans_string .= ' and ';
24124 }
24125 }
24126 }
24127 }
24128
24129 $message = sprintf(
24130 $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' ),
24131 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
24132 $plans_string,
24133 $trial_period
24134 );
24135
24136 // "No Credit-Card Required" or "No Commitment for N Days".
24137 $cc_string = $require_payment ?
24138 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
24139 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
24140
24141
24142 // Start trial button.
24143 $button = ' ' . sprintf(
24144 '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24145 $trial_url,
24146 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
24147 );
24148
24149 $this->_admin_notices->add_sticky(
24150 $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
24151 'trial_promotion',
24152 '',
24153 'promotion'
24154 );
24155
24156 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
24157
24158 return true;
24159 }
24160
24161 /**
24162 * Lets users/customers know that the product has an affiliate program.
24163 *
24164 * @author Leo Fajardo (@leorw)
24165 * @since 1.2.2.11
24166 *
24167 * @return bool Returns true if the notice has been added.
24168 */
24169 function _add_affiliate_program_notice() {
24170 if ( ! $this->is_user_admin() ) {
24171 return false;
24172 }
24173
24174 if ( ! $this->is_user_in_admin() ) {
24175 return false;
24176 }
24177
24178 // Check if the notice is already shown.
24179 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
24180 return false;
24181 }
24182
24183 if (
24184 // Product has no affiliate program.
24185 ! $this->has_affiliate_program() ||
24186 // User has applied for an affiliate account.
24187 ! empty( $this->_storage->affiliate_application_data )
24188 ) {
24189 return false;
24190 }
24191
24192 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
24193 // Developer explicitly asked not to show the notice about the affiliate program.
24194 return false;
24195 }
24196
24197 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24198 // If not yet opted in/skipped, or pending activation, don't show the notice.
24199 return false;
24200 }
24201
24202 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
24203 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
24204
24205 /**
24206 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
24207 * activation with FS.
24208 */
24209 if ( $was_notice_shown_before ||
24210 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
24211 ) {
24212 return false;
24213 }
24214
24215 if ( ! $this->is_paying() &&
24216 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
24217 ) {
24218 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
24219 return false;
24220 }
24221
24222 $message = sprintf(
24223 $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' ),
24224 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
24225 $this->get_module_label( true )
24226 );
24227
24228 // HTML code for the "Learn more..." button.
24229 $button = ' ' . sprintf(
24230 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24231 $this->_get_admin_page_url( 'affiliation' ),
24232 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
24233 );
24234
24235 $this->_admin_notices->add_sticky(
24236 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
24237 'affiliate_program',
24238 '',
24239 'promotion'
24240 );
24241
24242 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
24243
24244 return true;
24245 }
24246
24247 /**
24248 * @author Vova Feldman (@svovaf)
24249 * @since 1.2.1.5
24250 */
24251 function _enqueue_common_css() {
24252 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
24253 // Add basic CSS for admin-notices and menu-item colors.
24254 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
24255 }
24256 }
24257
24258 /**
24259 * @author Leo Fajardo (@leorw)
24260 * @since 1.2.2
24261 */
24262 function _show_theme_activation_optin_dialog() {
24263 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
24264
24265 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
24266 }
24267
24268 /**
24269 * @author Leo Fajardo (@leorw)
24270 * @since 1.2.2
24271 */
24272 function _add_fs_theme_activation_dialog() {
24273 global $pagenow;
24274
24275 if ( 'themes.php' !== $pagenow ) {
24276 return;
24277 }
24278
24279 $vars = array( 'id' => $this->_module_id );
24280 fs_require_once_template( 'connect.php', $vars );
24281 }
24282
24283 /* Action Links
24284 ------------------------------------------------------------------------------------------------------------------*/
24285 private $_action_links_hooked = false;
24286 private $_action_links = array();
24287
24288 /**
24289 * Hook to plugin action links filter.
24290 *
24291 * @author Vova Feldman (@svovaf)
24292 * @since 1.0.0
24293 */
24294 private function hook_plugin_action_links() {
24295 $this->_logger->entrance();
24296
24297 $this->_action_links_hooked = true;
24298
24299 $this->_logger->log( 'Adding action links hooks.' );
24300
24301 // Add action link to settings page.
24302 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
24303 &$this,
24304 '_modify_plugin_action_links_hook'
24305 ), WP_FS__DEFAULT_PRIORITY, 2 );
24306 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
24307 &$this,
24308 '_modify_plugin_action_links_hook'
24309 ), WP_FS__DEFAULT_PRIORITY, 2 );
24310 }
24311
24312 /**
24313 * Add plugin action link.
24314 *
24315 * @author Vova Feldman (@svovaf)
24316 * @since 1.0.0
24317 *
24318 * @param $label
24319 * @param $url
24320 * @param bool $external
24321 * @param int $priority
24322 * @param bool $key
24323 */
24324 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
24325 $this->_logger->entrance();
24326
24327 if ( ! isset( $this->_action_links[ $priority ] ) ) {
24328 $this->_action_links[ $priority ] = array();
24329 }
24330
24331 if ( false === $key ) {
24332 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
24333 }
24334
24335 $this->_action_links[ $priority ][] = array(
24336 'label' => $label,
24337 'href' => $url,
24338 'key' => $key,
24339 'external' => $external
24340 );
24341 }
24342
24343 /**
24344 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
24345 *
24346 * @author Vova Feldman (@svovaf)
24347 * @since 1.0.0
24348 */
24349 function _add_upgrade_action_link() {
24350 $this->_logger->entrance();
24351
24352 $is_activation_mode = $this->is_activation_mode();
24353
24354 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
24355
24356 /**
24357 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
24358 * and "Add-Ons" menus should be added.
24359 *
24360 * @author Leo Fajardo (@leorw)
24361 * @since 2.3.0
24362 */
24363 $add_upgrade_link = (
24364 $add_action_links ||
24365 ( $is_activation_mode && $this->is_only_premium() )
24366 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
24367
24368 $add_addons_link = ( $add_action_links && $this->has_addons() );
24369
24370 if ( ! $add_upgrade_link && ! $add_addons_link ) {
24371 return;
24372 }
24373
24374 if (
24375 $add_upgrade_link &&
24376 $this->is_pricing_page_visible() &&
24377 $this->is_submenu_item_visible( 'pricing' )
24378 ) {
24379 $this->add_plugin_action_link(
24380 $this->get_text_inline( 'Upgrade', 'upgrade' ),
24381 $this->get_upgrade_url(),
24382 false,
24383 7,
24384 'upgrade'
24385 );
24386 }
24387
24388 if (
24389 $add_addons_link &&
24390 $this->has_addons() &&
24391 $this->is_submenu_item_visible( 'addons' )
24392 ) {
24393 $this->add_plugin_action_link(
24394 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
24395 $this->_get_admin_page_url( 'addons' ),
24396 false,
24397 9,
24398 'addons'
24399 );
24400 }
24401 }
24402
24403 /**
24404 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
24405 *
24406 * @author Leo Fajardo (@leorw)
24407 * @since 1.1.9
24408 */
24409 function _add_license_action_link() {
24410 $this->_logger->entrance();
24411
24412 if ( ! self::is_ajax() ) {
24413 // Inject license activation dialog UI and client side code.
24414 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
24415 }
24416
24417 $link_text = $this->is_free_plan() ?
24418 $this->get_text_inline( 'Activate License', 'activate-license' ) :
24419 $this->get_text_inline( 'Change License', 'change-license' );
24420
24421 $this->add_plugin_action_link(
24422 $link_text,
24423 '#',
24424 false,
24425 11,
24426 ( 'activate-license ' . $this->get_unique_affix() )
24427 );
24428 }
24429
24430 /**
24431 * @author Leo Fajardo (@leorw)
24432 * @since 2.0.2
24433 */
24434 function _add_premium_version_upgrade_selection_action() {
24435 $this->_logger->entrance();
24436
24437 if ( ! self::is_ajax() ) {
24438 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
24439 }
24440 }
24441
24442 /**
24443 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
24444 *
24445 * @author Leo Fajardo (@leorw)
24446 * @since 1.2.1.5
24447 */
24448 function _add_tracking_links() {
24449 if ( ! current_user_can( 'manage_options' ) ) {
24450 return;
24451 }
24452
24453 $this->_logger->entrance();
24454
24455 if ( $this->is_only_premium() && $this->is_free_plan() ) {
24456 // 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.
24457 return;
24458 }
24459
24460 if (
24461 $this->is_addon() &&
24462 ! $this->is_only_premium()
24463 ) {
24464 $parent = $this->get_parent_instance();
24465
24466 if ( is_object( $parent ) && $parent->is_anonymous() ) {
24467 return;
24468 }
24469 }
24470
24471 if ( fs_is_network_admin() ) {
24472 if ( ! $this->_is_network_active ) {
24473 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
24474 return;
24475 } else if ( $this->is_network_delegated_connection() ) {
24476 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
24477 return;
24478 }
24479 } else {
24480 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
24481 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
24482 return;
24483 }
24484 }
24485
24486 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
24487 if ( ! $this->is_registered() && $this->is_anonymous() ) {
24488 $this->connect_again();
24489
24490 return;
24491 }
24492 }
24493
24494 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
24495 ( $this->is_theme() && ! self::is_themes_page() )
24496 ) {
24497 // Only show tracking links on the plugins and themes pages.
24498 return;
24499 }
24500
24501 if (
24502 $this->is_activation_mode() &&
24503 $this->is_premium() &&
24504 ! $this->is_registered()
24505 ) {
24506 // If not yet registered and running the premium code base, a license activation link will already be shown.
24507 return;
24508 }
24509
24510 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24511 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
24512 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
24513 return;
24514 }
24515 }
24516
24517 if ( $this->add_ajax_action( 'toggle_permission_tracking', array( &$this, '_toggle_permission_tracking_callback' ) ) ) {
24518 return;
24519 }
24520
24521 $link_text_id = '';
24522 $url = '#';
24523
24524 if ( $this->is_registered( true ) ) {
24525 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24526 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
24527 } else {
24528 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24529 }
24530 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
24531 /**
24532 * Show opt-in link only if skipped or in activation mode.
24533 */
24534 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24535
24536 $params = ! $this->is_anonymous() ?
24537 array() :
24538 array(
24539 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
24540 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
24541 );
24542
24543 $url = $this->get_activation_url( $params );
24544 }
24545
24546 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
24547
24548 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
24549 $this->add_plugin_action_link(
24550 $link_text_id,
24551 $url,
24552 false,
24553 13,
24554 "opt-in-or-opt-out {$this->_slug}"
24555 );
24556 }
24557 }
24558
24559 /**
24560 * Get the URL of the page that should be loaded right after the plugin activation.
24561 *
24562 * @author Vova Feldman (@svovaf)
24563 * @since 1.1.7.4
24564 *
24565 * @return string
24566 */
24567 function get_after_plugin_activation_redirect_url() {
24568 $url = false;
24569
24570 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
24571 $first_time_path = $this->_menu->get_first_time_path(
24572 fs_is_network_admin() && $this->_is_network_active
24573 );
24574
24575 if ( $this->is_activation_mode() ) {
24576 $url = $this->get_activation_url();
24577 } else if ( ! empty( $first_time_path ) ) {
24578 $url = $first_time_path;
24579 } else {
24580 $page = '';
24581 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
24582 if ( $this->is_network_registered() ) {
24583 $page = 'account';
24584 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
24585 $this->maybe_set_slug_and_network_menu_exists_flag();
24586 }
24587 }
24588
24589 $url = $this->_get_admin_page_url( $page );
24590 }
24591 } else {
24592 $plugin_fs = false;
24593
24594 if ( $this->is_parent_plugin_installed() ) {
24595 $plugin_fs = self::get_parent_instance();
24596 }
24597
24598 if ( is_object( $plugin_fs ) ) {
24599 if ( ! $plugin_fs->is_registered() ) {
24600 // Forward to parent plugin connect when parent not registered.
24601 $url = $plugin_fs->get_activation_url();
24602 } else {
24603 // Forward to account page.
24604 $url = $plugin_fs->_get_admin_page_url( 'account' );
24605 }
24606 }
24607 }
24608
24609 return $url;
24610 }
24611
24612 /**
24613 * Forward page to activation page.
24614 *
24615 * @author Vova Feldman (@svovaf)
24616 * @since 1.0.3
24617 */
24618 function _redirect_on_activation_hook() {
24619 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
24620 $url = $this->get_after_plugin_activation_redirect_url();
24621
24622 if ( is_string( $url ) ) {
24623 fs_redirect( $url );
24624 }
24625 }
24626 }
24627
24628 /**
24629 * Modify plugin's page action links collection.
24630 *
24631 * @author Vova Feldman (@svovaf)
24632 * @since 1.0.0
24633 *
24634 * @param array $links
24635 * @param $file
24636 *
24637 * @return array
24638 */
24639 function _modify_plugin_action_links_hook( $links, $file ) {
24640 $this->_logger->entrance();
24641
24642 $passed_deactivate = false;
24643 $deactivate_link = '';
24644 $before_deactivate = array();
24645 $after_deactivate = array();
24646 foreach ( $links as $key => $link ) {
24647 if ( 'deactivate' === $key ) {
24648 $deactivate_link = $link;
24649 $passed_deactivate = true;
24650 continue;
24651 }
24652
24653 if ( ! $passed_deactivate ) {
24654 $before_deactivate[ $key ] = $link;
24655 } else {
24656 $after_deactivate[ $key ] = $link;
24657 }
24658 }
24659
24660 ksort( $this->_action_links );
24661
24662 foreach ( $this->_action_links as $new_links ) {
24663 foreach ( $new_links as $link ) {
24664 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
24665 }
24666 }
24667
24668 if ( ! empty( $deactivate_link ) ) {
24669 /**
24670 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
24671 *
24672 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
24673 */
24674 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
24675
24676 // Append deactivation link.
24677 $before_deactivate['deactivate'] = $deactivate_link;
24678 }
24679
24680 return array_merge( $before_deactivate, $after_deactivate );
24681 }
24682
24683 /**
24684 * Adds admin message.
24685 *
24686 * @author Vova Feldman (@svovaf)
24687 * @since 1.0.4
24688 *
24689 * @param string $message
24690 * @param string $title
24691 * @param string $type
24692 */
24693 function add_admin_message( $message, $title = '', $type = 'success' ) {
24694 $this->_admin_notices->add( $message, $title, $type );
24695 }
24696
24697 /**
24698 * Adds sticky admin message.
24699 *
24700 * @author Vova Feldman (@svovaf)
24701 * @since 1.1.0
24702 *
24703 * @param string $message
24704 * @param string $id
24705 * @param string $title
24706 * @param string $type
24707 */
24708 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
24709 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
24710 }
24711
24712 /**
24713 * Check if the paid version of the module is installed.
24714 *
24715 * @author Vova Feldman (@svovaf)
24716 * @since 2.2.0
24717 *
24718 * @return bool
24719 */
24720 private function is_premium_version_installed() {
24721 $premium_plugin_basename = $this->premium_plugin_basename();
24722
24723 if ( $this->is_theme() ) {
24724 return $this->can_activate_theme( $this->get_premium_slug() );
24725 }
24726
24727 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_plugin_basename ) );
24728 }
24729
24730 /**
24731 * Helper function that returns the final steps for the upgrade completion.
24732 *
24733 * If the module is already running the premium code, returns an empty string.
24734 *
24735 * @author Vova Feldman (@svovaf)
24736 * @since 1.2.1
24737 *
24738 * @param string $plan_title
24739 *
24740 * @return string
24741 */
24742 private function get_complete_upgrade_instructions( $plan_title = '' ) {
24743 $this->_logger->entrance();
24744
24745 $activate_license_string = $this->get_license_network_activation_notice();
24746
24747 if ( ! $this->has_premium_version() || $this->is_premium() ) {
24748 return '' . $activate_license_string;
24749 }
24750
24751 if ( empty( $plan_title ) ) {
24752 $plan_title = $this->get_plan_title();
24753 }
24754
24755 if ( $this->is_premium_version_installed() ) {
24756 /**
24757 * If the premium version is already installed, instead of showing the installation instructions,
24758 * tell the current user to activate it.
24759 *
24760 * @author Leo Fajardo (@leorw)
24761 * @since 2.2.1
24762 */
24763 $premium_theme_slug_or_plugin_basename = $this->is_theme() ?
24764 $this->get_premium_slug() :
24765 $this->premium_plugin_basename();
24766
24767 return sprintf(
24768 /* translators: %1$s: Product title; %2$s: Plan title */
24769 $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' ),
24770 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24771 $plan_title,
24772 sprintf(
24773 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
24774 ( $this->is_theme() ?
24775 wp_nonce_url( 'themes.php?action=activate&amp;stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) :
24776 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ),
24777 esc_html( sprintf(
24778 /* translators: %s: Plan title */
24779 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
24780 $plan_title
24781 ) )
24782 )
24783 );
24784 } else {
24785 // @since 1.2.1.5 The free version is auto deactivated.
24786 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
24787 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
24788 '';
24789
24790 return sprintf(
24791 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
24792 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
24793 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
24794 $this->get_latest_download_link( sprintf(
24795 /* translators: %s: Plan title */
24796 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
24797 $plan_title
24798 ) ),
24799 $deactivation_step,
24800 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
24801 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/wp-' . $this->_module_type . '-upload' ),
24802 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
24803 );
24804 }
24805 }
24806
24807 /**
24808 * @author Leo Fajardo (@leorw)
24809 * @since 2.5.3
24810 *
24811 * @param string $message_before_the_instructions
24812 * @param string $message_id
24813 * @param string $plan_title
24814 */
24815 private function add_complete_upgrade_instructions_notice(
24816 $message_before_the_instructions,
24817 $message_id,
24818 $plan_title = ''
24819 ) {
24820 $this->_admin_notices->add_sticky(
24821 $message_before_the_instructions .
24822 $this->get_complete_upgrade_instructions( $plan_title ),
24823 $message_id,
24824 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
24825 );
24826 }
24827
24828 /**
24829 * @author Leo Fajardo (@leorw)
24830 * @since 2.5.3
24831 *
24832 * @param bool $is_upgrade
24833 */
24834 private function add_after_plan_activation_or_upgrade_instructions_notice( $is_upgrade = true ) {
24835 $this->add_complete_upgrade_instructions_notice(
24836 $is_upgrade ?
24837 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) :
24838 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ),
24839 'plan_upgraded'
24840 );
24841 }
24842
24843 /**
24844 * @author Leo Fajardo (@leorw)
24845 * @since 2.1.0
24846 *
24847 * @param string $url
24848 * @param array $request
24849 */
24850 private static function enrich_request_for_debug( &$url, &$request ) {
24851 if ( WP_FS__DEBUG_SDK || isset( $_COOKIE['XDEBUG_SESSION'] ) ) {
24852 $url = add_query_arg( 'XDEBUG_SESSION_START', rand( 0, 9999999 ), $url );
24853 $url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url );
24854
24855 $request['cookies'] = array(
24856 new WP_Http_Cookie( array(
24857 'name' => 'XDEBUG_SESSION',
24858 'value' => 'PHPSTORM',
24859 ) )
24860 );
24861 }
24862 }
24863
24864 /**
24865 * @author Leo Fajardo (@leorw)
24866 * @since 2.1.0
24867 *
24868 * @param string $url
24869 * @param array $request
24870 * @param int $success_cache_expiration
24871 * @param int $failure_cache_expiration
24872 * @param bool $maybe_enrich_request_for_debug
24873 *
24874 * @return WP_Error|array
24875 */
24876 static function safe_remote_post(
24877 &$url,
24878 $request,
24879 $success_cache_expiration = 0,
24880 $failure_cache_expiration = 0,
24881 $maybe_enrich_request_for_debug = true
24882 ) {
24883 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
24884
24885 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
24886
24887 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
24888 get_transient( $cache_key ) :
24889 false;
24890
24891 if ( false === $response ) {
24892 if ( $maybe_enrich_request_for_debug ) {
24893 self::enrich_request_for_debug( $url, $request );
24894 }
24895
24896 if ( ! isset( $request['method'] ) ) {
24897 $request['method'] = 'POST';
24898 }
24899
24900 $response = FS_Api::remote_request( $url, $request );
24901
24902 if (
24903 'https://' === substr( $url, 0, 8 ) &&
24904 FS_Api::is_ssl_error_response( $response )
24905 ) {
24906 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
24907 $url = 'http://' . substr( $url, 8 );
24908
24909 $request['timeout'] = 15;
24910 $response = FS_Api::remote_request( $url, $request );
24911 }
24912
24913 if ( false !== $cache_key ) {
24914 set_transient(
24915 $cache_key,
24916 $response,
24917 ( ( $response instanceof WP_Error ) ?
24918 $failure_cache_expiration :
24919 $success_cache_expiration )
24920 );
24921 }
24922 }
24923
24924 return $response;
24925 }
24926
24927 /**
24928 * This method is used to enrich the after upgrade notice instructions when the upgraded
24929 * license cannot be activated network wide (license quota isn't large enough).
24930 *
24931 * @author Vova Feldman (@svovaf)
24932 * @since 2.0.0
24933 *
24934 * @return string
24935 */
24936 private function get_license_network_activation_notice() {
24937 if ( ! $this->_is_network_active ) {
24938 // Module isn't network level activated.
24939 return '';
24940 }
24941
24942 if ( ! fs_is_network_admin() ) {
24943 // Not network level admin.
24944 return '';
24945 }
24946
24947 if ( get_blog_count() == 1 ) {
24948 // There's only a single site in the network so if there's a context license it was already activated.
24949 return '';
24950 }
24951
24952 if ( ! is_object( $this->_license ) ) {
24953 // No context license.
24954 return '';
24955 }
24956
24957 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
24958 // 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).
24959 return '';
24960 }
24961
24962 if ( $this->can_activate_license_on_network( $this->_license ) ) {
24963 // 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).
24964 return '';
24965 }
24966
24967 return sprintf(
24968 $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' ),
24969 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
24970 '</a>'
24971 );
24972 }
24973
24974 /**
24975 * @author Vova Feldman (@svovaf)
24976 * @since 1.2.1.7
24977 *
24978 * @param string $key
24979 *
24980 * @return string
24981 */
24982 function get_text( $key ) {
24983 return fs_text( $key, $this->_slug );
24984 }
24985
24986 /**
24987 * @author Vova Feldman (@svovaf)
24988 * @since 1.2.3
24989 *
24990 * @param string $text Translatable string.
24991 * @param string $key String key for overrides.
24992 *
24993 * @return string
24994 */
24995 function get_text_inline( $text, $key = '' ) {
24996 return _fs_text_inline( $text, $key, $this->_slug );
24997 }
24998
24999 /**
25000 * @author Vova Feldman (@svovaf)
25001 * @since 1.2.3
25002 *
25003 * @param string $text Translatable string.
25004 * @param string $context Context information for the translators.
25005 * @param string $key String key for overrides.
25006 *
25007 * @return string
25008 */
25009 function get_text_x_inline( $text, $context, $key ) {
25010 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
25011 }
25012
25013 /**
25014 * @author Vova Feldman (@svovaf)
25015 * @since 1.2.3
25016 *
25017 * @param string $text Translatable string.
25018 * @param string $key String key for overrides.
25019 *
25020 * @return string
25021 */
25022 function esc_html_inline( $text, $key ) {
25023 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
25024 }
25025
25026 #----------------------------------------------------------------------------------
25027 #region Versioning
25028 #----------------------------------------------------------------------------------
25029
25030 /**
25031 * Check if Freemius in SDK upgrade mode.
25032 *
25033 * @author Vova Feldman (@svovaf)
25034 * @since 1.0.9
25035 *
25036 * @return bool
25037 */
25038 function is_sdk_upgrade_mode() {
25039 return isset( $this->_storage->sdk_upgrade_mode ) ?
25040 $this->_storage->sdk_upgrade_mode :
25041 false;
25042 }
25043
25044 /**
25045 * Turn SDK upgrade mode off.
25046 *
25047 * @author Vova Feldman (@svovaf)
25048 * @since 1.0.9
25049 */
25050 function set_sdk_upgrade_complete() {
25051 $this->_storage->sdk_upgrade_mode = false;
25052 }
25053
25054 /**
25055 * Check if plugin upgrade mode.
25056 *
25057 * @author Vova Feldman (@svovaf)
25058 * @since 1.0.9
25059 *
25060 * @return bool
25061 */
25062 function is_plugin_upgrade_mode() {
25063 return isset( $this->_storage->plugin_upgrade_mode ) ?
25064 $this->_storage->plugin_upgrade_mode :
25065 false;
25066 }
25067
25068 /**
25069 * Turn plugin upgrade mode off.
25070 *
25071 * @author Vova Feldman (@svovaf)
25072 * @since 1.0.9
25073 */
25074 function set_plugin_upgrade_complete() {
25075 $this->_storage->plugin_upgrade_mode = false;
25076
25077 $license_migration = ! empty( $this->_storage->license_migration ) ?
25078 $this->_storage->license_migration :
25079 array();
25080
25081 $license_migration['is_migrating'] = false;
25082
25083 $this->_storage->license_migration = $license_migration;
25084 }
25085
25086 #endregion
25087
25088 #----------------------------------------------------------------------------------
25089 #region Permissions
25090 #----------------------------------------------------------------------------------
25091
25092 /**
25093 * Check if specific permission requested.
25094 *
25095 * @author Vova Feldman (@svovaf)
25096 * @since 1.1.6
25097 *
25098 * @param string $permission
25099 *
25100 * @return bool
25101 */
25102 function is_permission_requested( $permission ) {
25103 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
25104 }
25105
25106 #endregion
25107
25108 #----------------------------------------------------------------------------------
25109 #region Auto Activation
25110 #----------------------------------------------------------------------------------
25111
25112 /**
25113 * Hints the SDK if running an auto-installation.
25114 *
25115 * @var bool
25116 */
25117 private $_isAutoInstall = false;
25118
25119 /**
25120 * After upgrade callback to install and auto activate a plugin.
25121 * This code will only be executed on explicit request from the user,
25122 * following the practice Jetpack are using with their theme installations.
25123 *
25124 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
25125 *
25126 * @author Vova Feldman (@svovaf)
25127 * @since 1.2.1.7
25128 */
25129 function _install_premium_version_ajax_action() {
25130 $this->_logger->entrance();
25131
25132 $this->check_ajax_referer( 'install_premium_version' );
25133
25134 if ( ! $this->is_registered() ) {
25135 // Not registered.
25136 self::shoot_ajax_failure( array(
25137 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
25138 'code' => 'premium_installed',
25139 ) );
25140 }
25141
25142 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
25143
25144 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25145 // Invalid ID.
25146 self::shoot_ajax_failure( array(
25147 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25148 'code' => 'invalid_module_id',
25149 ) );
25150 }
25151
25152 if ( $plugin_id == $this->get_id() ) {
25153 if ( $this->is_premium() ) {
25154 // Already using the premium code version.
25155 self::shoot_ajax_failure( array(
25156 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
25157 'code' => 'premium_installed',
25158 ) );
25159 }
25160 if ( ! $this->can_use_premium_code() ) {
25161 // Don't have access to the premium code.
25162 self::shoot_ajax_failure( array(
25163 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
25164 'code' => 'invalid_license',
25165 ) );
25166 }
25167 if ( ! $this->has_release_on_freemius() ) {
25168 // Plugin is a serviceware, no premium code version.
25169 self::shoot_ajax_failure( array(
25170 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
25171 'code' => 'premium_version_missing',
25172 ) );
25173 }
25174 } else {
25175 $addon = $this->get_addon( $plugin_id );
25176
25177 if ( ! is_object( $addon ) ) {
25178 // Invalid add-on ID.
25179 self::shoot_ajax_failure( array(
25180 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25181 'code' => 'invalid_module_id',
25182 ) );
25183 }
25184
25185 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25186 // Premium add-on version is already activated.
25187 self::shoot_ajax_failure( array(
25188 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
25189 'code' => 'premium_installed',
25190 ) );
25191 }
25192 }
25193
25194 $this->_isAutoInstall = true;
25195
25196 // Try to install and activate.
25197 $updater = FS_Plugin_Updater::instance( $this );
25198 $result = $updater->install_and_activate_plugin( $plugin_id );
25199
25200 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
25201 self::shoot_ajax_failure( array(
25202 'message' => $result['message'],
25203 'code' => $result['code'],
25204 ) );
25205 }
25206
25207 self::shoot_ajax_success( $result );
25208 }
25209
25210 /**
25211 * Displays module activation dialog box after a successful upgrade
25212 * where the user explicitly requested to auto download and install
25213 * the premium version.
25214 *
25215 * @author Vova Feldman (@svovaf)
25216 * @since 1.2.1.7
25217 */
25218 function _add_auto_installation_dialog_box() {
25219 $this->_logger->entrance();
25220
25221 if ( ! $this->is_registered() ) {
25222 // Not registered.
25223 return;
25224 }
25225
25226 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
25227
25228 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25229 // Invalid module ID.
25230 return;
25231 }
25232
25233 if ( $plugin_id == $this->get_id() ) {
25234 if ( $this->is_premium() ) {
25235 // Already using the premium code version.
25236 return;
25237 }
25238 if ( ! $this->can_use_premium_code() ) {
25239 // Don't have access to the premium code.
25240 return;
25241 }
25242 if ( ! $this->has_release_on_freemius() ) {
25243 // Plugin is a serviceware, no premium code version.
25244 return;
25245 }
25246 } else {
25247 $addon = $this->get_addon( $plugin_id );
25248
25249 if ( ! is_object( $addon ) ) {
25250 // Invalid add-on ID.
25251 return;
25252 }
25253
25254 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25255 // Premium add-on version is already activated.
25256 return;
25257 }
25258 }
25259
25260 $vars = array(
25261 'id' => $this->_module_id,
25262 'target_module_id' => $plugin_id,
25263 'slug' => $this->_slug,
25264 );
25265
25266 fs_require_template( 'auto-installation.php', $vars );
25267 }
25268
25269 #endregion
25270
25271 #--------------------------------------------------------------------------------
25272 #region Tabs Integration
25273 #--------------------------------------------------------------------------------
25274
25275 #region Module's Original Tabs
25276
25277 /**
25278 * Inject a JavaScript logic to capture the theme tabs HTML.
25279 *
25280 * @author Vova Feldman (@svovaf)
25281 * @since 1.2.2.7
25282 */
25283 function _tabs_capture() {
25284 $this->_logger->entrance();
25285
25286 if (
25287 ! $this->is_product_settings_page() ||
25288 ! $this->should_page_include_tabs() ||
25289 ! $this->is_matching_url( $this->main_menu_url() )
25290 ) {
25291 return;
25292 }
25293
25294 $params = array(
25295 'id' => $this->_module_id,
25296 );
25297
25298 fs_require_once_template( 'tabs-capture-js.php', $params );
25299 }
25300
25301 /**
25302 * Cache theme's tabs HTML for a week. The cache will also be set as expired
25303 * after version and type (free/premium) changes, in addition to the week period.
25304 *
25305 * @author Vova Feldman (@svovaf)
25306 * @since 1.2.2.7
25307 */
25308 function _store_tabs_ajax_action() {
25309 $this->_logger->entrance();
25310
25311 $this->check_ajax_referer( 'store_tabs' );
25312
25313 // Init filesystem if not yet initiated.
25314 WP_Filesystem();
25315
25316 // Get POST body HTML data.
25317 global $wp_filesystem;
25318 $tabs_html = $wp_filesystem->get_contents( "php://input" );
25319
25320 if ( is_string( $tabs_html ) ) {
25321 $tabs_html = trim( $tabs_html );
25322 }
25323
25324 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
25325 self::shoot_ajax_failure();
25326 }
25327
25328 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25329
25330 self::shoot_ajax_success();
25331 }
25332
25333 /**
25334 * Cache theme's settings page custom styles. The cache will also be set as expired
25335 * after version and type (free/premium) changes, in addition to the week period.
25336 *
25337 * @author Vova Feldman (@svovaf)
25338 * @since 1.2.2.7
25339 */
25340 function _store_tabs_styles() {
25341 $this->_logger->entrance();
25342
25343 if (
25344 ! $this->is_product_settings_page() ||
25345 ! $this->should_page_include_tabs() ||
25346 ! $this->is_matching_url( $this->main_menu_url() )
25347 ) {
25348 return;
25349 }
25350
25351 $wp_styles = wp_styles();
25352
25353 $theme_styles_url = get_template_directory_uri();
25354
25355 $stylesheets = array();
25356 foreach ( $wp_styles->queue as $handler ) {
25357 if ( fs_starts_with( $handler, 'fs_' ) ) {
25358 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
25359 continue;
25360 }
25361
25362 /**
25363 * @var _WP_Dependency $stylesheet
25364 */
25365 $stylesheet = $wp_styles->registered[ $handler ];
25366
25367 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
25368 $stylesheets[] = $stylesheet->src;
25369 }
25370 }
25371
25372 if ( ! empty( $stylesheets ) ) {
25373 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25374 }
25375 }
25376
25377 /**
25378 * Check if module's original settings page has any tabs.
25379 *
25380 * @author Vova Feldman (@svovaf)
25381 * @since 1.2.2.7
25382 *
25383 * @return bool
25384 */
25385 private function has_tabs() {
25386 return $this->_cache->has( 'tabs' );
25387 }
25388
25389 /**
25390 * Get module's settings page HTML content, starting
25391 * from the beginning of the <div class="wrap"> element,
25392 * until the tabs HTML (including).
25393 *
25394 * @author Vova Feldman (@svovaf)
25395 * @since 1.2.2.7
25396 *
25397 * @return string
25398 */
25399 private function get_tabs_html() {
25400 $this->_logger->entrance();
25401
25402 return $this->_cache->get( 'tabs' );
25403 }
25404
25405 /**
25406 * Check if page should include tabs.
25407 *
25408 * @author Vova Feldman (@svovaf)
25409 * @since 1.2.2.7
25410 *
25411 * @return bool
25412 */
25413 private function should_page_include_tabs() {
25414 if ( ! $this->has_settings_menu() ) {
25415 // Don't add tabs if no settings at all.
25416 return false;
25417 }
25418
25419 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
25420 // Only add tabs to themes for now.
25421 return false;
25422 }
25423
25424 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
25425 // Only add tabs to monetizing themes.
25426 return false;
25427 }
25428
25429 if ( ! $this->is_product_settings_page() ) {
25430 // Only add tabs if browsing one of the product's setting pages.
25431 return false;
25432 }
25433
25434 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
25435 // Don't include tabs in the activation page.
25436 return false;
25437 }
25438
25439 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
25440 // Don't add tabs on checkout page, we want to reduce distractions
25441 // as much as possible.
25442 return false;
25443 }
25444
25445 return true;
25446 }
25447
25448 /**
25449 * Add the tabs HTML before the setting's page content and
25450 * enqueue any required stylesheets.
25451 *
25452 * @author Vova Feldman (@svovaf)
25453 * @since 1.2.2.7
25454 *
25455 * @return bool If tabs were included.
25456 */
25457 function _add_tabs_before_content() {
25458 $this->_logger->entrance();
25459
25460 if ( ! $this->should_page_include_tabs() ) {
25461 return false;
25462 }
25463
25464 $tabs_html = $this->get_tabs_html();
25465
25466 if ( empty( $tabs_html ) ) {
25467 return false;
25468 }
25469
25470 /**
25471 * Enqueue the original stylesheets that are included in the
25472 * theme settings page. That way, if the theme settings has
25473 * some custom _styled_ content above the tabs UI, this
25474 * will make sure that the styling is preserved.
25475 */
25476 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
25477 if ( is_array( $stylesheets ) ) {
25478 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
25479 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
25480 }
25481 }
25482
25483 // Cut closing </div> tag.
25484 echo substr( trim( $tabs_html ), 0, - 6 );
25485
25486 return true;
25487 }
25488
25489 /**
25490 * Add the tabs closing HTML after the setting's page content.
25491 *
25492 * @author Vova Feldman (@svovaf)
25493 * @since 1.2.2.7
25494 *
25495 * @return bool If tabs closing HTML was included.
25496 */
25497 function _add_tabs_after_content() {
25498 $this->_logger->entrance();
25499
25500 if ( ! $this->should_page_include_tabs() ) {
25501 return false;
25502 }
25503
25504 echo '</div>';
25505
25506 return true;
25507 }
25508
25509 #endregion
25510
25511 /**
25512 * Add in-page JavaScript to inject the Freemius tabs into
25513 * the module's setting tabs section.
25514 *
25515 * @author Vova Feldman (@svovaf)
25516 * @since 1.2.2.7
25517 */
25518 function _add_freemius_tabs() {
25519 $this->_logger->entrance();
25520
25521 if ( ! $this->should_page_include_tabs() ) {
25522 return;
25523 }
25524
25525 $params = array( 'id' => $this->_module_id );
25526 fs_require_once_template( 'tabs.php', $params );
25527 }
25528
25529 #endregion
25530
25531 #--------------------------------------------------------------------------------
25532 #region Customizer Integration for Themes
25533 #--------------------------------------------------------------------------------
25534
25535 /**
25536 * @author Vova Feldman (@svovaf)
25537 * @since 1.2.2.7
25538 *
25539 * @param WP_Customize_Manager $customizer
25540 */
25541 function _customizer_register( $customizer ) {
25542 $this->_logger->entrance();
25543
25544 if ( $this->is_pricing_page_visible() ) {
25545 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
25546
25547 $customizer->add_section( 'freemius_upsell', array(
25548 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
25549 'priority' => 1,
25550 ) );
25551 $customizer->add_setting( 'freemius_upsell', array(
25552 'sanitize_callback' => 'esc_html',
25553 ) );
25554
25555 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
25556 'fs' => $this,
25557 'section' => 'freemius_upsell',
25558 'priority' => 100,
25559 ) ) );
25560 }
25561
25562 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
25563 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
25564
25565 // Main Documentation Link In Customizer Root.
25566 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
25567 'fs' => $this,
25568 'priority' => 1000,
25569 ) ) );
25570 }
25571 }
25572
25573 #endregion
25574
25575 /**
25576 * If the theme has a paid version, add some custom
25577 * styling to the theme's premium version (if exists)
25578 * to highlight that it's the premium version of the
25579 * same theme, making it easier for identification
25580 * after the user upgrades and upload it to the site.
25581 *
25582 * @author Vova Feldman (@svovaf)
25583 * @since 1.2.2.7
25584 */
25585 function _style_premium_theme() {
25586 $this->_logger->entrance();
25587
25588 if ( ! self::is_themes_page() ) {
25589 // Only include in the themes page.
25590 return;
25591 }
25592
25593 if ( ! $this->has_paid_plan() ) {
25594 // Only include if has any paid plans.
25595 return;
25596 }
25597
25598 $params = null;
25599 fs_require_once_template( '/js/jquery.content-change.php', $params );
25600
25601 $params = array(
25602 'slug' => $this->_slug,
25603 'id' => $this->_module_id,
25604 );
25605
25606 fs_require_template( '/js/style-premium-theme.php', $params );
25607 }
25608
25609 /**
25610 * This method will return the absolute URL of the module's local icon.
25611 *
25612 * When you are running your plugin or theme on a **localhost** environment, if the icon
25613 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
25614 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
25615 * If an icon is found, this method will automatically attempt to download the icon and store it
25616 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
25617 *
25618 * It's important to mention that this method is NOT phoning home since the developer will deploy
25619 * the product with the local icon in the assets folder. The download process just simplifies
25620 * the process for the developer.
25621 *
25622 * @author Vova Feldman (@svovaf)
25623 * @since 2.0.0
25624 *
25625 * @return string
25626 */
25627 function get_local_icon_url() {
25628 global $fs_active_plugins;
25629
25630 /**
25631 * @since 1.1.7.5
25632 */
25633 $local_path = $this->apply_filters( 'plugin_icon', false );
25634
25635 if ( is_string( $local_path ) ) {
25636 $icons = array( $local_path );
25637 } else {
25638 $img_dir = WP_FS__DIR_IMG;
25639
25640 // Locate the main assets folder.
25641 if ( 1 < count( $fs_active_plugins->plugins ) ) {
25642 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
25643
25644 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
25645 if ( $data->plugin_path == $this->get_plugin_basename() ) {
25646 $img_dir = $plugin_or_theme_img_dir
25647 . '/'
25648 /**
25649 * The basename will be `themes` or the basename of a custom themes directory.
25650 *
25651 * @author Leo Fajardo (@leorw)
25652 * @since 2.2.3
25653 */
25654 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
25655 . '/assets/img';
25656
25657 break;
25658 }
25659 }
25660 }
25661
25662 // Try to locate the icon in the assets folder.
25663 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
25664
25665 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
25666 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
25667 $icons = array(
25668 fs_normalize_path( $img_dir . '/theme-icon.png' )
25669 );
25670 } else {
25671 $icon_found = false;
25672 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
25673
25674 if ( ! function_exists( 'get_filesystem_method' ) ) {
25675 require_once ABSPATH . 'wp-admin/includes/file.php';
25676 }
25677
25678 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
25679
25680 /**
25681 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
25682 *
25683 * This code will only be executed once during the testing
25684 * of the plugin in a local environment. The plugin icon file WILL
25685 * already exist in the assets folder when the plugin is deployed to
25686 * the repository.
25687 */
25688 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
25689 // Fetch icon from Freemius.
25690 $icon = $this->fetch_remote_icon_url();
25691
25692 // Fetch icon from WordPress.org.
25693 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
25694 if ( ! function_exists( 'plugins_api' ) ) {
25695 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
25696 }
25697
25698 $plugin_information = plugins_api( 'plugin_information', array(
25699 'slug' => $this->_slug,
25700 'fields' => array(
25701 'sections' => false,
25702 'tags' => false,
25703 'icons' => true
25704 )
25705 ) );
25706
25707 if (
25708 ! is_wp_error( $plugin_information )
25709 && isset( $plugin_information->icons )
25710 && ! empty( $plugin_information->icons )
25711 ) {
25712 /**
25713 * Get the smallest icon.
25714 *
25715 * @author Leo Fajardo (@leorw)
25716 * @since 1.2.2
25717 */
25718 $icon = end( $plugin_information->icons );
25719 }
25720 }
25721
25722 if ( ! empty( $icon ) ) {
25723 if ( 0 !== strpos( $icon, 'http' ) ) {
25724 $icon = 'http:' . $icon;
25725 }
25726
25727 /**
25728 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
25729 *
25730 * @author Leo Fajardo (@leorw)
25731 * @since 1.2.2
25732 */
25733 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
25734
25735 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
25736
25737 // Try to download the icon.
25738 $icon_found = fs_download_image( $icon, $local_path );
25739 }
25740 }
25741
25742 if ( ! $icon_found ) {
25743 // No icons found, fallback to default icon.
25744 if ( $have_write_permissions ) {
25745 // If have write permissions, copy default icon.
25746 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
25747 } else {
25748 // If doesn't have write permissions, use default icon path.
25749 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
25750 }
25751 }
25752
25753 $icons = array( $local_path );
25754 }
25755 }
25756 }
25757
25758 $icon_dir = dirname( $icons[0] );
25759
25760 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
25761 }
25762
25763 /**
25764 * Fetch module's extended info.
25765 *
25766 * @author Vova Feldman (@svovaf)
25767 * @since 2.0.0
25768 *
25769 * @return object|mixed
25770 */
25771 private function fetch_module_info() {
25772 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
25773 }
25774
25775 /**
25776 * Fetch module's remote icon URL.
25777 *
25778 * @author Vova Feldman (@svovaf)
25779 * @since 2.0.0
25780 *
25781 * @return string
25782 */
25783 function fetch_remote_icon_url() {
25784 $info = $this->fetch_module_info();
25785
25786 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
25787 $info->icon :
25788 '';
25789 }
25790
25791 #--------------------------------------------------------------------------------
25792 #region GDPR
25793 #--------------------------------------------------------------------------------
25794
25795 /**
25796 * @author Leo Fajardo (@leorw)
25797 * @since 2.1.0
25798 *
25799 * @param array $user_plugins
25800 *
25801 * @return string
25802 */
25803 private function get_gdpr_admin_notice_string( $user_plugins ) {
25804 $this->_logger->entrance();
25805
25806 $addons = self::get_all_addons();
25807
25808 foreach ( $user_plugins as $user_plugin ) {
25809 $has_addons = isset( $addons[ $user_plugin->id ] );
25810
25811 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
25812 if ( $this->_module_id == $user_plugin->id ) {
25813 $addons = $this->get_addons();
25814 $has_addons = ( ! empty( $addons ) );
25815 } else {
25816 $plugin_api = FS_Api::instance(
25817 $user_plugin->id,
25818 'plugin',
25819 $user_plugin->id,
25820 $user_plugin->public_key,
25821 ! $user_plugin->is_live,
25822 false,
25823 $this->get_sdk_version()
25824 );
25825
25826 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
25827
25828 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
25829 is_array( $addons_result->plugins ) &&
25830 ! empty( $addons_result->plugins )
25831 ) {
25832 $has_addons = true;
25833 }
25834 }
25835 }
25836
25837 $user_plugin->has_addons = $has_addons;
25838 }
25839
25840 $is_single_parent_product = ( 1 === count( $user_plugins ) );
25841
25842 $multiple_products_text = '';
25843
25844 if ( $is_single_parent_product ) {
25845 $single_parent_product = reset( $user_plugins );
25846
25847 $thank_you = sprintf(
25848 "<span data-plugin-id='%d'>%s</span>",
25849 $single_parent_product->id,
25850 sprintf(
25851 $single_parent_product->has_addons ?
25852 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
25853 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
25854 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
25855 )
25856 );
25857
25858 $already_opted_in = sprintf(
25859 $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' ),
25860 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
25861 );
25862 } else {
25863 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
25864 $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' );
25865
25866 $products_and_add_ons = '';
25867 foreach ( $user_plugins as $user_plugin ) {
25868 if ( ! empty( $products_and_add_ons ) ) {
25869 $products_and_add_ons .= ', ';
25870 }
25871
25872 if ( ! $user_plugin->has_addons ) {
25873 $products_and_add_ons .= sprintf(
25874 "<span data-plugin-id='%d'>%s</span>",
25875 $user_plugin->id,
25876 $user_plugin->title
25877 );
25878 } else {
25879 $products_and_add_ons .= sprintf(
25880 "<span data-plugin-id='%d'>%s</span>",
25881 $user_plugin->id,
25882 sprintf(
25883 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
25884 $user_plugin->title
25885 )
25886 );
25887 }
25888 }
25889
25890 $multiple_products_text = sprintf(
25891 "<small class='products'><strong>%s:</strong> %s</small>",
25892 $this->get_text_inline( 'Products', 'products' ),
25893 $products_and_add_ons
25894 );
25895 }
25896
25897 $actions = sprintf(
25898 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
25899 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
25900 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
25901 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
25902 sprintf(
25903 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
25904 '<span class="underlined">',
25905 '</span>'
25906 )
25907 );
25908
25909 return sprintf(
25910 '%s %s %s',
25911 $thank_you,
25912 $already_opted_in,
25913 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>' ) .
25914 '<br><br>' .
25915 '<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>' .
25916 $actions .
25917 ( $is_single_parent_product ? '' : $multiple_products_text )
25918 );
25919 }
25920
25921 /**
25922 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
25923 * plugins and themes they've opted in to.
25924 *
25925 * @author Leo Fajardo (@leorw)
25926 * @since 2.1.0
25927 *
25928 * @param string $user_email
25929 * @param string $license_key
25930 * @param array $plugin_ids
25931 * @param string|null $license_key
25932 *
25933 * @return array|false
25934 */
25935 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
25936 $request = array(
25937 'method' => 'POST',
25938 'body' => array(),
25939 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
25940 );
25941
25942 if ( is_string( $user_email ) ) {
25943 $request['body']['email'] = $user_email;
25944 } else {
25945 $request['body']['license_key'] = $license_key;
25946 }
25947
25948 $result = array();
25949
25950 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
25951 $total_plugin_ids = count( $plugin_ids );
25952
25953 $plugin_ids_count_per_request = 10;
25954 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
25955 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
25956
25957 $request['body']['plugin_ids'] = $plugin_ids_set;
25958
25959 $response = self::safe_remote_post(
25960 $url,
25961 $request,
25962 WP_FS__TIME_24_HOURS_IN_SEC,
25963 WP_FS__TIME_12_HOURS_IN_SEC
25964 );
25965
25966 if ( ! is_wp_error( $response ) ) {
25967 $decoded = is_string( $response['body'] ) ?
25968 json_decode( $response['body'] ) :
25969 null;
25970
25971 if (
25972 !is_object($decoded) ||
25973 !isset($decoded->success) ||
25974 true !== $decoded->success ||
25975 !isset( $decoded->data ) ||
25976 !is_array( $decoded->data )
25977 ) {
25978 return false;
25979 }
25980
25981 $result = array_merge( $result, $decoded->data );
25982 }
25983 }
25984
25985 return $result;
25986 }
25987
25988 /**
25989 * @author Leo Fajardo (@leorw)
25990 * @since 2.1.0
25991 */
25992 function _maybe_show_gdpr_admin_notice() {
25993 if ( ! $this->is_user_in_admin() ) {
25994 return;
25995 }
25996
25997 if ( ! $this->should_handle_gdpr_admin_notice() ) {
25998 return;
25999 }
26000
26001 if ( ! $this->is_user_admin() ) {
26002 return;
26003 }
26004
26005 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26006
26007 $lock = FS_User_Lock::instance();
26008
26009 /**
26010 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
26011 */
26012 if ( ! $lock->try_lock( 60 ) ) {
26013 return;
26014 }
26015
26016 /**
26017 * @var $current_wp_user WP_User
26018 */
26019 $current_wp_user = self::_get_current_wp_user();
26020
26021 /**
26022 * @var FS_User $current_fs_user
26023 */
26024 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
26025
26026 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
26027
26028 if ( ! is_object( $current_fs_user ) ) {
26029 // 10-year lock.
26030 $lock->lock( $ten_years_in_sec );
26031
26032 return;
26033 }
26034
26035 $gdpr = FS_GDPR_Manager::instance();
26036
26037 if ( $gdpr->is_opt_in_notice_shown() ) {
26038 // 30-day lock.
26039 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26040
26041 return;
26042 }
26043
26044 if ( ! $gdpr->should_show_opt_in_notice() ) {
26045 // 10-year lock.
26046 $lock->lock( $ten_years_in_sec );
26047
26048 return;
26049 }
26050
26051 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
26052 $was_notice_shown_before = ( false !== $last_time_notice_shown );
26053
26054 if ( $was_notice_shown_before &&
26055 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
26056 ) {
26057 // If the notice was shown before, show it again after 30 days from the last time it was shown.
26058 return;
26059 }
26060
26061 /**
26062 * Find all plugin IDs that were installed by the current admin.
26063 */
26064 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
26065
26066 if ( empty( $plugin_ids_map )) {
26067 $lock->lock( $ten_years_in_sec );
26068
26069 return;
26070 }
26071
26072 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26073 $current_fs_user->email,
26074 null,
26075 array_keys( $plugin_ids_map )
26076 );
26077
26078 if ( empty( $user_plugins ) ) {
26079 $lock->lock(
26080 is_array($user_plugins) ?
26081 $ten_years_in_sec :
26082 // Lock for 24-hours on errors.
26083 WP_FS__TIME_24_HOURS_IN_SEC
26084 );
26085
26086 return;
26087 }
26088
26089 $has_unset_marketing_optin = false;
26090
26091 foreach ( $user_plugins as $user_plugin ) {
26092 if ( true == $user_plugin->is_marketing_allowed ) {
26093 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
26094 }
26095
26096 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
26097 $has_unset_marketing_optin = true;
26098 }
26099 }
26100
26101 if ( empty( $plugin_ids_map ) ||
26102 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
26103 ) {
26104 $lock->lock( $ten_years_in_sec );
26105
26106 return;
26107 }
26108
26109 $modules = array_merge(
26110 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26111 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26112 );
26113
26114 foreach ( $modules as $module ) {
26115 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
26116 $plugin_ids_map[ $module->id ] = $module;
26117 }
26118 }
26119
26120 $plugin_title = null;
26121 if ( 1 === count( $plugin_ids_map ) ) {
26122 $module = reset( $plugin_ids_map );
26123 $plugin_title = $module->title;
26124 }
26125
26126 $gdpr->add_opt_in_sticky_notice(
26127 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
26128 $plugin_title
26129 );
26130
26131 $this->add_gdpr_optin_ajax_handler_and_style();
26132
26133 $gdpr->notice_was_just_shown();
26134
26135 // 30-day lock.
26136 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26137 }
26138
26139 /**
26140 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
26141 * marketing.
26142 *
26143 * @author Leo Fajardo (@leorw)
26144 * @since 2.1.0
26145 */
26146 private function disable_opt_in_notice_and_lock_user() {
26147 FS_GDPR_Manager::instance()->disable_opt_in_notice();
26148
26149 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26150
26151 // 10-year lock.
26152 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26153 }
26154
26155 /**
26156 * @author Leo Fajardo (@leorw)
26157 * @since 2.5.4
26158 */
26159 static function _add_api_connectivity_notice_handler_js() {
26160 fs_require_once_template( 'api-connectivity-message-js.php' );
26161 }
26162
26163 /**
26164 * @author Leo Fajardo (@leorw)
26165 * @since 2.1.0
26166 */
26167 function _add_gdpr_optin_js() {
26168 $vars = array( 'id' => $this->_module_id );
26169
26170 fs_require_once_template( 'gdpr-optin-js.php', $vars );
26171 }
26172
26173 /**
26174 * @author Leo Fajardo (@leorw)
26175 * @since 2.1.0
26176 */
26177 function enqueue_gdpr_optin_notice_style() {
26178 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
26179 }
26180
26181 /**
26182 * @author Leo Fajardo (@leorw)
26183 * @since 2.1.0
26184 */
26185 function _maybe_add_gdpr_optin_ajax_handler() {
26186 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
26187
26188 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
26189 $this->add_gdpr_optin_ajax_handler_and_style();
26190 }
26191 }
26192
26193 /**
26194 * @author Leo Fajardo (@leorw)
26195 * @since 2.1.0
26196 */
26197 function _fetch_is_marketing_required_flag_value_ajax_action() {
26198 $this->_logger->entrance();
26199
26200 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
26201
26202 $license_key = fs_request_get_raw( 'license_key' );
26203
26204 if ( empty($license_key) ) {
26205 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
26206 }
26207
26208 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26209 null,
26210 $license_key,
26211 array( $this->_module_id )
26212 );
26213
26214 if ( ! is_array( $user_plugins ) ||
26215 empty($user_plugins) ||
26216 !isset($user_plugins[0]->plugin_id) ||
26217 $user_plugins[0]->plugin_id != $this->_module_id
26218 ) {
26219 /**
26220 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
26221 *
26222 * @author Vova Feldman (@svovaf)
26223 */
26224 self::shoot_ajax_success( array(
26225 'is_marketing_allowed' => null,
26226 'license_owner_id' => null
26227 ) );
26228 }
26229
26230 self::shoot_ajax_success( array(
26231 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
26232 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
26233 ) );
26234 }
26235
26236 /**
26237 * @author Leo Fajardo (@leorw)
26238 * @since 2.3.2
26239 *
26240 * @param number[] $install_ids
26241 *
26242 * @return array {
26243 * An array of objects containing the installs' licenses owners data.
26244 *
26245 * @property number $id User ID.
26246 * @property string $email User email (can be masked email).
26247 * }
26248 */
26249 private function fetch_installs_licenses_owners_data( $install_ids ) {
26250 $this->_logger->entrance();
26251
26252 $response = $this->get_api_user_scope()->get(
26253 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
26254 );
26255
26256 $license_owners = array();
26257
26258 if ( $this->is_api_result_object( $response, 'owners' ) ) {
26259 $license_owners = $response->owners;
26260 }
26261
26262 return $license_owners;
26263 }
26264
26265 /**
26266 * @author Leo Fajardo (@leorw)
26267 * @since 2.1.0
26268 */
26269 private function add_gdpr_optin_ajax_handler_and_style() {
26270 // Add GDPR action AJAX callback.
26271 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
26272
26273 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
26274 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
26275 }
26276
26277 /**
26278 * @author Leo Fajardo (@leorw)
26279 * @since 2.1.0
26280 */
26281 function _gdpr_optin_ajax_action() {
26282 $this->_logger->entrance();
26283
26284 $this->check_ajax_referer( 'gdpr_optin_action' );
26285
26286 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
26287 self::shoot_ajax_failure();
26288 }
26289
26290 $current_wp_user = self::_get_current_wp_user();
26291
26292 $plugin_ids = fs_request_get( 'plugin_ids', array() );
26293 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
26294 self::shoot_ajax_failure();
26295 }
26296
26297 $modules = array_merge(
26298 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26299 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26300 );
26301
26302 foreach ( $modules as $key => $module ) {
26303 if ( ! in_array( $module->id, $plugin_ids ) ) {
26304 unset( $modules[ $key ] );
26305 }
26306 }
26307
26308 if ( empty( $modules ) ) {
26309 self::shoot_ajax_failure();
26310 }
26311
26312 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
26313
26314 foreach ( $modules as $module ) {
26315 $user_api->call( "?plugin_id={$module->id}", 'put', array(
26316 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
26317 ) );
26318 }
26319
26320 FS_GDPR_Manager::instance()->remove_opt_in_notice();
26321
26322 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26323
26324 // 10-year lock.
26325 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26326
26327 self::shoot_ajax_success();
26328 }
26329
26330 /**
26331 * 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.
26332 *
26333 * @author Vova Feldman (@svovaf)
26334 * @since 2.1.0
26335 *
26336 * @return bool
26337 */
26338 private function should_handle_gdpr_admin_notice() {
26339 return $this->apply_filters(
26340 'handle_gdpr_admin_notice',
26341 // Default to false.
26342 false
26343 );
26344 }
26345
26346 #endregion
26347
26348 #----------------------------------------------------------------------------------
26349 #region Marketing
26350 #----------------------------------------------------------------------------------
26351
26352 /**
26353 * Check if current user purchased any other plugins before.
26354 *
26355 * @author Vova Feldman (@svovaf)
26356 * @since 1.0.9
26357 *
26358 * @return bool
26359 */
26360 function has_purchased_before() {
26361 // TODO: Implement has_purchased_before() method.
26362 throw new Exception( 'not implemented' );
26363 }
26364
26365 /**
26366 * Check if current user classified as an agency.
26367 *
26368 * @author Vova Feldman (@svovaf)
26369 * @since 1.0.9
26370 *
26371 * @return bool
26372 */
26373 function is_agency() {
26374 // TODO: Implement is_agency() method.
26375 throw new Exception( 'not implemented' );
26376 }
26377
26378 /**
26379 * Check if current user classified as a developer.
26380 *
26381 * @author Vova Feldman (@svovaf)
26382 * @since 1.0.9
26383 *
26384 * @return bool
26385 */
26386 function is_developer() {
26387 // TODO: Implement is_developer() method.
26388 throw new Exception( 'not implemented' );
26389 }
26390
26391 /**
26392 * Check if current user classified as a business.
26393 *
26394 * @author Vova Feldman (@svovaf)
26395 * @since 1.0.9
26396 *
26397 * @return bool
26398 */
26399 function is_business() {
26400 // TODO: Implement is_business() method.
26401 throw new Exception( 'not implemented' );
26402 }
26403
26404 #endregion
26405
26406 #----------------------------------------------------------------------------------
26407 #region Helper
26408 #----------------------------------------------------------------------------------
26409
26410 /**
26411 * If running with a secret key, assume it's the developer and show pending plans as well.
26412 *
26413 * @author Vova Feldman (@svovaf)
26414 * @since 2.1.2
26415 *
26416 * @param string $path
26417 *
26418 * @return string
26419 */
26420 function add_show_pending( $path ) {
26421 if ( ! $this->has_secret_key() ) {
26422 return $path;
26423 }
26424
26425 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
26426 }
26427
26428 #endregion
26429 }
26430