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

class-freemius.php in BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites 3.1.1, at freemius/includes/class-freemius.php

26,429 lines 992.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Freemius
4 * @copyright Copyright (c) 2015, Freemius, Inc.
5 * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
6 * @since 1.0.3
7 */
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 // "final class"
13 class Freemius extends Freemius_Abstract {
14 /**
15 * SDK Version
16 *
17 * @var string
18 */
19 public $version = WP_FS__SDK_VERSION;
20
21 #region Plugin Info
22
23 /**
24 * @since 1.0.1
25 *
26 * @var string
27 */
28 private $_slug;
29
30 /**
31 * @since 1.0.0
32 *
33 * @var string
34 */
35 private $_plugin_basename;
36 /**
37 * @since 2.2.1
38 *
39 * @var string
40 */
41 private $_premium_plugin_basename;
42 /**
43 * @since 1.0.0
44 *
45 * @var string
46 */
47 private $_free_plugin_basename;
48 /**
49 * @since 1.0.0
50 *
51 * @var string
52 */
53 private $_plugin_dir_path;
54 /**
55 * @since 1.0.0
56 *
57 * @var string
58 */
59 private $_plugin_dir_name;
60 /**
61 * @since 1.0.0
62 *
63 * @var string
64 */
65 private $_plugin_main_file_path;
66 /**
67 * @var string[]
68 */
69 private $_plugin_data;
70 /**
71 * @since 1.0.9
72 *
73 * @var string
74 */
75 private $_plugin_name;
76 /**
77 * @since 1.2.2
78 *
79 * @var string
80 */
81 private $_module_type;
82
83 #endregion Plugin Info
84
85 /**
86 * @since 1.0.9
87 *
88 * @var bool If false, don't turn Freemius on.
89 */
90 private $_is_on;
91
92 /**
93 * @since 1.1.3
94 *
95 * @var bool If false, don't turn Freemius on.
96 */
97 private $_is_anonymous;
98
99 /**
100 * @since 1.0.9
101 * @var bool If false, issues with connectivity to Freemius API.
102 */
103 private $_has_api_connection;
104
105 /**
106 * @since 1.0.9
107 * @since 2.0.0 Default to true since we need the property during the instance construction, prior to the dynamic_init() execution.
108 * @var bool Hints the SDK if plugin can support anonymous mode (if skip connect is visible).
109 */
110 private $_enable_anonymous = true;
111
112 /**
113 * @since 1.1.7.5
114 * @var bool Hints the SDK if plugin should run in anonymous mode (only adds feedback form).
115 */
116 private $_anonymous_mode;
117
118 /**
119 * @since 1.1.9
120 * @var bool Hints the SDK if plugin have any free plans.
121 */
122 private $_is_premium_only;
123
124 /**
125 * @since 1.2.1.6
126 * @var bool Hints the SDK if plugin have premium code version at all.
127 */
128 private $_has_premium_version;
129
130 /**
131 * @since 1.2.1.6
132 * @var bool Hints the SDK if plugin should ignore pending mode by simulating a skip.
133 */
134 private $_ignore_pending_mode;
135
136 /**
137 * @since 1.0.8
138 * @var bool Hints the SDK if the plugin has any paid plans.
139 */
140 private $_has_paid_plans;
141
142 /**
143 * @since 1.2.1.5
144 * @var int Hints the SDK if the plugin offers a trial period. If negative, no trial, if zero - has a trial but
145 * without a specified period, if positive - the number of trial days.
146 */
147 private $_trial_days = - 1;
148
149 /**
150 * @since 1.2.1.5
151 * @var bool Hints the SDK if the trial requires a payment method or not.
152 */
153 private $_is_trial_require_payment = false;
154
155 /**
156 * @since 1.0.7
157 * @var bool Hints the SDK if the plugin is WordPress.org compliant.
158 */
159 private $_is_org_compliant;
160
161 /**
162 * @since 1.0.7
163 * @var bool Hints the SDK if the plugin is has add-ons.
164 */
165 private $_has_addons;
166
167 /**
168 * @since 2.4.5
169 * @var string Navigation type: 'menu' or 'tabs'.
170 */
171 private $_navigation;
172
173 const NAVIGATION_MENU = 'menu';
174 const NAVIGATION_TABS = 'tabs';
175
176 /**
177 * @since 1.1.6
178 * @var string[]bool.
179 */
180 private $_permissions;
181
182 /**
183 * @var FS_Storage
184 */
185 private $_storage;
186
187 /**
188 * @since 1.2.2.7
189 * @var FS_Cache_Manager
190 */
191 private $_cache;
192
193 /**
194 * @since 1.0.0
195 *
196 * @var FS_Logger
197 */
198 private $_logger;
199 /**
200 * @since 1.0.4
201 *
202 * @var FS_Plugin
203 */
204 private $_plugin = false;
205 /**
206 * @since 1.0.4
207 *
208 * @var FS_Plugin|false
209 */
210 private $_parent_plugin = false;
211 /**
212 * @since 1.1.1
213 *
214 * @var Freemius
215 */
216 private $_parent = false;
217 /**
218 * @since 1.0.1
219 *
220 * @var FS_User
221 */
222 private $_user = false;
223 /**
224 * @since 1.0.1
225 *
226 * @var FS_Site
227 */
228 private $_site = false;
229 /**
230 * @since 1.0.1
231 *
232 * @var FS_Plugin_License
233 */
234 private $_license;
235 /**
236 * @since 1.0.2
237 *
238 * @var FS_Plugin_Plan[]
239 */
240 private $_plans = false;
241 /**
242 * @var FS_Plugin_License[]
243 * @since 1.0.5
244 */
245 private $_licenses = false;
246
247 /**
248 * @since 1.0.1
249 *
250 * @var FS_Admin_Menu_Manager
251 */
252 private $_menu;
253
254 /**
255 * @var FS_Admin_Notices
256 */
257 private $_admin_notices;
258
259 /**
260 * @since 1.1.6
261 *
262 * @var FS_Admin_Notices
263 */
264 private static $_global_admin_notices;
265
266 /**
267 * @var FS_Logger
268 * @since 1.0.0
269 */
270 private static $_static_logger;
271
272 /**
273 * @var FS_Options
274 * @since 1.0.2
275 */
276 private static $_accounts;
277
278 /**
279 * @since 1.2.2
280 *
281 * @var number
282 */
283 private $_module_id;
284
285 /**
286 * @var Freemius[]
287 */
288 private static $_instances = array();
289
290 /**
291 * @since 1.2.3
292 *
293 * @var FS_Affiliate
294 */
295 private $affiliate = null;
296
297 /**
298 * @since 1.2.3
299 *
300 * @var FS_AffiliateTerms
301 */
302 private $plugin_affiliate_terms = null;
303
304 /**
305 * @since 1.2.3
306 *
307 * @var FS_AffiliateTerms
308 */
309 private $custom_affiliate_terms = null;
310
311 /**
312 * @since 2.0.0
313 *
314 * @var bool
315 */
316 private $_is_multisite_integrated;
317
318 /**
319 * @since 2.0.0
320 *
321 * @var bool True if the current request is for a network admin screen and the plugin is network active.
322 */
323 private $_is_network_active;
324
325 /**
326 * @since 2.0.0
327 *
328 * @var int|null The original blog ID the plugin was loaded with.
329 */
330 private $_blog_id = null;
331
332 /**
333 * @since 2.0.0
334 *
335 * @var int|null The current execution context. When true, run on network context. When int, run on the specified blog context.
336 */
337 private $_context_is_network_or_blog_id = null;
338
339 /**
340 * @since 2.0.0
341 *
342 * @var string
343 */
344 private $_dynamically_added_top_level_page_hook_name = '';
345
346 /**
347 * @author Leo Fajardo (@leorw)
348 * @since 2.3.1
349 *
350 * @var bool
351 */
352 private $is_whitelabeled;
353
354 /**
355 * @author Leo Fajardo (@leorw)
356 * @since 2.4.0
357 *
358 * @var bool
359 */
360 private $_is_bundle_license_auto_activation_enabled = false;
361
362 #region Uninstall Reasons IDs
363
364 const REASON_NO_LONGER_NEEDED = 1;
365 const REASON_FOUND_A_BETTER_PLUGIN = 2;
366 const REASON_NEEDED_FOR_A_SHORT_PERIOD = 3;
367 const REASON_BROKE_MY_SITE = 4;
368 const REASON_SUDDENLY_STOPPED_WORKING = 5;
369 const REASON_CANT_PAY_ANYMORE = 6;
370 const REASON_OTHER = 7;
371 const REASON_DIDNT_WORK = 8;
372 const REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION = 9;
373 const REASON_COULDNT_MAKE_IT_WORK = 10;
374 const REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE = 11;
375 const REASON_NOT_WORKING = 12;
376 const REASON_NOT_WHAT_I_WAS_LOOKING_FOR = 13;
377 const REASON_DIDNT_WORK_AS_EXPECTED = 14;
378 const REASON_TEMPORARY_DEACTIVATION = 15;
379
380 #endregion
381
382 /**
383 * @author Leo Fajardo (@leorw)
384 * @since 2.3.1
385 *
386 * @var boolean|null
387 */
388 private $_use_external_pricing = null;
389 /**
390 * @author Leo Fajardo (@leorw)
391 * @since 2.4.2
392 *
393 * @var string|null
394 */
395 private $_pricing_js_path = null;
396
397 const VERSION_MAX_CHARS = 16;
398 const LANGUAGE_MAX_CHARS = 8;
399
400 /* Ctor
401 ------------------------------------------------------------------------------------------------------------------*/
402
403 /**
404 * Main singleton instance.
405 *
406 * @author Vova Feldman (@svovaf)
407 * @since 1.0.0
408 *
409 * @param number $module_id
410 * @param string|bool $slug
411 * @param bool $is_init Since 1.2.1 Is initiation sequence.
412 */
413 private function __construct( $module_id, $slug = false, $is_init = false ) {
414 $main_file = false;
415
416 if ( $is_init && is_numeric( $module_id ) && is_string( $slug ) ) {
417 $main_file = $this->store_id_slug_type_path_map( $module_id, $slug );
418 }
419
420 $this->_module_id = $module_id;
421 $this->_slug = $this->get_slug();
422 $this->_module_type = $this->get_module_type();
423
424 $this->_blog_id = is_multisite() ? get_current_blog_id() : null;
425
426 $this->_storage = FS_Storage::instance( $this->_module_type, $this->_slug );
427
428 // If not set or 24 hours have already passed from the last time it's set, set the last load timestamp to the current time.
429 if (
430 ! isset( $this->_storage->last_load_timestamp ) ||
431 $this->_storage->last_load_timestamp < ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC ) )
432 ) {
433 $this->_storage->last_load_timestamp = time();
434 }
435
436 $this->_cache = FS_Cache_Manager::get_manager( WP_FS___OPTION_PREFIX . "cache_{$module_id}" );
437
438 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $this->get_unique_affix(), WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
439
440 $this->_plugin_main_file_path = $this->_find_caller_plugin_file( $is_init, $main_file );
441 $this->_plugin_dir_path = plugin_dir_path( $this->_plugin_main_file_path );
442 $this->_plugin_basename = $this->get_plugin_basename();
443 $this->_free_plugin_basename = str_replace( '-premium/', '/', $this->_plugin_basename );
444
445 $this->_is_multisite_integrated = (
446 defined( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) &&
447 ( true === constant( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) )
448 );
449
450 $this->_is_network_active = (
451 is_multisite() &&
452 $this->_is_multisite_integrated &&
453 // Themes are always network activated, but the ACTUAL activation is per site.
454 $this->is_plugin() &&
455 (
456 is_plugin_active_for_network( $this->_plugin_basename ) ||
457 // Plugin network level activation or uninstall.
458 ( fs_is_network_admin() && is_plugin_inactive( $this->_plugin_basename ) )
459 )
460 );
461
462 $this->_storage->set_network_active(
463 $this->_is_network_active,
464 $this->is_delegated_connection()
465 );
466
467 if ( ! isset( $this->_storage->is_network_activated ) ) {
468 $this->_storage->is_network_activated = $this->_is_network_active;
469 }
470
471 if ( $this->_storage->is_network_activated != $this->_is_network_active ) {
472 // Update last activation level.
473 $this->_storage->is_network_activated = $this->_is_network_active;
474
475 $this->maybe_adjust_storage();
476 }
477
478 #region Migration
479
480 if ( is_multisite() ) {
481 /**
482 * If the install_timestamp exists on the site level but doesn't exist on the
483 * network level storage, it means that we need to process the storage with migration.
484 *
485 * The code in this `if` scope will only be executed once and only for the first site that will execute it because once we migrate the storage data, install_timestamp will be already set in the network level storage.
486 *
487 * @author Vova Feldman (@svovaf)
488 * @since 2.0.0
489 */
490 if ( false === $this->_storage->get( 'install_timestamp', false, true ) &&
491 false !== $this->_storage->get( 'install_timestamp', false, false )
492 ) {
493 // Initiate storage migration.
494 $this->_storage->migrate_to_network();
495
496 // Migrate module cache to network level storage.
497 $this->_cache->migrate_to_network();
498 }
499 }
500
501 #endregion
502
503 $base_name_split = explode( '/', $this->_plugin_basename );
504 $this->_plugin_dir_name = $base_name_split[0];
505
506 if ( $this->_logger->is_on() ) {
507 $this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
508 $this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
509 $this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
510 $this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
511 $this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
512 }
513
514 // Remember link between file to slug.
515 $this->store_file_slug_map();
516
517 // Store plugin's initial install timestamp.
518 if ( ! isset( $this->_storage->install_timestamp ) ) {
519 $this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
520 }
521
522 if ( ! is_object( $this->_plugin ) ) {
523 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
524 }
525
526 $this->_admin_notices = FS_Admin_Notices::instance(
527 $this->_slug . ( $this->is_theme() ? ':theme' : '' ),
528 /**
529 * Ensure that the admin notice will always have a title by using the stored plugin title if available and
530 * retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
531 *
532 * @author Leo Fajardo (@leorw)
533 * @since 1.2.2
534 */
535 ( is_object( $this->_plugin ) && isset( $this->_plugin->title ) ?
536 $this->_plugin->title :
537 $this->get_plugin_name()
538 ),
539 $this->get_unique_affix()
540 );
541
542 if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
543 fs_request_is_action( 'restart_freemius' )
544 ) {
545 FS_Api::clear_cache();
546 $this->_cache->clear();
547 }
548
549 $this->register_constructor_hooks();
550
551 /**
552 * Starting from version 2.0.0, `FS_Site` entities no longer have the `plan` property and have `plan_id`
553 * instead. This should be called before calling `_load_account()`, otherwise, `$this->_site` will not be
554 * loaded in `_load_account` for versions of SDK starting from 2.0.0.
555 *
556 * @author Leo Fajardo (@leorw)
557 */
558 self::migrate_install_plan_to_plan_id( $this->_storage );
559
560 $this->_load_account();
561
562 $this->_version_updates_handler();
563 }
564
565 /**
566 * @author Leo Fajardo (@leorw)
567 * @since 2.3.0
568 */
569 private function maybe_adjust_storage() {
570 $install_timestamp = null;
571 $prev_is_premium = null;
572
573 $options_to_update = array();
574
575 $is_network_admin = fs_is_network_admin();
576
577 $network_install_timestamp = $this->_storage->get( 'install_timestamp', null, true );
578
579 if ( ! $is_network_admin ) {
580 if ( is_null( $network_install_timestamp ) ) {
581 // Plugin was not network-activated before.
582 return;
583 }
584
585 if ( is_null( $this->_storage->get( 'install_timestamp', null, false ) ) ) {
586 // Set the `install_timestamp` only if it's not yet set.
587 $install_timestamp = $network_install_timestamp;
588 }
589
590 $prev_is_premium = $this->_storage->get( 'prev_is_premium', null, true );
591 } else {
592 $current_wp_user = self::_get_current_wp_user();
593 $current_fs_user = self::_get_user_by_email( $current_wp_user->user_email );
594 $network_user_info = array();
595
596 $skips_count = 0;
597
598 $sites = self::get_sites();
599 $sites_count = count( $sites );
600
601 $blog_id_2_install_map = array();
602
603 $is_first_non_ignored_blog = true;
604
605 foreach ( $sites as $site ) {
606 $blog_id = self::get_site_blog_id( $site );
607
608 $blog_install_timestamp = $this->_storage->get( 'install_timestamp', null, $blog_id );
609
610 if ( is_null( $blog_install_timestamp ) ) {
611 // Plugin has not been installed on this blog.
612 continue;
613 }
614
615 $is_earlier_install = (
616 ! is_null( $install_timestamp ) &&
617 $blog_install_timestamp < $install_timestamp
618 );
619
620 $install = $this->get_install_by_blog_id( $blog_id );
621
622 $update_network_user_info = false;
623
624 if ( ! is_object( $install ) ) {
625 if ( ! $this->_storage->get( 'is_anonymous', false, $blog_id ) ) {
626 // The opt-in decision (whether to skip or opt in) is yet to be made.
627 continue;
628 }
629
630 $skips_count ++;
631 } else {
632 $blog_id_2_install_map[ $blog_id ] = $install;
633
634 if ( empty( $network_user_info ) ) {
635 // Set the network user info for the 1st time. Choose any user information whether or not it is for the current WP user.
636 $update_network_user_info = true;
637 }
638
639 if ( ! $update_network_user_info &&
640 is_object( $current_fs_user ) &&
641 $network_user_info['user_id'] != $current_fs_user->id &&
642 $install->user_id == $current_fs_user->id
643 ) {
644 // If an install that is owned by the current WP user is found, use its user information instead.
645 $update_network_user_info = true;
646 }
647
648 if ( ! $update_network_user_info &&
649 $is_earlier_install &&
650 ( ! is_object( $current_fs_user ) || $current_fs_user->id == $install->user_id )
651 ) {
652 // Update to the earliest install info if there's no install found so far that is owned by the current WP user; OR only if the found install is owned by the current WP user.
653 $update_network_user_info = true;
654 }
655 }
656
657 if ( $update_network_user_info ) {
658 $network_user_info = array(
659 'user_id' => $install->user_id,
660 'blog_id' => $blog_id
661 );
662 }
663
664 $site_prev_is_premium = $this->_storage->get( 'prev_is_premium', null, $blog_id );
665
666 if ( $is_first_non_ignored_blog ) {
667 $prev_is_premium = $site_prev_is_premium;
668
669 if ( is_null( $network_install_timestamp ) ) {
670 $install_timestamp = $blog_install_timestamp;
671 }
672
673 $is_first_non_ignored_blog = false;
674
675 continue;
676 }
677
678 if ( ! is_null( $prev_is_premium ) && $prev_is_premium !== $site_prev_is_premium ) {
679 // If a different `$site_prev_is_premium` value is found, do not include the option in the collection of options to update.
680 $prev_is_premium = null;
681 }
682
683 if ( $is_earlier_install ) {
684 // If an earlier install timestamp is found.
685 $install_timestamp = $blog_install_timestamp;
686 }
687 }
688
689 $installs_count = count( $blog_id_2_install_map );
690
691 if ( $sites_count === ( $installs_count + $skips_count ) ) {
692 if ( ! empty( $network_user_info ) ) {
693 $options_to_update['network_user_id'] = $network_user_info['user_id'];
694 $options_to_update['network_install_blog_id'] = $network_user_info['blog_id'];
695
696 foreach ( $blog_id_2_install_map as $blog_id => $install ) {
697 if ( $install->user_id == $network_user_info['user_id'] ) {
698 continue;
699 }
700
701 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
702 }
703 }
704
705 if ( $sites_count === $skips_count ) {
706 /**
707 * Assume network-level skipping as the intended action if all actions identified were only
708 * skipping of the connection (i.e., no opt-ins and delegated connections so far).
709 */
710 $options_to_update['is_anonymous_ms'] = true;
711 } else if ( $sites_count === $installs_count ) {
712 /**
713 * Assume network-level opt-in as the intended action if all actions identified were only opt-ins
714 * (i.e., no delegation and skipping of the connections so far).
715 */
716 $options_to_update['is_network_connected'] = true;
717 }
718 }
719 }
720
721 if ( ! is_null( $install_timestamp ) ) {
722 $options_to_update['install_timestamp'] = $install_timestamp;
723 }
724
725 if ( ! is_null( $prev_is_premium ) ) {
726 $options_to_update['prev_is_premium'] = $prev_is_premium;
727 }
728
729 if ( ! empty( $options_to_update ) ) {
730 $this->adjust_storage( $options_to_update, $is_network_admin );
731 }
732 }
733
734 /**
735 * @author Leo Fajardo (@leorw)
736 * @since 2.3.0
737 *
738 * @param array $options
739 * @param bool $is_network_admin
740 */
741 private function adjust_storage( $options, $is_network_admin ) {
742 foreach ( $options as $name => $value ) {
743 $this->_storage->store( $name, $value, $is_network_admin ? true : null );
744 }
745 }
746
747 /**
748 * Checks whether this module has a settings menu.
749 *
750 * @author Leo Fajardo (@leorw)
751 * @since 1.2.2
752 *
753 * @return bool
754 */
755 function has_settings_menu() {
756 return ( $this->_is_network_active && fs_is_network_admin() ) ?
757 $this->_menu->has_network_menu() :
758 $this->_menu->has_menu();
759 }
760
761 /**
762 * If `true` the opt-in should be shown as a modal dialog box on the themes.php page. WordPress.org themes guidelines prohibit from redirecting the user from the themes.php page after activating a theme.
763 *
764 * @author Vova Feldman (@svovaf)
765 * @since 2.4.5
766 *
767 * @return bool
768 */
769 function show_opt_in_on_themes_page() {
770 if ( ! $this->is_free_wp_org_theme() ) {
771 return false;
772 }
773
774 if ( ! $this->has_settings_menu() ) {
775 return true;
776 }
777
778 return $this->show_settings_with_tabs();
779 }
780
781 /**
782 * If `true` the opt-in should be shown on the product's main setting page.
783 *
784 * @author Vova Feldman (@svovaf)
785 * @since 2.4.5
786 *
787 * @return bool
788 *
789 * @uses show_opt_in_on_themes_page();
790 */
791 function show_opt_in_on_setting_page() {
792 return ! $this->show_opt_in_on_themes_page();
793 }
794
795 /**
796 * If `true` the settings should be shown using tabs.
797 *
798 * @author Vova Feldman (@svovaf)
799 * @since 2.4.5
800 *
801 * @return bool
802 */
803 function show_settings_with_tabs() {
804 return ( self::NAVIGATION_TABS === $this->_navigation );
805 }
806
807 /**
808 * Check if the context module is free wp.org theme.
809 *
810 * This method is helpful because:
811 * 1. wp.org themes are limited to a single submenu item,
812 * and sub-submenu items are most likely not allowed (never verified).
813 * 2. wp.org themes are not allowed to redirect the user
814 * after the theme activation, therefore, the agreed UX
815 * is showing the opt-in as a modal dialog box after
816 * activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
817 *
818 * @author Vova Feldman (@svovaf)
819 * @since 1.2.2.7
820 *
821 * @return bool
822 */
823 function is_free_wp_org_theme() {
824 return (
825 $this->is_theme() &&
826 $this->is_org_repo_compliant() &&
827 ! $this->is_premium()
828 );
829 }
830
831 /**
832 * Checks whether this a submenu item is visible.
833 *
834 * @author Vova Feldman (@svovaf)
835 * @since 1.2.2.6
836 * @since 1.2.2.7 Even if the menu item was specified to be hidden, when it is the context page, then show the submenu item so the user will have the right context page.
837 *
838 * @param string $slug
839 * @param bool $is_tabs_visibility_check This is used to decide if the associated tab should be shown or hidden.
840 *
841 * @return bool
842 */
843 function is_submenu_item_visible( $slug, $is_tabs_visibility_check = false ) {
844 if ( $this->is_admin_page( $slug ) ) {
845 /**
846 * It is the current context page, so show the submenu item
847 * so the user will have the right context page, even if it
848 * was set to hidden.
849 */
850 return true;
851 }
852
853 if ( ! $this->has_settings_menu() ) {
854 // No menu settings at all.
855 return false;
856 }
857
858 if (
859 ! $is_tabs_visibility_check &&
860 $this->is_org_repo_compliant() &&
861 $this->show_settings_with_tabs()
862 ) {
863 /**
864 * wp.org themes are limited to a single submenu item, and
865 * sub-submenu items are most likely not allowed (never verified).
866 */
867 return false;
868 }
869
870 return $this->_menu->is_submenu_item_visible( $slug );
871 }
872
873 /**
874 * Check if a Freemius page should be accessible via the UI.
875 *
876 * @author Vova Feldman (@svovaf)
877 * @since 1.2.2.7
878 *
879 * @param string $slug
880 *
881 * @return bool
882 */
883 function is_page_visible( $slug ) {
884 if ( $this->is_admin_page( $slug ) ) {
885 return true;
886 }
887
888 return $this->_menu->is_submenu_item_visible( $slug, true, true );
889 }
890
891 /**
892 * @author Vova Feldman (@svovaf)
893 * @since 1.0.9
894 */
895 private function _version_updates_handler() {
896 if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
897 // Freemius version upgrade mode.
898 $this->_storage->sdk_last_version = $this->_storage->sdk_version;
899 $this->_storage->sdk_version = $this->version;
900
901 if ( empty( $this->_storage->sdk_last_version ) ||
902 version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
903 ) {
904 $this->_storage->sdk_upgrade_mode = true;
905 $this->_storage->sdk_downgrade_mode = false;
906 } else {
907 $this->_storage->sdk_downgrade_mode = true;
908 $this->_storage->sdk_upgrade_mode = false;
909
910 }
911
912 $this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
913 }
914
915 $plugin_version = $this->get_plugin_version();
916 if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
917 // Plugin version upgrade mode.
918 $this->_storage->plugin_last_version = $this->_storage->plugin_version;
919 $this->_storage->plugin_version = $plugin_version;
920
921 if ( empty( $this->_storage->plugin_last_version ) ||
922 version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
923 ) {
924 $this->_storage->plugin_upgrade_mode = true;
925 $this->_storage->plugin_downgrade_mode = false;
926 } else {
927 $this->_storage->plugin_downgrade_mode = true;
928 $this->_storage->plugin_upgrade_mode = false;
929 }
930
931 if ( ! empty( $this->_storage->plugin_last_version ) ) {
932 // Different version of the plugin was installed before, therefore it's an update.
933 $this->_storage->is_plugin_new_install = false;
934 }
935
936 $this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
937 }
938 }
939
940 #--------------------------------------------------------------------------------
941 #region Data Migration on SDK Update
942 #--------------------------------------------------------------------------------
943
944 /**
945 * @author Vova Feldman (@svovaf)
946 * @since 1.1.5
947 *
948 * @param string $sdk_prev_version
949 * @param string $sdk_version
950 */
951 function _sdk_version_update( $sdk_prev_version, $sdk_version ) {
952 if ( empty( $sdk_prev_version ) ) {
953 return;
954 }
955
956 if (
957 version_compare( $sdk_prev_version, '2.5.1', '<' ) &&
958 version_compare( $sdk_version, '2.5.1', '>=' )
959 ) {
960 if ( $this->is_registered( true ) ) {
961 /**
962 * Migrate to new permissions layer.
963 */
964 require_once WP_FS__DIR_INCLUDES . '/supplements/fs-migration-2.5.1.php';
965
966 $install_by_blog_id = is_multisite() ?
967 $this->get_blog_install_map() :
968 array( 0 => $this->_site );
969
970 fs_migrate_251( $this, $install_by_blog_id );
971 }
972 }
973 }
974
975 /**
976 * @author Leo Fajardo (@leorw)
977 * @since 2.0.0
978 *
979 * @param \FS_Storage $storage
980 * @param bool|int|null $blog_id
981 */
982 private static function migrate_install_plan_to_plan_id( FS_Storage $storage, $blog_id = null ) {
983 if ( empty( $storage->sdk_version ) ) {
984 // New installation of the plugin, no need to upgrade.
985 return;
986 }
987
988 if ( ! version_compare( $storage->sdk_version, '2.0.0', '<' ) ) {
989 // Previous version is >= 2.0.0, so no need to migrate.
990 return;
991 }
992
993 // Alias.
994 $module_type = $storage->get_module_type();
995 $module_slug = $storage->get_module_slug();
996
997 $installs = self::get_all_sites( $module_type, $blog_id );
998 $install = isset( $installs[ $module_slug ] ) ? $installs[ $module_slug ] : null;
999
1000 if ( ! is_object( $install ) ) {
1001 return;
1002 }
1003
1004 if ( isset( $install->plan ) && is_object( $install->plan ) ) {
1005 if ( isset( $install->plan->id ) && ! empty( $install->plan->id ) ) {
1006 $install->plan_id = self::_decrypt( $install->plan->id );
1007 }
1008
1009 unset( $install->plan );
1010
1011 $installs[ $module_slug ] = clone $install;
1012
1013 self::set_account_option_by_module(
1014 $module_type,
1015 'sites',
1016 $installs,
1017 true,
1018 $blog_id
1019 );
1020 }
1021 }
1022
1023 /**
1024 * @author Vova Feldman (@svovaf)
1025 * @since 1.2.2.7
1026 *
1027 * @param string $plugin_prev_version
1028 * @param string $plugin_version
1029 */
1030 function _after_version_update( $plugin_prev_version, $plugin_version ) {
1031 if ( $this->is_theme() ) {
1032 // Expire the cache of the previous tabs since the theme may
1033 // have setting updates.
1034 $this->_cache->expire( 'tabs' );
1035 $this->_cache->expire( 'tabs_stylesheets' );
1036 }
1037 }
1038
1039 /**
1040 * A special migration logic for the $_accounts, executed for all the plugins in the system:
1041 * - Moves some data to the network level storage.
1042 * - If the plugin's connection was skipped for all sites, set the plugin as if it was network skipped.
1043 * - If the plugin's connection was ignored for all sites, don't do anything in terms of the network connection.
1044 * - If the plugin was connected to all sites by the same super-admin, set the plugin as if was network opted-in for all sites.
1045 * - If there's at least one site that was connected by a super-admin, find the "main super-admin" (the one that installed the majority of the plugin installs) and set the plugin as if was network activated with the main super-admin, set all the sites that were skipped or opted-in with a different user to delegated mode. Then, prompt the currently logged super-admin to choose what to do with the ignored sites.
1046 * - If there are any sites in the network which the connection decision was not yet taken for, set this plugin into network activation mode so a super-admin can choose what to do with the rest of the sites.
1047 *
1048 * @author Vova Feldman (@svovaf)
1049 * @since 2.0.0
1050 */
1051 private static function migrate_accounts_to_network() {
1052 $sites = self::get_sites();
1053 $sites_count = count( $sites );
1054 $connection_status = array();
1055 $plugin_slugs = array();
1056 foreach ( $sites as $site ) {
1057 $blog_id = self::get_site_blog_id( $site );
1058
1059 self::$_accounts->migrate_to_network( $blog_id );
1060
1061 /**
1062 * Build a list of all Freemius powered plugins slugs.
1063 */
1064 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array(), $blog_id );
1065 foreach ( $id_slug_type_path_map as $module_id => $data ) {
1066 if ( WP_FS__MODULE_TYPE_PLUGIN === $data['type'] ) {
1067 $plugin_slugs[ $data['slug'] ] = true;
1068 }
1069 }
1070
1071 $installs = self::get_account_option( 'sites', WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
1072
1073 if ( is_array( $installs ) ) {
1074 foreach ( $installs as $slug => $install ) {
1075 if ( ! isset( $connection_status[ $slug ] ) ) {
1076 $connection_status[ $slug ] = array();
1077 }
1078
1079 if ( is_object( $install ) &&
1080 FS_Site::is_valid_id( $install->id ) &&
1081 FS_User::is_valid_id( $install->user_id )
1082 ) {
1083 $connection_status[ $slug ][ $blog_id ] = $install->user_id;
1084 }
1085 }
1086 }
1087 }
1088
1089 foreach ( $plugin_slugs as $slug => $true ) {
1090 if ( ! isset( $connection_status[ $slug ] ) ) {
1091 $connection_status[ $slug ] = array();
1092 }
1093
1094 foreach ( $sites as $site ) {
1095 $blog_id = self::get_site_blog_id( $site );
1096
1097 if ( isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1098 continue;
1099 }
1100
1101 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1102
1103 $is_anonymous = $storage->get( 'is_anonymous', null, $blog_id );
1104
1105 if ( ! is_null( $is_anonymous ) ) {
1106 // Since 1.1.3 is_anonymous is an array.
1107 if ( is_array( $is_anonymous ) && isset( $is_anonymous['is'] ) ) {
1108 $is_anonymous = $is_anonymous['is'];
1109 }
1110
1111 if ( is_bool( $is_anonymous ) && true === $is_anonymous ) {
1112 $connection_status[ $slug ][ $blog_id ] = 'skipped';
1113 }
1114 }
1115
1116 if ( ! isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1117 $connection_status[ $slug ][ $blog_id ] = 'ignored';
1118 }
1119 }
1120 }
1121
1122 $super_admins = array();
1123
1124 foreach ( $connection_status as $slug => $blogs_status ) {
1125 $skips = 0;
1126 $ignores = 0;
1127 $connections = 0;
1128 $opted_in_users = array();
1129 $opted_in_super_admins = array();
1130
1131 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1132
1133 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1134 if ( 'skipped' === $status_or_user_id ) {
1135 $skips ++;
1136 } else if ( 'ignored' === $status_or_user_id ) {
1137 $ignores ++;
1138 } else if ( FS_User::is_valid_id( $status_or_user_id ) ) {
1139 $connections ++;
1140
1141 if ( ! isset( $opted_in_users[ $status_or_user_id ] ) ) {
1142 $opted_in_users[ $status_or_user_id ] = array();
1143 }
1144
1145 $opted_in_users[ $status_or_user_id ][] = $blog_id;
1146
1147 if ( isset( $super_admins[ $status_or_user_id ] ) ||
1148 self::is_super_admin( $status_or_user_id )
1149 ) {
1150 // Cache super-admin data.
1151 $super_admins[ $status_or_user_id ] = true;
1152
1153 // Remember opted-in super-admins for the plugin.
1154 $opted_in_super_admins[ $status_or_user_id ] = true;
1155 }
1156 }
1157 }
1158
1159 $main_super_admin_user_id = null;
1160 $all_migrated = false;
1161 if ( $sites_count == $skips ) {
1162 // All sites were skipped -> network skip by copying the anonymous mode from any of the sites.
1163 $storage->is_anonymous_ms = $storage->is_anonymous;
1164
1165 $all_migrated = true;
1166 } else if ( $sites_count == $ignores ) {
1167 // Don't do anything, still in activation mode.
1168
1169 $all_migrated = true;
1170 } else if ( 0 < count( $opted_in_super_admins ) ) {
1171 // Find the super-admin with the majority of installs.
1172 $max_installs_by_super_admin = 0;
1173 foreach ( $opted_in_super_admins as $user_id => $true ) {
1174 $installs_count = count( $opted_in_users[ $user_id ] );
1175
1176 if ( $installs_count > $max_installs_by_super_admin ) {
1177 $max_installs_by_super_admin = $installs_count;
1178 $main_super_admin_user_id = $user_id;
1179 }
1180 }
1181
1182 if ( $sites_count == $connections && 1 == count( $opted_in_super_admins ) ) {
1183 // Super-admin opted-in for all sites in the network.
1184 $storage->is_network_connected = true;
1185
1186 $all_migrated = true;
1187 }
1188
1189 // Store network user.
1190 $storage->network_user_id = $main_super_admin_user_id;
1191
1192 $storage->network_install_blog_id = ( $sites_count == $connections ) ?
1193 // Since all sites are opted-in, associating with the main site.
1194 get_current_blog_id() :
1195 // Associating with the 1st found opted-in site.
1196 $opted_in_users[ $main_super_admin_user_id ][0];
1197
1198 /**
1199 * Make sure we migrate the plan ID of the network install, otherwise, if after the migration
1200 * the 1st page that will be loaded is the network level WP Admin and $storage->network_install_blog_id
1201 * is different than the main site of the network, the $this->_site will not be set since the plan_id
1202 * will be empty.
1203 */
1204 $storage->migrate_to_network();
1205 self::migrate_install_plan_to_plan_id( $storage, $storage->network_install_blog_id );
1206 } else {
1207 // At least one opt-in. All the opt-in were created by a non-super-admin.
1208 if ( 0 == $ignores ) {
1209 // All sites were opted-in or skipped, all by non-super-admin. So delegate all.
1210 $storage->store( 'is_delegated_connection', true, true );
1211
1212 $all_migrated = true;
1213 }
1214 }
1215
1216 if ( ! $all_migrated ) {
1217 /**
1218 * Delegate all sites that were:
1219 * 1) Opted-in by a user that is NOT the main-super-admin.
1220 * 2) Skipped and non of the sites was opted-in by a super-admin. If any site was opted-in by a super-admin, there will be a main-super-admin, and we consider the skip as if it was done by that user.
1221 */
1222 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1223 if ( $status_or_user_id == $main_super_admin_user_id ) {
1224 continue;
1225 }
1226
1227 if ( FS_User::is_valid_id( $status_or_user_id ) ||
1228 ( 'skipped' === $status_or_user_id && is_null( $main_super_admin_user_id ) )
1229 ) {
1230 $storage->store( 'is_delegated_connection', true, $blog_id );
1231 }
1232 }
1233 }
1234
1235
1236 if ( ( $connections + $skips > 0 ) ) {
1237 if ( $ignores > 0 ) {
1238 /**
1239 * If admin already opted-in or skipped in any of the network sites, and also
1240 * have sites which the connection decision was not yet taken, set this plugin
1241 * into network activation mode so the super-admin can choose what to do with
1242 * the rest of the sites.
1243 */
1244 self::set_network_upgrade_mode( $storage );
1245 }
1246 }
1247 }
1248 }
1249
1250 /**
1251 * Set a module into network upgrade mode.
1252 *
1253 * @author Vova Feldman (@svovaf)
1254 * @since 2.0.0
1255 *
1256 * @param \FS_Storage $storage
1257 *
1258 * @return bool
1259 */
1260 private static function set_network_upgrade_mode( FS_Storage $storage ) {
1261 return $storage->is_network_activation = true;
1262 }
1263
1264 /**
1265 * Will return true after upgrading to the SDK with the network level integration,
1266 * when the super-admin involvement is required regarding the rest of the sites.
1267 *
1268 * @author Vova Feldman (@svovaf)
1269 * @since 2.0.0
1270 *
1271 * @return bool
1272 */
1273 function is_network_upgrade_mode() {
1274 return $this->_storage->get( 'is_network_activation' );
1275 }
1276
1277 /**
1278 * Clear flag after the upgrade mode completion.
1279 *
1280 * @author Vova Feldman (@svovaf)
1281 * @since 2.0.0
1282 *
1283 * @return bool True if network activation was on and now completed.
1284 */
1285 private function network_upgrade_mode_completed() {
1286 if ( fs_is_network_admin() && $this->is_network_upgrade_mode() ) {
1287 $this->_storage->remove( 'is_network_activation' );
1288
1289 return true;
1290 }
1291
1292 return false;
1293 }
1294
1295 #endregion
1296
1297 /**
1298 * This action is connected to the 'plugins_loaded' hook and helps to determine
1299 * if this is a new plugin installation or a plugin update.
1300 *
1301 * There are 3 different use-cases:
1302 * 1) New plugin installation right with Freemius:
1303 * 1.1 _activate_plugin_event_hook() will be executed first
1304 * 1.2 Since $this->_storage->is_plugin_new_install is not set,
1305 * and $this->_storage->plugin_last_version is not set,
1306 * $this->_storage->is_plugin_new_install will be set to TRUE.
1307 * 1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1308 * be already set to TRUE.
1309 *
1310 * 2) Plugin update, didn't have Freemius before, and now have the SDK:
1311 * 2.1 _activate_plugin_event_hook() will not be executed, because
1312 * the activation hook do NOT fires on updates since WP 3.1.
1313 * 2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1314 * be empty, therefore, it will be set to FALSE.
1315 *
1316 * 3) Plugin update, had Freemius in prev version as well:
1317 * 3.1 _version_updates_handler() will be executed 1st, since FS was installed
1318 * before, $this->_storage->plugin_last_version will NOT be empty,
1319 * therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
1320 * 3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
1321 * already set, therefore, it will not be modified.
1322 *
1323 * Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
1324 *
1325 * NOTE:
1326 * The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
1327 * and then, the next immediate PageView is the plugin's main settings page, it will not
1328 * show the opt-in right away. The reason it will happen is because Freemius execution
1329 * will be turned off till the plugin is fully loaded at least once
1330 * (till $this->_storage->was_plugin_loaded is TRUE).
1331 *
1332 * @author Vova Feldman (@svovaf)
1333 * @since 1.1.9
1334 *
1335 */
1336 function _plugins_loaded() {
1337 // Update flag that plugin was loaded with Freemius at least once.
1338 $this->_storage->was_plugin_loaded = true;
1339
1340 /**
1341 * Bug fix - only set to false when it's a plugin, due to the
1342 * execution sequence of the theme hooks and our methods, if
1343 * this will be set for themes, Freemius will always assume
1344 * it's a theme update.
1345 *
1346 * @author Vova Feldman (@svovaf)
1347 * @since 1.2.2.2
1348 */
1349 if ( $this->is_plugin() &&
1350 ! isset( $this->_storage->is_plugin_new_install )
1351 ) {
1352 $this->_storage->is_plugin_new_install = (
1353 ! is_plugin_active( $this->_plugin_basename ) &&
1354 empty( $this->_storage->plugin_last_version )
1355 );
1356 }
1357 }
1358
1359 function _run_garbage_collector() {
1360 if ( true !== fs_get_optional_constant( 'WP_FS__ENABLE_GARBAGE_COLLECTOR', true ) ) {
1361 return;
1362 }
1363
1364 if ( ! $this->is_user_in_admin() ) {
1365 return;
1366 }
1367
1368 require_once WP_FS__DIR_INCLUDES . '/class-fs-lock.php';
1369
1370 $lock = new FS_Lock( 'garbage_collection' );
1371
1372 if ( $lock->is_locked() ) {
1373 return;
1374 }
1375
1376 // Create a 1-day lock.
1377 $lock->lock( WP_FS__TIME_24_HOURS_IN_SEC );
1378
1379 FS_Garbage_Collector::instance()->clean();
1380 }
1381
1382 /**
1383 * Opens the support forum subemenu item in a new browser page.
1384 *
1385 * @author Vova Feldman (@svovaf)
1386 * @since 2.1.4
1387 */
1388 static function _open_support_forum_in_new_page() {
1389 ?>
1390 <script type="text/javascript">
1391 (function ($) {
1392 $('.fs-submenu-item.wp-support-forum').parent().attr( { target: '_blank', rel: 'noopener noreferrer' } );
1393 })(jQuery);
1394 </script>
1395 <?php
1396 }
1397
1398 /**
1399 * @author Vova Feldman (@svovaf)
1400 * @since 1.0.9
1401 */
1402 private function register_constructor_hooks() {
1403 $this->_logger->entrance();
1404
1405 if ( is_admin() ) {
1406 add_action( 'admin_init', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
1407
1408 if ( $this->is_plugin() ) {
1409 if ( self::is_plugin_install_page() && true !== fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) {
1410 /**
1411 * Unless the `fs_allow_updater_and_dialog` URL param exists and its value is `true`, make
1412 * Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php)
1413 * so that they won't interfere with the .org plugins' functionalities on that page (e.g.
1414 * updating of a .org plugin).
1415 */
1416 add_filter( 'site_transient_update_plugins', array( 'Freemius', '_remove_fs_updates_from_plugin_install_page' ), 10, 2 );
1417 } else if ( self::is_plugins_page() || self::is_updates_page() ) {
1418 /**
1419 * On the "Plugins" and "Updates" admin pages, if there are premium or non–org-compliant plugins, modify their details dialog URLs (add a Freemius-specific param) so that the SDK can determine if the plugin information dialog should show information from Freemius.
1420 *
1421 * @author Leo Fajardo (@leorw)
1422 * @since 2.2.3
1423 */
1424 add_action( 'admin_footer', array( 'Freemius', '_prepend_fs_allow_updater_and_dialog_flag_url_param' ) );
1425 }
1426
1427 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
1428
1429 /**
1430 * @since 1.2.2
1431 *
1432 * Hook to both free and premium version activations to support
1433 * auto deactivation on the other version activation.
1434 */
1435 register_activation_hook(
1436 $plugin_dir . $this->_free_plugin_basename,
1437 array( &$this, '_activate_plugin_event_hook' )
1438 );
1439
1440 register_activation_hook(
1441 $plugin_dir . $this->premium_plugin_basename(),
1442 array( &$this, '_activate_plugin_event_hook' )
1443 );
1444 } else {
1445 add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
1446
1447 add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
1448 }
1449
1450 /**
1451 * Part of the mechanism to identify new plugin install vs. plugin update.
1452 *
1453 * @author Vova Feldman (@svovaf)
1454 * @since 1.1.9
1455 */
1456 if ( empty( $this->_storage->was_plugin_loaded ) ) {
1457 /**
1458 * During the plugin activation (not theme), 'plugins_loaded' will be already executed
1459 * when the logic gets here since the activation logic first add the activate plugins,
1460 * then triggers 'plugins_loaded', and only then include the code of the plugin that
1461 * is activated. Which means that _plugins_loaded() will NOT be executed during the
1462 * plugin activation, and that IS intentional.
1463 *
1464 * @author Vova Feldman (@svovaf)
1465 */
1466 if ( $this->is_plugin() &&
1467 $this->is_activation_mode( false ) &&
1468 0 == did_action( 'plugins_loaded' )
1469 ) {
1470 add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
1471 } else {
1472 // If was activated before, then it was already loaded before.
1473 $this->_plugins_loaded();
1474 }
1475 }
1476
1477 add_action( 'plugins_loaded', array( &$this, '_run_garbage_collector' ) );
1478
1479 if ( ! self::is_ajax() ) {
1480 if ( ! $this->is_addon() ) {
1481 add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
1482 }
1483 }
1484
1485 if ( $this->_storage->handle_gdpr_admin_notice ) {
1486 add_action( 'init', array( &$this, '_maybe_show_gdpr_admin_notice' ) );
1487 }
1488
1489 add_action( 'init', array( &$this, '_maybe_add_gdpr_optin_ajax_handler') );
1490 add_action( 'init', array( &$this, '_maybe_add_pricing_ajax_handler' ) );
1491 }
1492
1493 if ( $this->is_plugin() ) {
1494 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1495 add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
1496 } else {
1497 add_action( 'wp_initialize_site', array( $this, '_after_wp_initialize_site_callback' ), 11, 2 );
1498 }
1499
1500 register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
1501 }
1502
1503 if ( is_multisite() ) {
1504 add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
1505 add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) );
1506 add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) );
1507
1508 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1509 add_action( 'deleted_blog', array( $this, '_after_site_deleted_callback' ), 10, 2 );
1510 } else {
1511 add_action( 'wp_delete_site', array( $this, '_after_wpsite_deleted_callback' ) );
1512 }
1513
1514 add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) );
1515 add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) );
1516 add_action( 'make_ham_blog', array( &$this, '_after_site_reactivated_callback' ) );
1517 }
1518
1519 if ( $this->is_theme() &&
1520 self::is_customizer() &&
1521 $this->apply_filters( 'show_customizer_upsell', true )
1522 ) {
1523 // Register customizer upsell.
1524 add_action( 'customize_register', array( &$this, '_customizer_register' ) );
1525 }
1526
1527 add_action( 'admin_init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
1528
1529 if ( $this->is_theme() && ! $this->is_migration() ) {
1530 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1531 }
1532
1533 add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
1534 add_action( 'admin_init', array( &$this, '_add_premium_version_upgrade_selection' ) );
1535 add_action( 'admin_init', array( &$this, '_add_beta_mode_update_handler' ) );
1536 add_action( 'admin_init', array( &$this, '_add_user_change_option' ) );
1537 add_action( 'admin_init', array( &$this, '_add_email_address_update_option' ) );
1538
1539 $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
1540 $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
1541 $this->add_ajax_action( 'set_data_debug_mode', array( &$this, '_set_data_debug_mode' ) );
1542 $this->add_ajax_action( 'toggle_whitelabel_mode', array( &$this, '_toggle_whitelabel_mode_ajax_handler' ) );
1543
1544 if ( $this->_is_network_active && fs_is_network_admin() ) {
1545 $this->add_ajax_action( 'network_activate', array( &$this, '_network_activate_ajax_action' ) );
1546 }
1547
1548 $this->add_ajax_action( 'install_premium_version', array(
1549 &$this,
1550 '_install_premium_version_ajax_action'
1551 ) );
1552
1553 $this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
1554
1555 $this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
1556
1557 $this->add_action( 'sdk_version_update', array( &$this, '_sdk_version_update' ), WP_FS__DEFAULT_PRIORITY, 2 );
1558
1559 $this->add_action(
1560 'plugin_version_update',
1561 array( &$this, '_after_version_update' ),
1562 WP_FS__DEFAULT_PRIORITY,
1563 2
1564 );
1565 $this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
1566
1567 add_action( 'admin_init', array( &$this, '_add_trial_notice' ) ); // @phpstan-ignore-line
1568 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) ); // @phpstan-ignore-line
1569 add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) );
1570
1571 /**
1572 * Handle request to reset anonymous mode for `get_reconnect_url()` or reset the pending activation mode.
1573 *
1574 * @author Vova Feldman (@svovaf)
1575 * @since 1.2.1.5
1576 */
1577 if (
1578 (
1579 fs_request_is_action( 'reset_anonymous_mode' ) ||
1580 fs_request_is_action( 'reset_pending_activation_mode' )
1581 ) &&
1582 $this->get_unique_affix() === fs_request_get_raw( 'fs_unique_affix' )
1583 ) {
1584 add_action( 'admin_init', array( &$this, 'connect_again' ) );
1585 }
1586 }
1587
1588 /**
1589 * Register the required hooks right after the settings parse is completed.
1590 *
1591 * @author Vova Feldman (@svovaf)
1592 * @since 2.3.1
1593 */
1594 private function register_after_settings_parse_hooks() {
1595 if ( is_admin() &&
1596 $this->is_theme() &&
1597 $this->is_premium() &&
1598 ! $this->has_active_valid_license()
1599 ) {
1600 $this->add_ajax_action(
1601 'delete_theme_update_data',
1602 array( &$this, '_delete_theme_update_data_action' )
1603 );
1604 }
1605
1606 if ( $this->show_settings_with_tabs() ) {
1607 /**
1608 * Include the required hooks to capture the theme settings' page tabs
1609 * and cache them.
1610 *
1611 * @author Vova Feldman (@svovaf)
1612 * @since 1.2.2.7
1613 */
1614 if ( ! $this->_cache->has_valid( 'tabs' ) ) {
1615 add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
1616 // Add license activation AJAX callback.
1617 $this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
1618
1619 add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
1620 }
1621
1622 add_action(
1623 'admin_footer',
1624 array( &$this, '_add_freemius_tabs' ),
1625 /**
1626 * The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
1627 * That's why the priority is 11 while the tabs capture logic is added
1628 * with priority 10.
1629 *
1630 * @author Vova Feldman (@svovaf)
1631 */
1632 11
1633 );
1634 }
1635
1636 if ( ! self::is_ajax() ) {
1637 if ( ! $this->is_addon() || $this->is_only_premium() ) {
1638 add_action(
1639 ( $this->_is_network_active && fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu',
1640 array( &$this, '_prepare_admin_menu' ),
1641 WP_FS__LOWEST_PRIORITY
1642 );
1643 }
1644 }
1645 }
1646
1647 /**
1648 * Makes Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php) so that
1649 * they won't interfere with the .org plugins' functionalities on that page (e.g. updating of a .org plugin).
1650 *
1651 * @author Leo Fajardo (@leorw)
1652 * @since 2.2.3
1653 *
1654 * @param object $updates
1655 * @param string|null $transient
1656 *
1657 * @return object
1658 */
1659 static function _remove_fs_updates_from_plugin_install_page( $updates, $transient = null ) {
1660 if ( is_object( $updates ) && isset( $updates->response ) ) {
1661 foreach ( $updates->response as $file => $plugin ) {
1662 if ( isset( $plugin->package ) && false !== strpos( $plugin->package, 'api.freemius' ) ) {
1663 unset( $updates->response[ $file ] );
1664 }
1665 }
1666 }
1667
1668 return $updates;
1669 }
1670
1671 /**
1672 * Prepends the `fs_allow_updater_and_dialog` param to the plugin information URLs to tell the SDK to handle
1673 * the information that is shown on the plugin details dialog that is shown when the relevant link is clicked.
1674 *
1675 * @author Leo Fajardo (@leorw)
1676 * @since 2.2.3
1677 *
1678 * @return void
1679 */
1680 static function _prepend_fs_allow_updater_and_dialog_flag_url_param() {
1681 $slug_basename_map = array();
1682 foreach ( self::$_instances as $instance ) {
1683 if ( ! $instance->is_plugin() ) {
1684 continue;
1685 }
1686
1687 $slug_basename_map[ $instance->get_slug() ] = $instance->premium_plugin_basename();
1688 }
1689 ?>
1690 <script type="text/javascript">
1691 (function( $ ) {
1692 var slugBasenameMap = <?php echo json_encode( $slug_basename_map ) ?>;
1693 for ( var slug in slugBasenameMap ) {
1694 var basename = slugBasenameMap[ slug ];
1695
1696 // Try to get the plugin rows if on the "Plugins" page.
1697 var $pluginRows = $( '.wp-list-table.plugins tr[data-plugin="' + basename + '"]');
1698
1699 if ( 0 === $pluginRows.length ) {
1700 // Try to get the plugin rows if on the "Updates" page.
1701 var $pluginCheckbox = $( '#update-plugins-table input[type="checkbox"][value="' + basename + '"]' );
1702 if ( 0 !== $pluginCheckbox.length ) {
1703 $pluginRows = $pluginCheckbox.parents( 'tr:first' );
1704 }
1705 }
1706
1707 if ( 0 === $pluginRows.length ) {
1708 // No plugin rows found.
1709 continue;
1710 }
1711
1712 // Find the "View details" links and add the `fs_allow_updater_and_dialog` param to the URL.
1713 $pluginRows.find( 'a[href*="plugin-install.php?tab=plugin-information"]' ).each(function() {
1714 var $this = $( this ),
1715 href = $this.attr( 'href' ).replace( '?tab=', '?fs_allow_updater_and_dialog=true&tab=');
1716
1717 $this.attr( 'href', href );
1718 });
1719 }
1720 })( jQuery );
1721 </script>
1722 <?php
1723 }
1724
1725 /**
1726 * @author Leo Fajardo (@leorw)
1727 * @since 2.3.0
1728 */
1729 static function _maybe_add_beta_label_styles() {
1730 $has_any_beta_version = false;
1731
1732 foreach ( self::$_instances as $instance ) {
1733 if ( $instance->is_beta() ) {
1734 $has_any_beta_version = true;
1735 break;
1736 }
1737 }
1738
1739 if ( $has_any_beta_version ) {
1740 fs_enqueue_local_style( 'fs_plugins', '/admin/plugins.css' );
1741 }
1742 }
1743
1744 /**
1745 * @author Leo Fajardo (@leorw)
1746 * @since 2.3.0
1747 */
1748 static function _maybe_add_beta_label_to_plugins_and_handle_confirmation() {
1749 $beta_data = array();
1750
1751 foreach ( self::$_instances as $instance ) {
1752 if ( ! $instance->is_premium() ) {
1753 continue;
1754 }
1755
1756 /**
1757 * If there's an available beta version update, a confirmation message will be shown when the
1758 * "Update now" link on the "Plugins" or "Themes" page is clicked.
1759 */
1760 $has_beta_update = $instance->has_beta_update();
1761
1762 $is_beta = (
1763 // The "Beta" label is added separately for themes.
1764 $instance->is_plugin() &&
1765 $instance->is_beta()
1766 );
1767
1768 if ( ! $is_beta && ! $has_beta_update ) {
1769 continue;
1770 }
1771
1772 $beta_data[ $instance->get_plugin_basename() ] = array( 'is_installed_version_beta' => $is_beta );
1773
1774 if ( ! $has_beta_update ) {
1775 continue;
1776 }
1777
1778 $beta_data[ $instance->get_plugin_basename() ]['beta_version_update_confirmation_message'] = sprintf(
1779 '%s %s',
1780 sprintf(
1781 fs_esc_attr_inline(
1782 'An update to a Beta version will replace your installed version of %s with the latest Beta release - use with caution, and not on production sites. You have been warned.',
1783 'beta-version-update-caution',
1784 $instance->get_slug()
1785 ),
1786 $instance->get_plugin_title()
1787 ),
1788 fs_esc_attr_inline( 'Would you like to proceed with the update?', 'update-confirmation', $instance->get_slug() )
1789 );
1790 }
1791
1792 if ( empty( $beta_data ) ) {
1793 return;
1794 }
1795 ?>
1796 <script type="text/javascript">
1797 ( function( $ ) {
1798 var betaData = <?php echo json_encode( $beta_data ) ?>;
1799
1800 for ( var pluginBasename in betaData ) {
1801 if ( ! betaData.hasOwnProperty( pluginBasename ) ) {
1802 continue;
1803 }
1804
1805 if ( ! betaData[ pluginBasename ].is_installed_version_beta ) {
1806 continue;
1807 }
1808
1809 var $parentContainer = $( '.wp-list-table.plugins tr[data-plugin="' + pluginBasename + '"]' );
1810 if ( 0 === $parentContainer.length ) {
1811 continue;
1812 }
1813
1814 $parentContainer.find( '.plugin-title > strong:first-child').append(
1815 '<span class="fs-tag fs-info"><?php fs_esc_js_echo_inline( 'Beta', 'beta' ) ?></span>'
1816 );
1817 }
1818
1819 setTimeout( function() {
1820 // Wait a little bit before adding the event handler, otherwise, it will be overridden by the core WP logic.
1821 $( '.plugins .update-message .update-link, .themes .theme .update-message' ).on( 'click', function() {
1822 var $parentContainer = $( this ).parents( 'tr:first' );
1823 pluginBasename = ( 0 !== $parentContainer.length ) ?
1824 $parentContainer.data( 'plugin' ) :
1825 $( this ).parents( '.theme:first' ).data( 'slug' );
1826
1827 if (
1828 betaData[ pluginBasename ] &&
1829 betaData[ pluginBasename ].beta_version_update_confirmation_message &&
1830 ! confirm( betaData[ pluginBasename ].beta_version_update_confirmation_message )
1831 ) {
1832 return false;
1833 }
1834 } );
1835 }, 20 );
1836 } )( jQuery );
1837 </script>
1838 <?php
1839 }
1840
1841 /**
1842 * Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
1843 * could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
1844 * plugin will trigger inclusion of the free or premium version and if one of them is active during the
1845 * uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
1846 *
1847 * @author Leo Fajardo (@leorw)
1848 *
1849 * @since 1.2.0
1850 */
1851 private function unregister_uninstall_hook() {
1852 $uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
1853 unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
1854 unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
1855
1856 update_option( 'uninstall_plugins', $uninstallable_plugins );
1857 }
1858
1859 /**
1860 * @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
1861 *
1862 * @param bool $store_prev_path
1863 */
1864 private function clear_module_main_file_cache( $store_prev_path = true ) {
1865 if ( ! isset( $this->_storage->plugin_main_file ) ||
1866 empty( $this->_storage->plugin_main_file->path )
1867 ) {
1868 return;
1869 }
1870
1871 if ( ! $store_prev_path ) {
1872 /**
1873 * Storing the previous path is not needed when clearing the cache after an SDK version update since
1874 * the main purpose of the cache clearing in that event is to correct a wrong plugin main file path
1875 * which causes data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
1876 *
1877 * @author Leo Fajardo (@leorw)
1878 * @since 2.2.1
1879 */
1880 unset( $this->_storage->plugin_main_file->path );
1881 } else {
1882 $plugin_main_file = clone $this->_storage->plugin_main_file;
1883
1884 // Store cached path (2nd layer cache).
1885 $plugin_main_file->prev_path = $plugin_main_file->path;
1886
1887 // Clear cached path.
1888 unset( $plugin_main_file->path );
1889
1890 $this->_storage->plugin_main_file = $plugin_main_file;
1891 }
1892
1893 /**
1894 * Clear global cached path.
1895 *
1896 * @author Leo Fajardo (@leorw)
1897 * @since 1.2.2
1898 */
1899 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
1900 unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
1901 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
1902 }
1903
1904 /**
1905 * @author Leo Fajardo (@leorw)
1906 * @since 2.0.0
1907 */
1908 function _hook_action_links_and_register_account_hooks() {
1909 if ( $this->is_migration() ) {
1910 return;
1911 }
1912
1913 if (
1914 ( self::is_plugins_page() && $this->is_plugin() ) ||
1915 ( self::is_themes_page() && $this->is_theme() ) ||
1916 fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' )
1917 ) {
1918 $this->_add_tracking_links();
1919 }
1920
1921 if ( self::is_plugins_page() && $this->is_plugin() ) {
1922 $this->hook_plugin_action_links();
1923 }
1924
1925 $this->_register_account_hooks();
1926 }
1927
1928 /**
1929 * @author Vova Feldman (@svovaf)
1930 * @since 1.0.9
1931 */
1932 private function _register_account_hooks() {
1933 if ( ! is_admin() ) {
1934 return;
1935 }
1936
1937 /**
1938 * Always show the deactivation feedback form since we added
1939 * automatic free version deactivation upon premium code activation.
1940 *
1941 * @since 1.2.1.6
1942 */
1943 $this->add_ajax_action(
1944 'submit_uninstall_reason',
1945 array( &$this, '_submit_uninstall_reason_action' )
1946 );
1947
1948 $this->add_ajax_action(
1949 'cancel_subscription_or_trial',
1950 array( &$this, 'cancel_subscription_or_trial_ajax_action' )
1951 );
1952
1953 if ( ! $this->is_addon() || $this->is_parent_plugin_installed() ) {
1954 if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
1955 ( $this->is_theme() && self::is_themes_page() )
1956 ) {
1957 add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
1958 }
1959 }
1960 }
1961
1962 /**
1963 * Leverage backtrace to find caller plugin file path.
1964 *
1965 * @param bool $is_init Is initiation sequence.
1966 * @param string $main_file Since 2.5.0 expects the module's main file path to potentially purge the cached path.
1967 *
1968 * @return string
1969 * @since 1.0.6
1970 *
1971 * @author Vova Feldman (@svovaf)
1972 */
1973 private function _find_caller_plugin_file( $is_init = false, $main_file = '' ) {
1974 // Try to load the cached value of the file path.
1975 if ( isset( $this->_storage->plugin_main_file ) ) {
1976 $plugin_main_file = $this->_storage->plugin_main_file;
1977 if ( ! empty( $plugin_main_file->path ) ) {
1978 $absolute_path = $this->get_absolute_path( $plugin_main_file->path );
1979 if ( file_exists( $absolute_path ) ) {
1980 if ( $is_init && $absolute_path !== $this->get_absolute_path( $main_file ) ) {
1981 // Update cached path if not matching the actual path.
1982 $plugin_main_file->path = $main_file;
1983 $this->_storage->plugin_main_file = $plugin_main_file;
1984 }
1985
1986 return $absolute_path;
1987 }
1988 }
1989 }
1990
1991 /**
1992 * @since 1.2.1
1993 *
1994 * `clear_module_main_file_cache()` is clearing the plugin's cached path on
1995 * deactivation. Therefore, if any plugin/theme was initiating `Freemius`
1996 * with that plugin's slug, it was overriding the empty plugin path with a wrong path.
1997 *
1998 * So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
1999 * when the class instantiator isn't the module.
2000 */
2001 if ( ! $is_init ) {
2002 // Fetch prev path cache.
2003 if ( isset( $this->_storage->plugin_main_file ) &&
2004 ! empty( $this->_storage->plugin_main_file->prev_path )
2005 ) {
2006 $absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
2007 if ( file_exists( $absolute_path ) ) {
2008 return $absolute_path;
2009 }
2010 }
2011
2012 wp_die(
2013 $this->get_text_inline( 'Freemius SDK couldn\'t find the plugin\'s main file. Please contact sdk@freemius.com with the current error.', 'failed-finding-main-path' ) .
2014 " Module: {$this->_slug}; SDK: " . WP_FS__SDK_VERSION . ";",
2015 $this->get_text_inline( 'Error', 'error' ),
2016 array( 'back_link' => true )
2017 );
2018 }
2019
2020 /**
2021 * @since 1.2.1
2022 *
2023 * Only the original instantiator that calls dynamic_init can modify the module's path.
2024 */
2025 // Find caller module.
2026 $this->_storage->plugin_main_file = (object) array(
2027 'path' => $main_file,
2028 );
2029
2030 return $this->get_absolute_path( $main_file );
2031 }
2032
2033 /**
2034 * @author Leo Fajardo (@leorw)
2035 * @since 1.2.3
2036 *
2037 * @param string $path
2038 *
2039 * @return string
2040 */
2041 private function get_relative_path( $path ) {
2042 $module_root_dir = $this->get_module_root_dir_path();
2043 if ( 0 === strpos( $path, $module_root_dir ) ) {
2044 $path = substr( $path, strlen( $module_root_dir ) );
2045 }
2046
2047 return $path;
2048 }
2049
2050 /**
2051 * @author Leo Fajardo (@leorw)
2052 * @since 1.2.3
2053 *
2054 * @param string $path
2055 * @param string|bool $module_type
2056 *
2057 * @return string
2058 */
2059 private function get_absolute_path( $path, $module_type = false ) {
2060 $module_root_dir = $this->get_module_root_dir_path( $module_type );
2061 if ( 0 !== strpos( $path, $module_root_dir ) ) {
2062 $path = fs_normalize_path( $module_root_dir . $path );
2063 }
2064
2065 return $path;
2066 }
2067
2068 /**
2069 * @author Leo Fajardo (@leorw)
2070 * @since 1.2.3
2071 *
2072 * @param string|bool $module_type
2073 *
2074 * @return string
2075 */
2076 private function get_module_root_dir_path( $module_type = false ) {
2077 $is_plugin = empty( $module_type ) ?
2078 $this->is_plugin() :
2079 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
2080
2081 return fs_normalize_path( trailingslashit( $is_plugin ?
2082 WP_PLUGIN_DIR :
2083 get_theme_root( get_stylesheet() ) ) );
2084 }
2085
2086 /**
2087 * @author Leo Fajardo (@leorw)
2088 *
2089 * @param number $module_id
2090 * @param string $slug
2091 *
2092 * @return string Since 2.5.0 return the module's main file path.
2093 *
2094 * @since 1.2.2
2095 */
2096 private function store_id_slug_type_path_map( $module_id, $slug ) {
2097 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
2098
2099 $store_option = false;
2100
2101 if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
2102 $id_slug_type_path_map[ $module_id ] = array(
2103 'slug' => $slug
2104 );
2105
2106 $store_option = true;
2107 } else if (
2108 isset( $id_slug_type_path_map[ $module_id ]['slug'] ) &&
2109 $slug !== $id_slug_type_path_map[ $module_id ]['slug']
2110 ) {
2111 $id_slug_type_path_map[ $module_id ]['slug'] = $slug;
2112 $store_option = true;
2113 }
2114
2115 $find_caller = empty( $id_slug_type_path_map[ $module_id ]['path'] );
2116
2117 if ( ! $find_caller ) {
2118 /**
2119 * This verification is for cases when suddenly the same module
2120 * is installed but with a different folder name.
2121 *
2122 * @author Vova Feldman (@svovaf)
2123 * @since 1.2.3
2124 */
2125 $find_caller = ! file_exists( $this->get_absolute_path(
2126 $id_slug_type_path_map[ $module_id ]['path'],
2127 $id_slug_type_path_map[ $module_id ]['type']
2128 ) );
2129 }
2130
2131 foreach ( $id_slug_type_path_map as $id => $data ) {
2132 if ( empty( $id ) ) {
2133 // Remove maps with empty module ID.
2134 unset( $id_slug_type_path_map[ $id ] );
2135 $store_option = true;
2136 continue;
2137 }
2138
2139 /**
2140 * If the module's main file path is identical to the main file path of another module then it means that the cached path of the current module or the other one with the same path is wrong, and therefore, we need to recalculate those paths.
2141 *
2142 * @author Vova Feldman (@svovaf)
2143 * @since 2.5.0
2144 */
2145 if ( ! $find_caller ) {
2146 if ( $id == $module_id ) {
2147 continue;
2148 }
2149
2150 if (
2151 isset( $data['path'] ) &&
2152 $data['path'] === $id_slug_type_path_map[ $module_id ]['path']
2153 ) {
2154 $find_caller = true;
2155 }
2156 }
2157 }
2158
2159 if ( $find_caller ) {
2160 $caller_main_file_and_type = $this->get_caller_main_file_and_type( $module_id );
2161
2162 $id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type;
2163 $id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path;
2164
2165 $store_option = true;
2166 }
2167
2168 if ( $store_option ) {
2169 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
2170 }
2171
2172 return $id_slug_type_path_map[ $module_id ]['path'];
2173 }
2174
2175 /**
2176 * Identifies the caller type: plugin or theme.
2177 *
2178 * @author Leo Fajardo (@leorw)
2179 * @since 1.2.2
2180 *
2181 * @author Vova Feldman (@svovaf)
2182 * @since 1.2.2.3 Find the earliest module in the call stack that calls to the SDK. This fix is for cases when
2183 * add-ons are relying on loading the SDK from the parent module, and also allows themes including the
2184 * SDK an internal file instead of directly from functions.php.
2185 * @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic.
2186 *
2187 * @param number $module_id @since 2.5.0
2188 */
2189 private function get_caller_main_file_and_type( $module_id ) {
2190 self::require_plugin_essentials();
2191
2192 $all_plugins = fs_get_plugins( true );
2193 $all_plugins_paths = array();
2194
2195 // Get active plugin's main files real full names (might be symlinks).
2196 foreach ( $all_plugins as $relative_path => $data ) {
2197 if ( false === strpos( fs_normalize_path( $relative_path ), '/' ) ) {
2198 /**
2199 * Ignore plugins that don't have a folder (e.g. Hello Dolly) since they
2200 * can't really include the SDK.
2201 *
2202 * @author Vova Feldman
2203 * @since 1.2.1.7
2204 */
2205 continue;
2206 }
2207
2208 $all_plugins_paths[] = fs_normalize_path( realpath( WP_PLUGIN_DIR . '/' . $relative_path ) );
2209 }
2210
2211 $caller_file_candidate = false;
2212 $caller_map = array();
2213 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2214 $themes_dir = fs_normalize_path( get_theme_root( get_stylesheet() ) );
2215 $plugin_dir_to_skip = false;
2216
2217 for ( $i = 1, $bt = debug_backtrace(), $len = count( $bt ); $i < $len; $i ++ ) {
2218 if ( empty( $bt[ $i ]['file'] ) ) {
2219 continue;
2220 }
2221
2222 if ( $i > 1 && ! empty( $bt[ $i - 1 ]['file'] ) && $bt[ $i ]['file'] === $bt[ $i - 1 ]['file'] ) {
2223 // If file same as the prev file in the stack, skip it.
2224 continue;
2225 }
2226
2227 if ( ! empty( $bt[ $i ]['function'] ) && in_array( $bt[ $i ]['function'], array(
2228 'do_action',
2229 'apply_filter',
2230 // The string split is stupid, but otherwise, theme check
2231 // throws info notices.
2232 'requir' . 'e_once',
2233 'requir' . 'e',
2234 'includ' . 'e_once',
2235 'includ' . 'e',
2236 'install_and_activate_plugin',
2237 'try_activate_plugin',
2238 'activate_plugin'
2239 ) )
2240 ) {
2241 if ( 'activate_plugin' === $bt[ $i ]['function'] ) {
2242 /**
2243 * Store the directory of the activator plugin so that any other file that starts with it
2244 * cannot be mistakenly chosen as a candidate caller file.
2245 *
2246 * @author Leo Fajardo
2247 *
2248 * @since 2.3.0
2249 */
2250 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2251
2252 foreach ( $all_plugins_paths as $plugin_path ) {
2253 $plugin_dir = fs_normalize_path( dirname( $plugin_path ) . '/' );
2254 if ( false !== strpos( $caller_file_path, $plugin_dir ) ) {
2255 $plugin_dir_to_skip = $plugin_dir;
2256
2257 break;
2258 }
2259 }
2260 }
2261
2262 // Ignore call stack hooks and files inclusion.
2263 continue;
2264 }
2265
2266 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2267
2268 if ( ! empty( $plugin_dir_to_skip ) ) {
2269 /**
2270 * Skip if it's an activator plugin file to avoid mistakenly choosing it as a candidate caller file.
2271 *
2272 * @author Leo Fajardo
2273 *
2274 * @since 2.3.0
2275 */
2276 if ( 0 === strpos( $caller_file_path, $plugin_dir_to_skip ) ) {
2277 continue;
2278 }
2279 }
2280
2281 if ( 'functions.php' === basename( $caller_file_path ) ) {
2282 /**
2283 * 1. Assumes that theme's starting execution file is functions.php.
2284 * 2. This complex logic fixes symlink issues (e.g. with Vargant).
2285 *
2286 * @author Vova Feldman (@svovaf)
2287 * @since 1.2.2.5
2288 */
2289
2290 if ( $caller_file_path == fs_normalize_path( realpath( trailingslashit( $themes_dir ) . basename( dirname( $caller_file_path ) ) . '/' . basename( $caller_file_path ) ) ) ) {
2291 $module_type = WP_FS__MODULE_TYPE_THEME;
2292
2293 /**
2294 * Relative path of the theme, e.g.:
2295 * `my-theme/functions.php`
2296 *
2297 * @author Leo Fajardo (@leorw)
2298 */
2299 $caller_file_candidate = basename( dirname( $caller_file_path ) ) .
2300 '/' .
2301 basename( $caller_file_path );
2302
2303 continue;
2304 }
2305 }
2306
2307 $caller_file_hash = md5( $caller_file_path );
2308
2309 if ( ! isset( $caller_map[ $caller_file_hash ] ) ) {
2310 foreach ( $all_plugins_paths as $plugin_path ) {
2311 if ( empty( $plugin_path ) ) {
2312 continue;
2313 }
2314
2315 if ( false !== strpos( $caller_file_path, fs_normalize_path( dirname( $plugin_path ) . '/' ) ) ) {
2316 $caller_map[ $caller_file_hash ] = fs_normalize_path( $plugin_path );
2317 break;
2318 }
2319 }
2320 }
2321
2322 if ( isset( $caller_map[ $caller_file_hash ] ) ) {
2323 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2324 $caller_file_candidate = plugin_basename( $caller_map[ $caller_file_hash ] );
2325 }
2326 }
2327
2328 $caller_main_file_and_type = (object) array(
2329 'module_type' => $module_type,
2330 'path' => $caller_file_candidate
2331 );
2332
2333 return apply_filters( "fs_{$module_id}_caller_main_file_and_type", $caller_main_file_and_type );
2334 }
2335
2336 #----------------------------------------------------------------------------------
2337 #region Deactivation Feedback Form
2338 #----------------------------------------------------------------------------------
2339
2340 /**
2341 * Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
2342 * page.
2343 *
2344 * @author Vova Feldman (@svovaf)
2345 * @author Leo Fajardo (@leorw)
2346 *
2347 * @since 1.1.2
2348 */
2349 function _add_deactivation_feedback_dialog_box() {
2350 if (
2351 $this->is_clone() ||
2352 ( is_object( $this->_site ) && ! $this->is_registered() )
2353 ) {
2354 return;
2355 }
2356
2357 $subscription_cancellation_dialog_box_template_params = $this->apply_filters( 'show_deactivation_subscription_cancellation', true ) ?
2358 $this->_get_subscription_cancellation_dialog_box_template_params() :
2359 array();
2360
2361 /**
2362 * @since 2.3.0 Developers can optionally hide the deactivation feedback form using the 'show_deactivation_feedback_form' filter.
2363 */
2364 $show_deactivation_feedback_form = ! self::is_deactivation_snoozed();
2365 if ( $this->has_filter( 'show_deactivation_feedback_form' ) ) {
2366 $show_deactivation_feedback_form = $this->apply_filters( 'show_deactivation_feedback_form', true );
2367 } else if ( $this->is_addon() ) {
2368 /**
2369 * If the add-on's 'show_deactivation_feedback_form' is not set, try to inherit the value from the parent.
2370 */
2371 $show_deactivation_feedback_form = $this->get_parent_instance()->apply_filters( 'show_deactivation_feedback_form', true );
2372 }
2373
2374 $uninstall_confirmation_message = $this->apply_filters( 'uninstall_confirmation_message', '' );
2375
2376 if (
2377 empty( $subscription_cancellation_dialog_box_template_params ) &&
2378 ! $show_deactivation_feedback_form &&
2379 empty( $uninstall_confirmation_message )
2380 ) {
2381 return;
2382 }
2383
2384 $vars = array( 'id' => $this->_module_id );
2385
2386 if ( $show_deactivation_feedback_form ) {
2387 /* Check the type of user:
2388 * 1. Long-term (long-term)
2389 * 2. Non-registered and non-anonymous short-term (non-registered-and-non-anonymous-short-term).
2390 * 3. Short-term (short-term)
2391 */
2392 $is_long_term_user = true;
2393
2394 // Check if the site is at least 2 days old.
2395 $time_installed = $this->_storage->install_timestamp;
2396
2397 // Difference in seconds.
2398 $date_diff = time() - $time_installed;
2399
2400 // Convert seconds to days.
2401 $date_diff_days = floor( $date_diff / ( 60 * 60 * 24 ) );
2402
2403 if ( $date_diff_days < 2 ) {
2404 $is_long_term_user = false;
2405 }
2406
2407 $is_long_term_user = $this->apply_filters( 'is_long_term_user', $is_long_term_user );
2408
2409 if ( $is_long_term_user ) {
2410 $user_type = 'long-term';
2411 } else {
2412 if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
2413 $user_type = 'non-registered-and-non-anonymous-short-term';
2414 } else {
2415 $user_type = 'short-term';
2416 }
2417 }
2418
2419 $uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
2420
2421 $vars['reasons'] = $uninstall_reasons;
2422 }
2423
2424 $vars['subscription_cancellation_dialog_box_template_params'] = &$subscription_cancellation_dialog_box_template_params;
2425 $vars['show_deactivation_feedback_form'] = $show_deactivation_feedback_form;
2426 $vars['uninstall_confirmation_message'] = $uninstall_confirmation_message;
2427
2428 /**
2429 * Load the HTML template for the deactivation feedback dialog box.
2430 *
2431 * @todo Deactivation form core functions should be loaded only once! Otherwise, when there are multiple Freemius powered plugins the same code is loaded multiple times. The only thing that should be loaded differently is the various deactivation reasons object based on the state of the plugin.
2432 */
2433 fs_require_template( 'forms/deactivation/form.php', $vars );
2434 }
2435
2436 /**
2437 * @author Leo Fajardo (@leorw)
2438 * @since 1.1.2
2439 *
2440 * @param string $user_type
2441 *
2442 * @return array The uninstall reasons for the specified user type.
2443 */
2444 function _get_uninstall_reasons( $user_type = 'long-term' ) {
2445 $module_type = $this->_module_type;
2446
2447 $internal_message_template_var = array(
2448 'id' => $this->_module_id
2449 );
2450
2451 $plan = $this->get_plan();
2452
2453 if ( $this->is_registered() && is_object( $plan ) && $plan->has_technical_support() ) {
2454 $contact_support_template = fs_get_template( 'forms/deactivation/contact.php', $internal_message_template_var );
2455 } else {
2456 $contact_support_template = '';
2457 }
2458
2459 $reason_found_better_plugin = array(
2460 'id' => self::REASON_FOUND_A_BETTER_PLUGIN,
2461 'text' => sprintf( $this->get_text_inline( 'I found a better %s', 'reason-found-a-better-plugin' ), $module_type ),
2462 'input_type' => 'textfield',
2463 'input_placeholder' => sprintf( $this->get_text_inline( "What's the %s's name?", 'placeholder-plugin-name' ), $module_type ),
2464 );
2465
2466 $reason_temporary_deactivation = array(
2467 'id' => self::REASON_TEMPORARY_DEACTIVATION,
2468 'text' => sprintf(
2469 $this->get_text_inline( "It's a temporary %s - I'm troubleshooting an issue", 'reason-temporary-x' ),
2470 strtolower( $this->is_plugin() ?
2471 $this->get_text_inline( 'Deactivation', 'deactivation' ) :
2472 $this->get_text_inline( 'Theme Switch', 'theme-switch' )
2473 )
2474 ),
2475 'input_type' => '',
2476 'input_placeholder' => ''
2477 );
2478
2479 $reason_other = array(
2480 'id' => self::REASON_OTHER,
2481 'text' => $this->get_text_inline( 'Other', 'reason-other' ),
2482 'input_type' => 'textfield',
2483 'input_placeholder' => ''
2484 );
2485
2486 $long_term_user_reasons = array(
2487 array(
2488 'id' => self::REASON_NO_LONGER_NEEDED,
2489 'text' => sprintf( $this->get_text_inline( 'I no longer need the %s', 'reason-no-longer-needed' ), $module_type ),
2490 'input_type' => '',
2491 'input_placeholder' => ''
2492 ),
2493 $reason_found_better_plugin,
2494 array(
2495 'id' => self::REASON_NEEDED_FOR_A_SHORT_PERIOD,
2496 'text' => sprintf( $this->get_text_inline( 'I only needed the %s for a short period', 'reason-needed-for-a-short-period' ), $module_type ),
2497 'input_type' => '',
2498 'input_placeholder' => ''
2499 ),
2500 array(
2501 'id' => self::REASON_BROKE_MY_SITE,
2502 'text' => sprintf( $this->get_text_inline( 'The %s broke my site', 'reason-broke-my-site' ), $module_type ),
2503 'input_type' => '',
2504 'input_placeholder' => '',
2505 'internal_message' => $contact_support_template
2506 ),
2507 array(
2508 'id' => self::REASON_SUDDENLY_STOPPED_WORKING,
2509 'text' => sprintf( $this->get_text_inline( 'The %s suddenly stopped working', 'reason-suddenly-stopped-working' ), $module_type ),
2510 'input_type' => '',
2511 'input_placeholder' => '',
2512 'internal_message' => $contact_support_template
2513 )
2514 );
2515
2516 if ( $this->is_paying() ) {
2517 $long_term_user_reasons[] = array(
2518 'id' => self::REASON_CANT_PAY_ANYMORE,
2519 'text' => $this->get_text_inline( "I can't pay for it anymore", 'reason-cant-pay-anymore' ),
2520 'input_type' => 'textfield',
2521 'input_placeholder' => $this->get_text_inline( 'What price would you feel comfortable paying?', 'placeholder-comfortable-price' )
2522 );
2523 }
2524
2525 $reason_dont_share_info = array(
2526 'id' => self::REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION,
2527 'text' => $this->get_text_inline( "I don't like to share my information with you", 'reason-dont-like-to-share-my-information' ),
2528 'input_type' => '',
2529 'input_placeholder' => ''
2530 );
2531
2532 /**
2533 * If the current user has selected the "don't share data" reason in the deactivation feedback modal, inform the
2534 * user by showing additional message that he doesn't have to share data and can just choose to skip the opt-in
2535 * (the Skip button is included in the message to show). This message will only be shown if anonymous mode is
2536 * enabled and the user's account is currently not in pending activation state (similar to the way the Skip
2537 * button in the opt-in form is shown/hidden).
2538 */
2539 if ( $this->is_enable_anonymous() && ! $this->is_pending_activation() ) {
2540 $reason_dont_share_info['internal_message'] = fs_get_template( 'forms/deactivation/retry-skip.php', $internal_message_template_var );
2541 }
2542
2543 $uninstall_reasons = array(
2544 'long-term' => $long_term_user_reasons,
2545 'non-registered-and-non-anonymous-short-term' => array(
2546 array(
2547 'id' => self::REASON_DIDNT_WORK,
2548 'text' => sprintf( $this->get_text_inline( "The %s didn't work", 'reason-didnt-work' ), $module_type ),
2549 'input_type' => '',
2550 'input_placeholder' => ''
2551 ),
2552 $reason_dont_share_info,
2553 $reason_found_better_plugin
2554 ),
2555 'short-term' => array(
2556 array(
2557 'id' => self::REASON_COULDNT_MAKE_IT_WORK,
2558 'text' => $this->get_text_inline( "I couldn't understand how to make it work", 'reason-couldnt-make-it-work' ),
2559 'input_type' => '',
2560 'input_placeholder' => '',
2561 'internal_message' => $contact_support_template
2562 ),
2563 $reason_found_better_plugin,
2564 array(
2565 'id' => self::REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE,
2566 'text' => sprintf( $this->get_text_inline( "The %s is great, but I need specific feature that you don't support", 'reason-great-but-need-specific-feature' ), $module_type ),
2567 'input_type' => 'textarea',
2568 'input_placeholder' => $this->get_text_inline( 'What feature?', 'placeholder-feature' )
2569 ),
2570 array(
2571 'id' => self::REASON_NOT_WORKING,
2572 'text' => sprintf( $this->get_text_inline( 'The %s is not working', 'reason-not-working' ), $module_type ),
2573 'input_type' => 'textarea',
2574 'input_placeholder' => $this->get_text_inline( "Kindly share what didn't work so we can fix it for future users...", 'placeholder-share-what-didnt-work' )
2575 ),
2576 array(
2577 'id' => self::REASON_NOT_WHAT_I_WAS_LOOKING_FOR,
2578 'text' => $this->get_text_inline( "It's not what I was looking for", 'reason-not-what-i-was-looking-for' ),
2579 'input_type' => 'textarea',
2580 'input_placeholder' => $this->get_text_inline( "What you've been looking for?", 'placeholder-what-youve-been-looking-for' )
2581 ),
2582 array(
2583 'id' => self::REASON_DIDNT_WORK_AS_EXPECTED,
2584 'text' => sprintf( $this->get_text_inline( "The %s didn't work as expected", 'reason-didnt-work-as-expected' ), $module_type ),
2585 'input_type' => 'textarea',
2586 'input_placeholder' => $this->get_text_inline( 'What did you expect?', 'placeholder-what-did-you-expect' )
2587 )
2588 )
2589 );
2590
2591 // Randomize the reasons for the current user type.
2592 shuffle( $uninstall_reasons[ $user_type ] );
2593
2594 // Keep the following reasons as the last items in the list.
2595 $uninstall_reasons[ $user_type ][] = $reason_temporary_deactivation;
2596 $uninstall_reasons[ $user_type ][] = $reason_other;
2597
2598 $uninstall_reasons = $this->apply_filters( 'uninstall_reasons', $uninstall_reasons );
2599
2600 return $uninstall_reasons[ $user_type ];
2601 }
2602
2603 /**
2604 * Called after the user has submitted his reason for deactivating the plugin.
2605 *
2606 * @author Leo Fajardo (@leorw)
2607 * @since 1.1.2
2608 */
2609 function _submit_uninstall_reason_action() {
2610 $this->_logger->entrance();
2611
2612 $this->check_ajax_referer( 'submit_uninstall_reason' );
2613
2614 $reason_id = fs_request_get( 'reason_id' );
2615
2616 // Check if the given reason ID is an unsigned integer.
2617 if ( ! ctype_digit( $reason_id ) ) {
2618 exit;
2619 }
2620
2621 $reason_info = trim( fs_request_get( 'reason_info', '' ) );
2622 if ( ! empty( $reason_info ) ) {
2623 $reason_info = substr( $reason_info, 0, 128 );
2624 }
2625
2626 $reason = (object) array(
2627 'id' => $reason_id,
2628 'info' => $reason_info,
2629 'is_anonymous' => fs_request_get_bool( 'is_anonymous' )
2630 );
2631
2632 $this->_storage->store( 'uninstall_reason', $reason );
2633
2634 if ( self::REASON_TEMPORARY_DEACTIVATION == $reason->id ) {
2635 $snooze_period = fs_request_get( 'snooze_period' );
2636
2637 if ( is_numeric( $snooze_period ) && 0 < $snooze_period ) {
2638 self::snooze_deactivation_form( (int) $snooze_period );
2639 }
2640 }
2641
2642 /**
2643 * If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do
2644 * not support uninstall hook.
2645 *
2646 * @author Leo Fajardo (@leorw)
2647 * @since 1.2.2
2648 */
2649 if ( $this->is_theme() ) {
2650 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
2651 FS_Plugin_Updater::instance( $this )->delete_update_data();
2652 }
2653
2654 $this->_uninstall_plugin_event( false );
2655 $this->remove_sdk_reference();
2656 }
2657
2658 // Print '1' for successful operation.
2659 echo 1;
2660 exit;
2661 }
2662
2663 #--------------------------------------------------------------------------------
2664 #region Deactivation Feedback Snoozing
2665 #--------------------------------------------------------------------------------
2666
2667 /**
2668 * @author Vova Feldman (@svovaf)
2669 * @since 2.4.3
2670 *
2671 * @param int $period
2672 *
2673 * @return bool True if the value was set, false otherwise.
2674 */
2675 private static function snooze_deactivation_form( $period ) {
2676 return ( 0 < $period && self::reset_deactivation_snoozing( $period ) );
2677 }
2678
2679 /**
2680 * Check if deactivation feedback form is snoozed.
2681 *
2682 * @author Vova Feldman (@svovaf)
2683 * @since 2.4.3
2684 *
2685 * @return bool
2686 */
2687 static function is_deactivation_snoozed() {
2688 $is_snoozed = ( ! is_multisite() || fs_is_network_admin() ) ?
2689 get_transient( 'fs_snooze_period' ) :
2690 get_site_transient( 'fs_snooze_period' );
2691
2692
2693 return ( 'true' === $is_snoozed );
2694 }
2695
2696 /**
2697 * Reset deactivation snoozing. When `$period` is `0` will stop deactivation snoozing by deleting the transients. Otherwise, will set the transients for the selected period.
2698 *
2699 * @param int $period Period in seconds.
2700 *
2701 * @author Vova Feldman (@svovaf)
2702 * @since 2.4.3
2703 */
2704 private static function reset_deactivation_snoozing( $period = 0 ) {
2705 $value = ( 0 === $period ) ? null : 'true';
2706
2707 if ( ! is_multisite() || fs_is_network_admin() ) {
2708 return set_transient( 'fs_snooze_period', $value, $period );
2709 } else {
2710 return set_site_transient( 'fs_snooze_period', $value, $period );
2711 }
2712 }
2713
2714 /**
2715 * The deactivation snooze expiration UNIX timestamp (in sec).
2716 *
2717 * @author Vova Feldman (@svovaf)
2718 * @since 2.4.3
2719 *
2720 * @return int
2721 */
2722 static function deactivation_snooze_expires_at() {
2723 return ( ! is_multisite() || fs_is_network_admin() ) ?
2724 (int) get_option( '_transient_timeout_fs_snooze_period' ) :
2725 (int) get_site_option( '_site_transient_timeout_fs_snooze_period' );
2726 }
2727
2728 #endregion
2729
2730 /**
2731 * @author Leo Fajardo (@leorw)
2732 * @since 2.1.4
2733 */
2734 function cancel_subscription_or_trial_ajax_action() {
2735 $this->_logger->entrance();
2736
2737 $this->check_ajax_referer( 'cancel_subscription_or_trial' );
2738
2739 $result = $this->cancel_subscription_or_trial( fs_request_get( 'plugin_id', $this->get_id() ), false );
2740
2741 if ( $this->is_api_error( $result ) ) {
2742 $this->shoot_ajax_failure( $result->error->message );
2743 }
2744
2745 $this->shoot_ajax_success();
2746 }
2747
2748 /**
2749 * @author Leo Fajardo (@leorw)
2750 * @since 2.1.4
2751 *
2752 * @param number $plugin_id
2753 *
2754 * @return object
2755 */
2756 private function cancel_subscription_or_trial( $plugin_id ) {
2757 $fs = null;
2758 if ( $plugin_id == $this->get_id() ) {
2759 $fs = $this;
2760 } else if ( $this->is_addon_activated( $plugin_id ) ) {
2761 $fs = self::get_instance_by_id( $plugin_id );
2762 }
2763
2764 $result = null;
2765
2766 if ( ! is_null( $fs ) ) {
2767 $result = $fs->is_paid_trial() ?
2768 $fs->_cancel_trial() :
2769 $fs->_downgrade_site();
2770 }
2771
2772 return $result;
2773 }
2774
2775 /**
2776 * @author Leo Fajardo (@leorw)
2777 * @since 2.0.2
2778 */
2779 function _delete_theme_update_data_action() {
2780 FS_Plugin_Updater::instance( $this )->delete_update_data();
2781 }
2782
2783 #endregion
2784
2785 #----------------------------------------------------------------------------------
2786 #region Instance
2787 #----------------------------------------------------------------------------------
2788
2789 /**
2790 * Main singleton instance.
2791 *
2792 * @author Vova Feldman (@svovaf)
2793 * @since 1.0.0
2794 *
2795 * @param number $module_id
2796 * @param string|bool $slug
2797 * @param bool $is_init Is initiation sequence.
2798 *
2799 * @return Freemius|false
2800 */
2801 static function instance( $module_id, $slug = false, $is_init = false ) {
2802 if ( empty( $module_id ) ) {
2803 return false;
2804 }
2805
2806 /**
2807 * Load the essential static data prior to initiating FS_Plugin_Manager since there's an essential MS network migration logic that needs to be executed prior to the initiation.
2808 */
2809 self::_load_required_static();
2810
2811 if ( ! is_numeric( $module_id ) ) {
2812 if ( ! $is_init && true === $slug ) {
2813 $is_init = true;
2814 }
2815
2816 $slug = $module_id;
2817
2818 $module = FS_Plugin_Manager::instance( $slug )->get();
2819
2820 if ( is_object( $module ) ) {
2821 $module_id = $module->id;
2822 }
2823 }
2824
2825 $key = 'm_' . $module_id;
2826
2827 if ( ! isset( self::$_instances[ $key ] ) ) {
2828 self::$_instances[ $key ] = new Freemius( $module_id, $slug, $is_init );
2829 }
2830
2831 return self::$_instances[ $key ];
2832 }
2833
2834 /**
2835 * @author Vova Feldman (@svovaf)
2836 * @since 1.0.6
2837 *
2838 * @param number $addon_id
2839 *
2840 * @return bool
2841 */
2842 private static function has_instance( $addon_id ) {
2843 return isset( self::$_instances[ 'm_' . $addon_id ] );
2844 }
2845
2846 /**
2847 * @author Leo Fajardo (@leorw)
2848 * @since 1.2.2
2849 *
2850 * @param string|number $id_or_slug
2851 * @param string $module_type
2852 *
2853 * @return number|false
2854 */
2855 private static function get_module_id( $id_or_slug, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2856 if ( is_numeric( $id_or_slug ) ) {
2857 return $id_or_slug;
2858 }
2859
2860 foreach ( self::$_instances as $instance ) {
2861 // Also check the module type since there can be a plugin and a theme with the same slug.
2862 if ( ( $module_type === $instance->get_module_type() ) && ( $id_or_slug === $instance->get_slug() ) ) {
2863 return $instance->get_id();
2864 }
2865 }
2866
2867 return false;
2868 }
2869
2870 /**
2871 * @author Vova Feldman (@svovaf)
2872 * @since 1.0.6
2873 *
2874 * @param number $id
2875 *
2876 * @return false|Freemius
2877 */
2878 static function get_instance_by_id( $id ) {
2879 return isset ( self::$_instances[ 'm_' . $id ] ) ?
2880 self::$_instances[ 'm_' . $id ] :
2881 false;
2882 }
2883
2884 /**
2885 *
2886 * @author Vova Feldman (@svovaf)
2887 * @since 1.0.1
2888 *
2889 * @param string $plugin_file
2890 * @param string $module_type
2891 *
2892 * @return false|Freemius
2893 */
2894 static function get_instance_by_file( $plugin_file, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2895 $slug = self::find_slug_by_basename( $plugin_file );
2896
2897 return ( false !== $slug ) ?
2898 self::instance( self::get_module_id( $slug, $module_type ) ) :
2899 false;
2900 }
2901
2902 /**
2903 * @author Vova Feldman (@svovaf)
2904 * @since 1.0.6
2905 *
2906 * @return false|Freemius
2907 */
2908 function get_parent_instance() {
2909 return self::get_instance_by_id( $this->_plugin->parent_plugin_id );
2910 }
2911
2912 /**
2913 * @author Vova Feldman (@svovaf)
2914 * @since 1.0.6
2915 *
2916 * @param string|number $id_or_slug
2917 *
2918 * @return false|Freemius
2919 */
2920 function get_addon_instance( $id_or_slug ) {
2921 $addon_id = self::get_module_id( $id_or_slug );
2922
2923 return self::instance( $addon_id );
2924 }
2925
2926 /**
2927 * @return Freemius[]
2928 */
2929 static function _get_all_instances() {
2930 return self::$_instances;
2931 }
2932
2933 #endregion ------------------------------------------------------------------
2934
2935 /**
2936 * @author Vova Feldman (@svovaf)
2937 * @since 1.0.6
2938 *
2939 * @return bool
2940 */
2941 function is_parent_plugin_installed() {
2942 $is_active = self::has_instance( $this->_plugin->parent_plugin_id );
2943
2944 if ( $is_active ) {
2945 return true;
2946 }
2947
2948 /**
2949 * Parent module might be a theme. If that's the case, the add-on's FS
2950 * instance will be loaded prior to the theme's FS instance, therefore,
2951 * we need to check if it's active with a "look ahead".
2952 *
2953 * @author Vova Feldman
2954 * @since 1.2.2.3
2955 */
2956 global $fs_active_plugins;
2957 if ( is_object( $fs_active_plugins ) && is_array( $fs_active_plugins->plugins ) ) {
2958 $active_theme = wp_get_theme();
2959
2960 foreach ( $fs_active_plugins->plugins as $sdk => $module ) {
2961 if ( WP_FS__MODULE_TYPE_THEME === $module->type ) {
2962 if ( $module->plugin_path == $active_theme->get_stylesheet() ) {
2963 // Parent module is a theme and it's currently active.
2964 return true;
2965 }
2966 }
2967 }
2968 }
2969
2970 return false;
2971 }
2972
2973 /**
2974 * Check if add-on parent plugin in activation mode.
2975 *
2976 * @author Vova Feldman (@svovaf)
2977 * @since 1.0.7
2978 *
2979 * @return bool
2980 */
2981 function is_parent_in_activation() {
2982 $parent_fs = $this->get_parent_instance();
2983 if ( ! is_object( $parent_fs ) ) {
2984 return false;
2985 }
2986
2987 return ( $parent_fs->is_activation_mode() );
2988 }
2989
2990 /**
2991 * Is plugin in activation mode.
2992 *
2993 * @author Vova Feldman (@svovaf)
2994 * @since 1.0.7
2995 *
2996 * @param bool $and_on
2997 *
2998 * @return bool
2999 */
3000 function is_activation_mode( $and_on = true ) {
3001 return fs_is_network_admin() ?
3002 $this->is_network_activation_mode( $and_on ) :
3003 $this->is_site_activation_mode( $and_on );
3004 }
3005
3006 /**
3007 * Is plugin in activation mode.
3008 *
3009 * @author Vova Feldman (@svovaf)
3010 * @since 1.0.7
3011 *
3012 * @param bool $and_on
3013 *
3014 * @return bool
3015 */
3016 function is_site_activation_mode( $and_on = true ) {
3017 return (
3018 ( $this->is_on() || ! $and_on ) &&
3019 (
3020 ( $this->is_premium() && true === $this->_storage->require_license_activation ) ||
3021 (
3022 ( ! $this->is_registered() ||
3023 ( $this->is_only_premium() && ! $this->has_features_enabled_license() ) ) &&
3024 ( ! $this->is_enable_anonymous() ||
3025 ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) )
3026 )
3027 )
3028 );
3029 }
3030
3031 /**
3032 * Checks if the SDK in network activation mode.
3033 *
3034 * @author Leo Fajardo (@leorw)
3035 * @since 2.0.0
3036 *
3037 * @param bool $and_on
3038 *
3039 * @return bool
3040 */
3041 private function is_network_activation_mode( $and_on = true ) {
3042 if ( ! $this->_is_network_active ) {
3043 // Not network activated.
3044 return false;
3045 }
3046
3047 if ( $this->is_network_upgrade_mode() ) {
3048 // Special flag to enforce network activation mode to decide what to do with the sites that are not yet opted-in nor skipped.
3049 return true;
3050 }
3051
3052 if ( ! $this->is_site_activation_mode( $and_on ) ) {
3053 // Whether the context is single site or the network, if the plugin is no longer in activation mode then it is not in network activation mode as well.
3054 return false;
3055 }
3056
3057 if ( $this->is_network_delegated_connection() ) {
3058 // Super-admin delegated the connection to the site admins -> not activation mode.
3059 return false;
3060 }
3061
3062 if ( $this->is_network_anonymous() && true !== $this->_storage->require_license_activation ) {
3063 // Super-admin skipped the connection network wide -> not activation mode.
3064 return false;
3065 }
3066
3067 if ( $this->is_network_registered() ) {
3068 // Super-admin connected at least one site -> not activation mode.
3069 return false;
3070 }
3071
3072 return true;
3073 }
3074
3075 /**
3076 * Check if current page is the opt-in/pending-activation page.
3077 *
3078 * @author Vova Feldman (@svovaf)
3079 * @since 1.2.1.7
3080 *
3081 * @return bool
3082 */
3083 function is_activation_page() {
3084 if ( $this->_menu->is_activation_page( $this->show_opt_in_on_themes_page() ) ) {
3085 return true;
3086 }
3087
3088 if ( ! $this->is_activation_mode() ) {
3089 return false;
3090 }
3091
3092 // Check if current page is matching the activation page.
3093 return $this->is_matching_url( $this->get_activation_url() );
3094 }
3095
3096 /**
3097 * Check if URL path's are matching and that all querystring
3098 * arguments of the $sub_url exist in the $url with the same values.
3099 *
3100 * WARNING:
3101 * 1. This method doesn't check if the sub/domain are matching.
3102 * 2. Ignore case sensitivity.
3103 *
3104 * @author Vova Feldman (@svovaf)
3105 * @since 1.2.1.7
3106 *
3107 * @param string $sub_url
3108 * @param string $url If argument is not set, check if the sub_url matching the current's page URL.
3109 *
3110 * @return bool
3111 */
3112 private function is_matching_url( $sub_url, $url = '' ) {
3113 if ( empty( $url ) ) {
3114 $url = $_SERVER['REQUEST_URI'];
3115 }
3116
3117 $url = strtolower( $url );
3118 $sub_url = strtolower( $sub_url );
3119
3120 if ( parse_url( $sub_url, PHP_URL_PATH ) !== parse_url( $url, PHP_URL_PATH ) ) {
3121 // Different path - DO NOT OVERRIDE PAGE.
3122 return false;
3123 }
3124
3125 $url_params = fs_parse_url_params( $url );
3126 $sub_url_params = fs_parse_url_params( $sub_url );
3127
3128 foreach ( $sub_url_params as $key => $val ) {
3129 if ( ! isset( $url_params[ $key ] ) || $val != $url_params[ $key ] ) {
3130 // Not matching query string - DO NOT OVERRIDE PAGE.
3131 return false;
3132 }
3133 }
3134
3135 return true;
3136 }
3137
3138 /**
3139 * Get the basenames of all active plugins for specific blog. Including network activated plugins.
3140 *
3141 * @author Vova Feldman (@svovaf)
3142 * @since 2.0.0
3143 *
3144 * @param int $blog_id
3145 *
3146 * @return string[]
3147 */
3148 private static function get_active_plugins_basenames( $blog_id = 0 ) {
3149 if ( is_multisite() && $blog_id > 0 ) {
3150 $active_basenames = get_blog_option( $blog_id, 'active_plugins' );
3151 } else {
3152 $active_basenames = get_option( 'active_plugins' );
3153 }
3154
3155 if ( ! is_array( $active_basenames ) ) {
3156 $active_basenames = array();
3157 }
3158
3159 if ( is_multisite() ) {
3160 $network_active_basenames = get_site_option( 'active_sitewide_plugins' );
3161
3162 if ( is_array( $network_active_basenames ) && ! empty( $network_active_basenames ) ) {
3163 $active_basenames = array_merge( $active_basenames, array_keys( $network_active_basenames ) );
3164 }
3165 }
3166
3167 return $active_basenames;
3168 }
3169
3170 /**
3171 * @author Leo Fajardo (@leorw)
3172 * @since 2.3.0
3173 *
3174 * @param int $blog_id
3175 *
3176 * @return array
3177 */
3178 static function get_active_plugins_directories_map( $blog_id = 0 ) {
3179 $active_basenames = self::get_active_plugins_basenames( $blog_id );
3180
3181 $map = array();
3182
3183 foreach ( $active_basenames as $active_basename ) {
3184 $active_basename = fs_normalize_path( $active_basename );
3185
3186 if ( false === strpos( $active_basename, '/' ) ) {
3187 continue;
3188 }
3189
3190 $map[ dirname( $active_basename ) ] = true;
3191 }
3192
3193 return $map;
3194 }
3195
3196 /**
3197 * Get collection of all active plugins. Including network activated plugins.
3198 *
3199 * @author Vova Feldman (@svovaf)
3200 * @since 1.0.9
3201 *
3202 * @param int $blog_id Since 2.0.0
3203 *
3204 * @return array[string]array
3205 */
3206 private static function get_active_plugins( $blog_id = 0 ) {
3207 self::require_plugin_essentials();
3208
3209 $active_plugin = array();
3210 $all_plugins = fs_get_plugins();
3211 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3212
3213 foreach ( $active_plugins_basenames as $plugin_basename ) {
3214 $active_plugin[ $plugin_basename ] = $all_plugins[ $plugin_basename ];
3215 }
3216
3217 return $active_plugin;
3218 }
3219
3220 /**
3221 * Get collection of all site active plugins for a specified blog.
3222 *
3223 * @author Vova Feldman (@svovaf)
3224 * @since 2.0.0
3225 *
3226 * @param int $blog_id
3227 *
3228 * @return array[string]array
3229 */
3230 private static function get_site_active_plugins( $blog_id = 0 ) {
3231 $active_basenames = ( is_multisite() && $blog_id > 0 ) ?
3232 get_blog_option( $blog_id, 'active_plugins' ) :
3233 get_option( 'active_plugins' );
3234
3235 $active = array();
3236
3237 if ( ! is_array( $active_basenames ) ) {
3238 return $active;
3239 }
3240
3241 foreach ( $active_basenames as $basename ) {
3242 $active[ $basename ] = array(
3243 'is_active' => true,
3244 'Version' => '1.0', // Dummy version.
3245 'slug' => self::get_plugin_slug( $basename ),
3246 );
3247 }
3248
3249 return $active;
3250 }
3251
3252 /**
3253 * Get collection of all plugins with their activation status for a specified blog.
3254 *
3255 * @author Vova Feldman (@svovaf)
3256 * @since 1.1.8
3257 *
3258 * @param int $blog_id Since 2.0.0
3259 *
3260 * @return array Key is the plugin file path and the value is an array of the plugin data.
3261 */
3262 private static function get_all_plugins( $blog_id = 0 ) {
3263 self::require_plugin_essentials();
3264
3265 $all_plugins = fs_get_plugins();
3266
3267 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3268
3269 foreach ( $all_plugins as $basename => &$data ) {
3270 // By default set to inactive (next foreach update the active plugins).
3271 $data['is_active'] = false;
3272 // Enrich with plugin slug.
3273 $data['slug'] = self::get_plugin_slug( $basename );
3274 }
3275
3276 // Flag active plugins.
3277 foreach ( $active_plugins_basenames as $basename ) {
3278 if ( isset( $all_plugins[ $basename ] ) ) {
3279 $all_plugins[ $basename ]['is_active'] = true;
3280 }
3281 }
3282
3283 return $all_plugins;
3284 }
3285
3286 /**
3287 * Get collection of all plugins and if they are network level activated.
3288 *
3289 * @author Vova Feldman (@svovaf)
3290 * @since 2.0.0
3291 *
3292 * @return array Key is the plugin basename and the value is an array of the plugin data.
3293 */
3294 private static function get_network_plugins() {
3295 self::require_plugin_essentials();
3296
3297 $all_plugins = fs_get_plugins();
3298
3299 $network_active_basenames = is_multisite() ?
3300 get_site_option( 'active_sitewide_plugins' ) :
3301 array();
3302
3303 foreach ( $all_plugins as $basename => &$data ) {
3304 // By default set to inactive (next foreach update the active plugins).
3305 $data['is_active'] = false;
3306 // Enrich with plugin slug.
3307 $data['slug'] = self::get_plugin_slug( $basename );
3308 }
3309
3310 // Flag active plugins.
3311 foreach ( $network_active_basenames as $basename ) {
3312 if ( isset( $all_plugins[ $basename ] ) ) {
3313 $all_plugins[ $basename ]['is_active'] = true;
3314 }
3315 }
3316
3317 return $all_plugins;
3318 }
3319
3320 /**
3321 * Cached result of get_site_transient( 'update_plugins' )
3322 *
3323 * @author Vova Feldman (@svovaf)
3324 * @since 1.1.8
3325 *
3326 * @var object
3327 */
3328 private static $_plugins_info;
3329
3330 /**
3331 * Helper function to get specified plugin's slug.
3332 *
3333 * @author Vova Feldman (@svovaf)
3334 * @since 1.1.8
3335 *
3336 * @param $basename
3337 *
3338 * @return string
3339 */
3340 private static function get_plugin_slug( $basename ) {
3341 if ( ! isset( self::$_plugins_info ) ) {
3342 self::$_plugins_info = get_site_transient( 'update_plugins' );
3343 }
3344
3345 $slug = '';
3346
3347 if ( is_object( self::$_plugins_info ) ) {
3348 if ( isset( self::$_plugins_info->no_update ) &&
3349 isset( self::$_plugins_info->no_update[ $basename ] ) &&
3350 ! empty( self::$_plugins_info->no_update[ $basename ]->slug )
3351 ) {
3352 $slug = self::$_plugins_info->no_update[ $basename ]->slug;
3353 } else if ( isset( self::$_plugins_info->response ) &&
3354 isset( self::$_plugins_info->response[ $basename ] ) &&
3355 ! empty( self::$_plugins_info->response[ $basename ]->slug )
3356 ) {
3357 $slug = self::$_plugins_info->response[ $basename ]->slug;
3358 }
3359 }
3360
3361 if ( empty( $slug ) ) {
3362 // Try to find slug from FS data.
3363 $slug = self::find_slug_by_basename( $basename );
3364 }
3365
3366 if ( empty( $slug ) ) {
3367 // Fallback to plugin's folder name.
3368 $slug = dirname( $basename );
3369 }
3370
3371 return $slug;
3372 }
3373
3374 private static $_statics_loaded = false;
3375
3376 /**
3377 * Load static resources.
3378 *
3379 * @author Vova Feldman (@svovaf)
3380 * @since 1.0.1
3381 */
3382 private static function _load_required_static() {
3383 if ( self::$_statics_loaded ) {
3384 return;
3385 }
3386
3387 self::$_static_logger = FS_Logger::get_logger( WP_FS__SLUG, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
3388
3389 self::$_static_logger->entrance();
3390
3391 self::$_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
3392
3393 if ( is_multisite() ) {
3394 $has_skipped_migration = (
3395 // 'id_slug_type_path_map' - was never stored on older versions, therefore, not exists on the site level.
3396 null === self::$_accounts->get_option( 'id_slug_type_path_map', null, false ) &&
3397 // 'file_slug_map' stored on the site level, so it was running an SDK version before it was integrated with MS-network.
3398 null !== self::$_accounts->get_option( 'file_slug_map', null, false )
3399 );
3400
3401 /**
3402 * If the file_slug_map exists on the site level but doesn't exist on the
3403 * network level storage, it means that we need to process the storage with migration.
3404 *
3405 * The code in this `if` scope will only be executed once and only for the first site that will execute it because once we migrate the storage data, file_slug_map will be already set in the network level storage.
3406 *
3407 * @author Vova Feldman (@svovaf)
3408 * @since 2.0.0
3409 */
3410 if (
3411 ( $has_skipped_migration && true !== self::$_accounts->get_option( 'ms_migration_complete', false, true ) ) ||
3412 ( null === self::$_accounts->get_option( 'file_slug_map', null, true ) &&
3413 null !== self::$_accounts->get_option( 'file_slug_map', null, false ) )
3414 ) {
3415 self::migrate_options_to_network();
3416 }
3417 }
3418
3419 self::$_global_admin_notices = FS_Admin_Notices::instance( 'global' );
3420
3421 if ( ! WP_FS__DEMO_MODE ) {
3422 add_action( ( fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu', array(
3423 'Freemius',
3424 '_add_debug_section'
3425 ) );
3426 }
3427
3428 add_action( "wp_ajax_fs_toggle_debug_mode", array( 'Freemius', '_toggle_debug_mode' ) );
3429
3430 self::add_ajax_action_static( 'get_debug_log', array( 'Freemius', '_get_debug_log' ) );
3431
3432 self::add_ajax_action_static( 'get_db_option', array( 'Freemius', '_get_db_option' ) );
3433
3434 self::add_ajax_action_static( 'set_db_option', array( 'Freemius', '_set_db_option' ) );
3435
3436 if ( 0 == did_action( 'plugins_loaded' ) ) {
3437 add_action( 'plugins_loaded', array( 'Freemius', '_load_textdomain' ), 1 );
3438 }
3439
3440 $clone_manager = FS_Clone_Manager::instance();
3441 add_action( 'init', array( $clone_manager, '_init' ) );
3442
3443 add_action( 'admin_footer', array( 'Freemius', '_open_support_forum_in_new_page' ) );
3444
3445 if ( self::is_plugins_page() || self::is_themes_page() ) {
3446 add_action( 'admin_print_footer_scripts', array( 'Freemius', '_maybe_add_beta_label_styles' ), 9 );
3447
3448 /**
3449 * Specifically use this hook so that the JS event handlers will work properly on the "Themes"
3450 * page.
3451 *
3452 * @author Leo Fajardo (@leorw)
3453 * @since 2.3.0
3454 */
3455 add_action( 'admin_footer-' . self::get_current_page(), array( 'Freemius', '_maybe_add_beta_label_to_plugins_and_handle_confirmation') );
3456 }
3457
3458 self::$_statics_loaded = true;
3459 }
3460
3461 #--------------------------------------------------------------------------------
3462 #region Clone
3463 #--------------------------------------------------------------------------------
3464
3465 /**
3466 * @author Leo Fajardo (@leorw)
3467 * @since 2.5.0
3468 *
3469 * @param bool $only_if_manual_resolution_is_not_hidden
3470 *
3471 * @return bool
3472 */
3473 private function is_unresolved_clone( $only_if_manual_resolution_is_not_hidden = false ) {
3474 if ( ! $this->is_clone( $only_if_manual_resolution_is_not_hidden ) ) {
3475 return false;
3476 }
3477
3478 return FS_Clone_Manager::instance()->has_temporary_duplicate_mode_expired();
3479 }
3480
3481 /**
3482 * @author Leo Fajardo (@leorw)
3483 * @since 2.5.0
3484 *
3485 * @param bool $only_if_manual_resolution_is_not_hidden
3486 */
3487 function is_clone( $only_if_manual_resolution_is_not_hidden = false ) {
3488 if ( ! is_object( $this->_site ) ) {
3489 return false;
3490 }
3491
3492 $blog_id = null;
3493
3494 if (
3495 fs_is_network_admin() &&
3496 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
3497 ) {
3498 // Ensure that we're comparing the network install's URL with the relevant subsite's URL.
3499 $blog_id = $this->_storage->network_install_blog_id;
3500 }
3501
3502 $site_url = Freemius::get_unfiltered_site_url( $blog_id, true, true );
3503
3504 if ( ! $this->_site->is_clone( $site_url ) ) {
3505 return false;
3506 }
3507
3508 return (
3509 ! $only_if_manual_resolution_is_not_hidden ||
3510 ! FS_Clone_Manager::instance()->should_hide_manual_resolution()
3511 );
3512 }
3513
3514 /**
3515 * @author Leo Fajardo (@leorw)
3516 * @since 2.5.0
3517 *
3518 * @param int|null $blog_id
3519 * @param bool $strip_protocol
3520 * @param bool $add_trailing_slash
3521 *
3522 * @return string
3523 */
3524 static function get_unfiltered_site_url( $blog_id = null, $strip_protocol = false, $add_trailing_slash = false ) {
3525 $url = ( ! is_multisite() && defined( 'WP_SITEURL' ) ) ? WP_SITEURL : self::get_site_url_from_wp_option( $blog_id );
3526
3527 if ( $strip_protocol ) {
3528 $url = fs_strip_url_protocol( $url );
3529 }
3530
3531 if ( $add_trailing_slash ) {
3532 $url = trailingslashit( $url );
3533 }
3534
3535 return $url;
3536 }
3537
3538 /**
3539 * @author Leo Fajardo (@leorw)
3540 * @since 2.6.0
3541 *
3542 * @param int|null $blog_id
3543 *
3544 * @return string
3545 */
3546 private static function get_site_url_from_wp_option( $blog_id = null ) {
3547 global $wp_filter;
3548
3549 $site_url_filters = array(
3550 'site_url' => null,
3551 'pre_option_siteurl' => null,
3552 'default_option_siteurl' => null,
3553 'option_siteurl' => null,
3554 );
3555
3556 // Detach all URL-related filters to get the actual site's URL (stripped of potential manipulations by multilingual plugins).
3557 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3558 if ( ! empty( $wp_filter[ $hook_name ] ) ) {
3559 $site_url_filters[ $hook_name ] = $wp_filter[ $hook_name ];
3560 unset( $wp_filter[ $hook_name ] );
3561 }
3562 }
3563
3564 $url = get_site_url( $blog_id );
3565
3566 // Re-attach the filters back.
3567 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3568 if ( ! empty( $site_url_filter ) ) {
3569 $wp_filter[ $hook_name ] = $site_url_filter;
3570 }
3571 }
3572
3573 return $url;
3574 }
3575
3576 /**
3577 * @author Leo Fajardo (@leorw)
3578 * @since 2.5.0
3579 *
3580 * @param number $site_id
3581 */
3582 function fetch_install_by_id( $site_id ) {
3583 return $this->get_current_or_network_user_api_scope()->get( "/installs/{$site_id}.json" );
3584 }
3585
3586 /**
3587 * @author Leo Fajardo (@leorw)
3588 * @since 2.5.0
3589 *
3590 * @return string|object|bool
3591 */
3592 function _handle_long_term_duplicate() {
3593 $this->_logger->entrance();
3594
3595 $this->delete_current_install( false );
3596
3597 $license_key = false;
3598
3599 if (
3600 is_object( $this->_license ) &&
3601 ! $this->_license->is_utilized(
3602 ( WP_FS__IS_LOCALHOST_FOR_SERVER || FS_Site::is_localhost_by_address( self::get_unfiltered_site_url() ) )
3603 )
3604 ) {
3605 $license_key = $this->_license->secret_key;
3606 }
3607
3608 return $this->opt_in(
3609 false,
3610 false,
3611 false,
3612 $license_key,
3613 false,
3614 false,
3615 false,
3616 null,
3617 array(),
3618 false
3619 );
3620 }
3621
3622 #endregion
3623
3624 /**
3625 * @author Leo Fajardo (@leorw)
3626 *
3627 * @since 2.1.3
3628 */
3629 private static function migrate_options_to_network() {
3630 self::migrate_accounts_to_network();
3631
3632 // Migrate API options from site level to network level.
3633 $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
3634 $api_network_options->migrate_to_network();
3635
3636 // Migrate API cache to network level storage.
3637 FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
3638
3639 self::$_accounts->set_option( 'ms_migration_complete', true, true );
3640 }
3641
3642 #----------------------------------------------------------------------------------
3643 #region Localization
3644 #----------------------------------------------------------------------------------
3645
3646 /**
3647 * Load framework's text domain.
3648 *
3649 * @author Vova Feldman (@svovaf)
3650 * @since 1.2.1
3651 */
3652 static function _load_textdomain() {
3653 if ( ! is_admin() ) {
3654 return;
3655 }
3656
3657 global $fs_active_plugins;
3658
3659 // Works both for plugins and themes.
3660 load_plugin_textdomain(
3661 'freemius',
3662 false,
3663 $fs_active_plugins->newest->sdk_path . '/languages/'
3664 );
3665 }
3666
3667 #endregion
3668
3669 #----------------------------------------------------------------------------------
3670 #region Debugging
3671 #----------------------------------------------------------------------------------
3672
3673 /**
3674 * @author Vova Feldman (@svovaf)
3675 * @since 1.0.8
3676 */
3677 static function _add_debug_section() {
3678 if ( ! is_super_admin() ) {
3679 // Add debug page only for super-admins.
3680 return;
3681 }
3682
3683 self::$_static_logger->entrance();
3684
3685 $title = sprintf( '%s [v.%s]', fs_text_inline( 'Freemius Debug' ), WP_FS__SDK_VERSION );
3686
3687 if ( WP_FS__DEV_MODE ) {
3688 // Add top-level debug menu item.
3689 $hook = FS_Admin_Menu_Manager::add_page(
3690 $title,
3691 $title,
3692 'manage_options',
3693 'freemius',
3694 array( 'Freemius', '_debug_page_render' )
3695 );
3696 } else {
3697 // Add hidden debug page.
3698 $hook = FS_Admin_Menu_Manager::add_subpage(
3699 '',
3700 $title,
3701 $title,
3702 'manage_options',
3703 'freemius',
3704 array( 'Freemius', '_debug_page_render' )
3705 );
3706 }
3707
3708 if ( ! empty( $hook ) ) {
3709 add_action( "load-$hook", array( 'Freemius', '_debug_page_actions' ) );
3710 }
3711 }
3712
3713 /**
3714 * @author Vova Feldman (@svovaf)
3715 * @since 1.1.7.3
3716 */
3717 static function _toggle_debug_mode() {
3718 check_admin_referer( 'fs_toggle_debug_mode' );
3719
3720 if ( ! is_super_admin() ) {
3721 return;
3722 }
3723
3724 $is_on = fs_request_get( 'is_on', false, 'post' );
3725
3726 if ( fs_request_is_post() && in_array( $is_on, array( 0, 1 ) ) ) {
3727 update_option( 'fs_debug_mode', $is_on );
3728
3729 // Turn on/off storage logging.
3730 FS_Logger::_set_storage_logging( ( 1 == $is_on ) );
3731 }
3732
3733 exit;
3734 }
3735
3736 /**
3737 * @author Vova Feldman (@svovaf)
3738 * @since 1.2.1.6
3739 */
3740 static function _get_debug_log() {
3741 check_admin_referer( 'fs_get_debug_log' );
3742
3743 if ( ! is_super_admin() ) {
3744 return;
3745 }
3746
3747 $limit = min( ! empty( $_POST['limit'] ) ? absint( $_POST['limit'] ) : 200, 200 );
3748 $offset = min( ! empty( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 200, 200 );
3749
3750 $logs = FS_Logger::load_db_logs(
3751 fs_request_get( 'filters', false, 'post' ),
3752 $limit,
3753 $offset
3754 );
3755
3756 self::shoot_ajax_success( $logs );
3757 }
3758
3759 /**
3760 * @author Vova Feldman (@svovaf)
3761 * @since 1.2.1.7
3762 */
3763 static function _get_db_option() {
3764 check_admin_referer( 'fs_get_db_option' );
3765
3766 $option_name = fs_request_get( 'option_name' );
3767
3768 if ( ! is_super_admin() ||
3769 ! fs_starts_with( $option_name, 'fs_' )
3770 ) {
3771 self::shoot_ajax_failure();
3772 }
3773
3774 $value = get_option( $option_name );
3775
3776 $result = array(
3777 'name' => $option_name,
3778 );
3779
3780 if ( false !== $value ) {
3781 if ( ! is_string( $value ) ) {
3782 $value = json_encode( $value );
3783 }
3784
3785 $result['value'] = $value;
3786 }
3787
3788 self::shoot_ajax_success( $result );
3789 }
3790
3791 /**
3792 * @author Vova Feldman (@svovaf)
3793 * @since 1.2.1.7
3794 */
3795 static function _set_db_option() {
3796 check_admin_referer( 'fs_set_db_option' );
3797
3798 $option_name = fs_request_get( 'option_name' );
3799
3800 if ( ! is_super_admin() ||
3801 ! fs_starts_with( $option_name, 'fs_' )
3802 ) {
3803 self::shoot_ajax_failure();
3804 }
3805
3806 $option_value = fs_request_get_raw( 'option_value' );
3807
3808 if ( ! empty( $option_value ) ) {
3809 update_option( $option_name, $option_value );
3810 }
3811
3812 self::shoot_ajax_success();
3813 }
3814
3815 /**
3816 * @author Vova Feldman (@svovaf)
3817 * @since 1.0.8
3818 */
3819 static function _debug_page_actions() {
3820 self::_clean_admin_content_section();
3821
3822 if ( fs_request_is_action( 'restart_freemius' ) ) {
3823 check_admin_referer( 'restart_freemius' );
3824
3825 if ( ! is_multisite() ) {
3826 // Clear accounts data.
3827 self::$_accounts->clear( null, true );
3828 } else {
3829 $sites = self::get_sites();
3830 foreach ( $sites as $site ) {
3831 $blog_id = self::get_site_blog_id( $site );
3832 self::$_accounts->clear( $blog_id, true );
3833 }
3834
3835 // Clear network level storage.
3836 self::$_accounts->clear( true, true );
3837 }
3838
3839 // Clear SDK reference cache.
3840 delete_option( 'fs_active_plugins' );
3841 } else if ( fs_request_is_action( 'clear_updates_data' ) ) {
3842 check_admin_referer( 'clear_updates_data' );
3843
3844 if ( ! is_multisite() ) {
3845 set_site_transient( 'update_plugins', null );
3846 set_site_transient( 'update_themes', null );
3847 } else {
3848 $current_blog_id = get_current_blog_id();
3849
3850 $sites = self::get_sites();
3851 foreach ( $sites as $site ) {
3852 switch_to_blog( self::get_site_blog_id( $site ) );
3853
3854 set_site_transient( 'update_plugins', null );
3855 set_site_transient( 'update_themes', null );
3856 }
3857
3858 switch_to_blog( $current_blog_id );
3859 }
3860 } else if ( fs_request_is_action( 'reset_deactivation_snoozing' ) ) {
3861 check_admin_referer( 'reset_deactivation_snoozing' );
3862
3863 self::reset_deactivation_snoozing();
3864 } else if ( fs_request_is_action( 'simulate_trial' ) ) {
3865 check_admin_referer( 'simulate_trial' );
3866
3867 $fs = freemius( fs_request_get( 'module_id' ) );
3868
3869 // Update SDK install to at least 24 hours before.
3870 $fs->_storage->install_timestamp = ( time() - WP_FS__TIME_24_HOURS_IN_SEC );
3871 // Unset the trial shown timestamp.
3872 unset( $fs->_storage->trial_promotion_shown );
3873 } else if ( fs_request_is_action( 'simulate_network_upgrade' ) ) {
3874 check_admin_referer( 'simulate_network_upgrade' );
3875
3876 $fs = freemius( fs_request_get( 'module_id' ) );
3877
3878 self::set_network_upgrade_mode( $fs->_storage );
3879 } else if ( fs_request_is_action( 'delete_install' ) ) {
3880 check_admin_referer( 'delete_install' );
3881
3882 self::_delete_site_by_slug(
3883 fs_request_get( 'slug' ),
3884 fs_request_get( 'module_type' ),
3885 true,
3886 fs_request_get( 'blog_id', null )
3887 );
3888 } else if ( fs_request_is_action( 'delete_user' ) ) {
3889 check_admin_referer( 'delete_user' );
3890
3891 self::delete_user( fs_request_get( 'user_id' ) );
3892 } else if ( fs_request_is_action( 'download_logs' ) ) {
3893 check_admin_referer( 'download_logs' );
3894
3895 $download_url = FS_Logger::download_db_logs(
3896 fs_request_get( 'filters', false, 'post' )
3897 );
3898
3899 if ( false === $download_url ) {
3900 wp_die( 'Oops... there was an error while generating the logs download file. Please try again and if it doesn\'t work contact support@freemius.com.' );
3901 }
3902
3903 fs_redirect( $download_url );
3904 } else if ( fs_request_is_action( 'migrate_options_to_network' ) ) {
3905 check_admin_referer( 'migrate_options_to_network' );
3906
3907 self::migrate_options_to_network();
3908 }
3909 }
3910
3911 /**
3912 * @author Leo Fajardo (@leorw)
3913 * @since 2.5.0
3914 *
3915 * @return array
3916 */
3917 static function get_all_modules_sites() {
3918 self::$_static_logger->entrance();
3919
3920 $sites_by_type = array(
3921 WP_FS__MODULE_TYPE_PLUGIN => array(),
3922 WP_FS__MODULE_TYPE_THEME => array(),
3923 );
3924
3925 $module_types = array_keys( $sites_by_type );
3926
3927 if ( ! is_multisite() ) {
3928 foreach ( $module_types as $type ) {
3929 $sites_by_type[ $type ] = self::get_all_sites( $type );
3930
3931 foreach ( $sites_by_type[ $type ] as $slug => $install ) {
3932 $sites_by_type[ $type ][ $slug ] = array( $install );
3933 }
3934 }
3935 } else {
3936 $sites = self::get_sites();
3937
3938 foreach ( $sites as $site ) {
3939 $blog_id = self::get_site_blog_id( $site );
3940
3941 foreach ( $module_types as $type ) {
3942 $installs = self::get_all_sites( $type, $blog_id );
3943
3944 foreach ( $installs as $slug => $install ) {
3945 if ( ! isset( $sites_by_type[ $type ][ $slug ] ) ) {
3946 $sites_by_type[ $type ][ $slug ] = array();
3947 }
3948
3949 $install->blog_id = $blog_id;
3950
3951 $sites_by_type[ $type ][ $slug ][] = $install;
3952 }
3953
3954 }
3955 }
3956 }
3957
3958 return $sites_by_type;
3959 }
3960
3961 /**
3962 * @author Vova Feldman (@svovaf)
3963 * @since 1.0.8
3964 */
3965 static function _debug_page_render() {
3966 self::$_static_logger->entrance();
3967
3968 $all_modules_sites = self::get_all_modules_sites();
3969
3970 $licenses_by_module_type = self::get_all_licenses_by_module_type();
3971
3972 $vars = array(
3973 'plugin_sites' => $all_modules_sites[ WP_FS__MODULE_TYPE_PLUGIN ],
3974 'theme_sites' => $all_modules_sites[ WP_FS__MODULE_TYPE_THEME ],
3975 'users' => self::get_all_users(),
3976 'addons' => self::get_all_addons(),
3977 'account_addons' => self::get_all_account_addons(),
3978 'plugin_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_PLUGIN ],
3979 'theme_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_THEME ]
3980 );
3981
3982 fs_enqueue_local_style( 'fs_debug', '/admin/debug.css' );
3983 fs_require_once_template( 'debug.php', $vars );
3984 }
3985
3986 #endregion
3987
3988 #----------------------------------------------------------------------------------
3989 #region Connectivity Issues
3990 #----------------------------------------------------------------------------------
3991
3992 /**
3993 * Check if Freemius should be turned on for the current plugin install.
3994 *
3995 * Note:
3996 * $this->_is_on is updated in has_api_connectivity()
3997 *
3998 * @author Vova Feldman (@svovaf)
3999 * @since 1.0.9
4000 *
4001 * @return bool
4002 */
4003 function is_on() {
4004 self::$_static_logger->entrance();
4005
4006 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
4007 return false;
4008 }
4009
4010 if ( isset( $this->_is_on ) ) {
4011 return $this->_is_on;
4012 }
4013
4014 // If already installed or pending then sure it's on :)
4015 if ( $this->is_registered() || $this->is_pending_activation() ) {
4016 $this->_is_on = true;
4017
4018 return true;
4019 }
4020
4021 return false;
4022 }
4023
4024 /**
4025 * @author Vova Feldman (@svovaf)
4026 * @since 1.1.7.3
4027 *
4028 * @param bool $flush_if_no_connectivity
4029 *
4030 * @return bool
4031 */
4032 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
4033 if ( ! isset( $this->_storage->connectivity_test ) ) {
4034 // Connectivity test was never executed, or cache was cleared.
4035 return true;
4036 }
4037
4038 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
4039 if ( WP_FS__IS_HTTP_REQUEST ) {
4040 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
4041 // Domain changed.
4042 return true;
4043 }
4044
4045 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
4046 // Server IP changed.
4047 return true;
4048 }
4049 }
4050 }
4051
4052 if ( $this->_storage->connectivity_test['is_connected'] &&
4053 $this->_storage->connectivity_test['is_active']
4054 ) {
4055 // API connected and Freemius is active - no need to run connectivity check.
4056 return false;
4057 }
4058
4059 if ( $flush_if_no_connectivity ) {
4060 /**
4061 * If explicitly asked to flush when no connectivity - do it only
4062 * if at least 10 sec passed from the last API connectivity test.
4063 */
4064 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
4065 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
4066 }
4067
4068 /**
4069 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
4070 */
4071 $version = $this->get_plugin_version();
4072 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
4073 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
4074 return true;
4075 }
4076
4077 return false;
4078 }
4079
4080 /**
4081 * @author Leo Fajardo (@leorw)
4082 * @since 2.5.4
4083 *
4084 * @param bool $is_update
4085 *
4086 * @return bool
4087 */
4088 private function should_turn_fs_on( $is_update = true ) {
4089 if (
4090 empty( $this->_plugin->opt_in_moderation ) ||
4091 ! is_array( $this->_plugin->opt_in_moderation )
4092 ) {
4093 return true;
4094 }
4095
4096 $optin_config = $this->_plugin->opt_in_moderation;
4097
4098 if (
4099 WP_FS__IS_LOCALHOST &&
4100 ( ! isset( $optin_config['localhost'] ) || false !== $optin_config['localhost'] )
4101 ) {
4102 return true;
4103 }
4104
4105 $optin_config_key = $is_update ?
4106 'updates' :
4107 'new';
4108
4109 if ( ! isset( $optin_config[ $optin_config_key ] ) ) {
4110 return true;
4111 }
4112
4113 $visibility_percentage = $optin_config[ $optin_config_key ];
4114
4115 if ( 0 == $visibility_percentage ) {
4116 return false;
4117 }
4118
4119 if ( ! is_numeric( $visibility_percentage ) ) {
4120 return true;
4121 }
4122
4123 $min = 1;
4124 $max = 100;
4125
4126 if ( function_exists( 'random_int' ) ) {
4127 $random = random_int( $min, $max ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.random_intFound
4128 } else {
4129 $random = rand( $min, $max );
4130 }
4131
4132 return ( $random <= $visibility_percentage );
4133 }
4134
4135 /**
4136 * Check if there's any connectivity issue to Freemius API.
4137 *
4138 * @author Vova Feldman (@svovaf)
4139 * @since 1.0.9
4140 *
4141 * @param bool $flush_if_no_connectivity
4142 *
4143 * @return bool|null
4144 */
4145 function has_api_connectivity( $flush_if_no_connectivity = false ) {
4146 $this->_logger->entrance();
4147
4148 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
4149 return $this->_has_api_connection;
4150 }
4151
4152 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
4153 isset( $this->_storage->connectivity_test ) &&
4154 true === $this->_storage->connectivity_test['is_connected']
4155 ) {
4156 $this->clear_connectivity_info();
4157 }
4158
4159 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
4160 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
4161 /**
4162 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
4163 *
4164 * @since 1.2.1.5 If the user running the premium version then ignore the 'is_active' flag and turn Freemius on to enable license key activation.
4165 */
4166 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
4167 $this->is_premium() ||
4168 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
4169
4170 return $this->_has_api_connection;
4171 }
4172
4173 if (
4174 ! empty( $this->_storage->connectivity_test ) &&
4175 isset( $this->_storage->connectivity_test['is_active'] )
4176 ) {
4177 $is_active = $this->_storage->connectivity_test['is_active'];
4178 } else {
4179 $is_active = $this->should_turn_fs_on( $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() ) );
4180
4181 $this->store_connectivity_info( (object) array( 'is_active' => $is_active ), null );
4182 }
4183
4184 if ( $is_active ) {
4185 $this->_is_on = true;
4186 }
4187
4188 return $this->_has_api_connection;
4189 }
4190
4191 /**
4192 * @author Leo Fajardo (@leorw)
4193 * @since 2.5.4
4194 */
4195 private function clear_connectivity_info() {
4196 unset( $this->_storage->connectivity_test );
4197
4198 FS_Api::clear_force_http_flag();
4199 }
4200
4201 /**
4202 * @author Vova Feldman (@svovaf)
4203 * @since 1.1.7.4
4204 *
4205 * @param object $pong
4206 * @param bool|null $is_connected
4207 */
4208 private function store_connectivity_info( $pong, $is_connected ) {
4209 $this->_logger->entrance();
4210
4211 $version = $this->get_plugin_version();
4212
4213 if ( false === $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
4214 $is_active = false;
4215 } else {
4216 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
4217 }
4218
4219 $is_active = $this->apply_filters(
4220 'is_on',
4221 $is_active,
4222 $this->is_plugin_update(),
4223 $version
4224 );
4225
4226 $this->_storage->connectivity_test = array(
4227 'is_connected' => $is_connected,
4228 'host' => $_SERVER['HTTP_HOST'],
4229 'server_ip' => WP_FS__REMOTE_ADDR,
4230 'is_active' => $is_active,
4231 'timestamp' => WP_FS__SCRIPT_START_TIME,
4232 // Last version with connectivity attempt.
4233 'version' => $version,
4234 );
4235
4236 $this->_has_api_connection = $is_connected;
4237 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
4238 }
4239
4240 /**
4241 * @author Leo Fajardo (@leorw)
4242 * @since 2.5.4
4243 *
4244 * @param bool $is_connected
4245 */
4246 private function update_connectivity_info( $is_connected ) {
4247 $this->store_connectivity_info(
4248 // This is true since we update the connection info only after a successful opt-in or license activation which means that Freemius has already been on even before the process.
4249 (object) array( 'is_active' => true ),
4250 $is_connected
4251 );
4252 }
4253
4254 /**
4255 * Force turning Freemius on.
4256 *
4257 * @author Vova Feldman (@svovaf)
4258 * @since 1.1.8.1
4259 *
4260 * @return bool TRUE if successfully turned on.
4261 */
4262 private function turn_on() {
4263 $this->_logger->entrance();
4264
4265 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
4266 return false;
4267 }
4268
4269 $updated_connectivity = $this->_storage->connectivity_test;
4270 $updated_connectivity['is_active'] = true;
4271 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
4272 $this->_storage->connectivity_test = $updated_connectivity;
4273
4274 $this->_is_on = true;
4275
4276 return true;
4277 }
4278
4279 /**
4280 * Anonymous and unique site identifier (Hash).
4281 *
4282 * @author Vova Feldman (@svovaf)
4283 * @since 1.1.0
4284 *
4285 * @param null|int $blog_id Since 2.0.0
4286 *
4287 * @return string
4288 */
4289 function get_anonymous_id( $blog_id = null ) {
4290 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
4291
4292 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
4293 $key = self::get_unfiltered_site_url( $blog_id, true );
4294
4295 $secure_auth = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
4296 if ( empty( $secure_auth ) ||
4297 false !== strpos( $secure_auth, ' ' ) ||
4298 'put your unique phrase here' === $secure_auth
4299 ) {
4300 // Protect against default auth key.
4301 $secure_auth = md5( microtime() );
4302 }
4303
4304 /**
4305 * Base the unique identifier on the WP secure authentication key. Which
4306 * turns the key into a secret anonymous identifier. This will help us
4307 * to avoid duplicate installs generation on the backend upon opt-in.
4308 *
4309 * @author Vova Feldman (@svovaf)
4310 * @since 1.2.3
4311 */
4312 $unique_id = md5( $key . $secure_auth );
4313
4314 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
4315 }
4316
4317 $this->_logger->departure( $unique_id );
4318
4319 return $unique_id;
4320 }
4321
4322 /**
4323 * Returns anonymous network ID.
4324 *
4325 * @since 2.4.3
4326 *
4327 * @return string
4328 */
4329 function get_anonymous_network_id() {
4330 return $this->get_anonymous_id( get_network()->site_id );
4331 }
4332
4333 /**
4334 * @author Vova Feldman (@svovaf)
4335 * @since 1.1.7.4
4336 *
4337 * @return \WP_User
4338 */
4339 static function _get_current_wp_user() {
4340 self::require_pluggable_essentials();
4341 self::wp_cookie_constants();
4342
4343 return wp_get_current_user();
4344 }
4345
4346 /**
4347 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
4348 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
4349 * is network activated the cookie constants are only configured after the network
4350 * plugins activation, therefore, if we don't define those constants WP will throw
4351 * PHP warnings/notices.
4352 *
4353 * @author Vova Feldman (@svovaf)
4354 * @since 2.1.1
4355 */
4356 private static function wp_cookie_constants() {
4357 if ( defined( 'LOGGED_IN_COOKIE' ) &&
4358 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
4359 ) {
4360 return;
4361 }
4362
4363 /**
4364 * Used to guarantee unique hash cookies
4365 *
4366 * @since 1.5.0
4367 */
4368 if ( ! defined( 'COOKIEHASH' ) ) {
4369 $siteurl = get_site_option( 'siteurl' );
4370 if ( $siteurl ) {
4371 define( 'COOKIEHASH', md5( $siteurl ) );
4372 } else {
4373 define( 'COOKIEHASH', '' );
4374 }
4375 }
4376
4377 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
4378 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
4379 }
4380
4381 /**
4382 * @since 2.5.0
4383 */
4384 if ( ! defined( 'AUTH_COOKIE' ) ) {
4385 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
4386 }
4387
4388 /**
4389 * @since 2.6.0
4390 */
4391 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
4392 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
4393 }
4394 }
4395
4396 /**
4397 * @author Vova Feldman (@svovaf)
4398 * @since 2.1.0
4399 *
4400 * @return int
4401 */
4402 static function get_current_wp_user_id() {
4403 $wp_user = self::_get_current_wp_user();
4404
4405 return $wp_user->ID;
4406 }
4407
4408 /**
4409 * @author Vova Feldman (@svovaf)
4410 * @since 1.2.1.7
4411 *
4412 * @param string $email
4413 *
4414 * @return bool
4415 */
4416 static function is_valid_email( $email ) {
4417 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4418 return false;
4419 }
4420
4421 $parts = explode( '@', $email );
4422
4423 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4424 return false;
4425 }
4426
4427 $blacklist = array(
4428 'admin.',
4429 'webmaster.',
4430 'localhost.',
4431 'dev.',
4432 'development.',
4433 'test.',
4434 'stage.',
4435 'staging.',
4436 );
4437
4438 // Make sure domain is not one of the blacklisted.
4439 foreach ( $blacklist as $invalid ) {
4440 if ( 0 === strpos( $parts[1], $invalid ) ) {
4441 return false;
4442 }
4443 }
4444
4445 // Get the UTF encoded domain name.
4446 /**
4447 * @note - The check of `defined('...')` is there to account for PHP servers compiled with some older version of ICU where the constants are not defined.
4448 * @author - @swashata
4449 */
4450 $is_new_idn_available = (
4451 version_compare( PHP_VERSION, '5.6.40') > 0 &&
4452 defined( 'IDNA_DEFAULT' ) &&
4453 defined( 'INTL_IDNA_VARIANT_UTS46' )
4454 );
4455 if ( $is_new_idn_available ) {
4456 $domain = idn_to_ascii( $parts[1], IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 );
4457 } else {
4458 $domain = idn_to_ascii( $parts[1] ); // phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet
4459 }
4460
4461 $domain = $domain . '.';
4462
4463 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4464 }
4465
4466 #endregion
4467
4468 #----------------------------------------------------------------------------------
4469 #region Email
4470 #----------------------------------------------------------------------------------
4471
4472 /**
4473 * Generates and sends an HTML email with customizable sections.
4474 *
4475 * @author Leo Fajardo (@leorw)
4476 * @since 1.1.2
4477 *
4478 * @param string $to_address
4479 * @param string $subject
4480 * @param array $sections
4481 * @param array $headers
4482 *
4483 * @return bool Whether the email contents were sent successfully.
4484 */
4485 private function send_email(
4486 $to_address,
4487 $subject,
4488 $sections = array(),
4489 $headers = array()
4490 ) {
4491 $default_sections = $this->get_email_sections();
4492
4493 // Insert new sections or replace the default email sections.
4494 if ( is_array( $sections ) && ! empty( $sections ) ) {
4495 foreach ( $sections as $section_id => $custom_section ) {
4496 if ( ! isset( $default_sections[ $section_id ] ) ) {
4497 // If the section does not exist, add it.
4498 $default_sections[ $section_id ] = $custom_section;
4499 } else {
4500 // If the section already exists, override it.
4501 $current_section = $default_sections[ $section_id ];
4502
4503 // Replace the current section's title if a custom section title exists.
4504 if ( isset( $custom_section['title'] ) ) {
4505 $current_section['title'] = $custom_section['title'];
4506 }
4507
4508 // Insert new rows under the current section or replace the default rows.
4509 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4510 foreach ( $custom_section['rows'] as $row_id => $row ) {
4511 $current_section['rows'][ $row_id ] = $row;
4512 }
4513 }
4514
4515 $default_sections[ $section_id ] = $current_section;
4516 }
4517 }
4518 }
4519
4520 $vars = array( 'sections' => $default_sections );
4521 $message = fs_get_template( 'email.php', $vars );
4522
4523 // Set the type of email to HTML.
4524 $headers[] = 'Content-type: text/html; charset=UTF-8';
4525
4526 $header_string = implode( "\r\n", $headers );
4527
4528 return wp_mail(
4529 $to_address,
4530 $subject,
4531 $message,
4532 $header_string
4533 );
4534 }
4535
4536 /**
4537 * Generates the data for the sections of the email content.
4538 *
4539 * @author Leo Fajardo (@leorw)
4540 * @since 1.1.2
4541 *
4542 * @return array
4543 */
4544 private function get_email_sections() {
4545 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4546 $current_user = self::_get_current_wp_user();
4547
4548 // Retrieve the cURL version information so that we can get the version number below.
4549 $curl_version_information = curl_version();
4550
4551 $active_plugin = self::get_active_plugins();
4552
4553 // Generate the list of active plugins separated by new line.
4554 $active_plugin_string = '';
4555 foreach ( $active_plugin as $plugin ) {
4556 $active_plugin_string .= sprintf(
4557 '<a href="%s">%s</a> [v%s]<br>',
4558 $plugin['PluginURI'],
4559 $plugin['Name'],
4560 $plugin['Version']
4561 );
4562 }
4563
4564 $server_ip = WP_FS__REMOTE_ADDR;
4565
4566 // Add PHP info for deeper investigation.
4567 ob_start();
4568 phpinfo();
4569 $php_info = ob_get_clean();
4570
4571 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4572
4573 // Generate the default email sections.
4574 $sections = array(
4575 'sdk' => array(
4576 'title' => 'SDK',
4577 'rows' => array(
4578 'fs_version' => array( 'FS Version', $this->version ),
4579 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4580 )
4581 ),
4582 'plugin' => array(
4583 'title' => ucfirst( $this->get_module_type() ),
4584 'rows' => array(
4585 'name' => array( 'Name', $this->get_plugin_name() ),
4586 'version' => array( 'Version', $this->get_plugin_version() )
4587 )
4588 ),
4589 'api' => array(
4590 'title' => 'API Subdomain',
4591 'rows' => array(
4592 'dns' => array(
4593 'DNS_CNAME',
4594 function_exists( 'dns_get_record' ) ?
4595 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4596 'dns_get_record() disabled/blocked'
4597 ),
4598 'ip' => array(
4599 'IP',
4600 function_exists( 'gethostbyname' ) ?
4601 gethostbyname( $api_domain ) :
4602 'gethostbyname() disabled/blocked'
4603 ),
4604 ),
4605 ),
4606 'site' => array(
4607 'title' => 'Site',
4608 'rows' => array(
4609 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4610 'address' => array( 'Address', site_url() ),
4611 'host' => array(
4612 'HTTP_HOST',
4613 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4614 ),
4615 'hosting' => array(
4616 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4617 fs_request_get( 'hosting_company' ) :
4618 'Unknown',
4619 ),
4620 'server_addr' => array(
4621 'SERVER_ADDR',
4622 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4623 )
4624 )
4625 ),
4626 'user' => array(
4627 'title' => 'User',
4628 'rows' => array(
4629 'email' => array( 'Email', $current_user->user_email ),
4630 'first' => array( 'First', $current_user->user_firstname ),
4631 'last' => array( 'Last', $current_user->user_lastname )
4632 )
4633 ),
4634 'plugins' => array(
4635 'title' => 'Plugins',
4636 'rows' => array(
4637 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4638 )
4639 ),
4640 'php_info' => array(
4641 'title' => 'PHP Info',
4642 'rows' => array(
4643 'info' => array( $php_info )
4644 ),
4645 )
4646 );
4647
4648 // Allow the sections to be modified by other code.
4649 $sections = $this->apply_filters( 'email_template_sections', $sections );
4650
4651 return $sections;
4652 }
4653
4654 #endregion
4655
4656 #----------------------------------------------------------------------------------
4657 #region Initialization
4658 #----------------------------------------------------------------------------------
4659
4660 /**
4661 * Init plugin's Freemius instance.
4662 *
4663 * @author Vova Feldman (@svovaf)
4664 * @since 1.0.1
4665 *
4666 * @param number $id
4667 * @param string $public_key
4668 * @param bool $is_live
4669 * @param bool $is_premium
4670 */
4671 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4672 $this->_logger->entrance();
4673
4674 $this->dynamic_init( array(
4675 'id' => $id,
4676 'public_key' => $public_key,
4677 'is_live' => $is_live,
4678 'is_premium' => $is_premium,
4679 ) );
4680 }
4681
4682 /**
4683 * Dynamic initiator, originally created to support initiation
4684 * with parent_id for add-ons.
4685 *
4686 * @author Vova Feldman (@svovaf)
4687 * @since 1.0.6
4688 *
4689 * @param array $plugin_info
4690 *
4691 * @throws Freemius_Exception
4692 */
4693 function dynamic_init( array $plugin_info ) {
4694 $this->_logger->entrance();
4695
4696 $this->parse_settings( $plugin_info );
4697
4698 $this->register_after_settings_parse_hooks();
4699
4700 /**
4701 * If anonymous but there's already a user entity and the user's site is associated with a valid license or trial period, update the anonymous mode accordingly.
4702 *
4703 * @todo Remove this entire `if` block after several releases as starting from this version, the anonymous mode will already be updated accordingly after a purchase.
4704 */
4705 if ( $this->is_anonymous() ) {
4706 $is_network_level = ( $this->_is_network_active && fs_is_network_admin() );
4707
4708 if (
4709 ! $is_network_level ||
4710 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
4711 ) {
4712 if ( $this->is_paying_or_trial() ) {
4713 $this->reset_anonymous_mode( $is_network_level );
4714 }
4715 } else {
4716 $network = get_network();
4717
4718 if ( is_object( $network ) ) {
4719 $main_blog_id = $network->site_id;
4720 $first_install = $this->get_install_by_blog_id( $main_blog_id );
4721
4722 if ( is_object( $first_install ) ) {
4723 $this->_storage->network_install_blog_id = $main_blog_id;
4724 $this->_storage->network_user_id = $first_install->user_id;
4725 }
4726 }
4727 }
4728 }
4729
4730 if ( $this->should_stop_execution() ) {
4731 return;
4732 }
4733
4734 if ( ! $this->is_registered() ) {
4735 if ( $this->is_anonymous() ) {
4736 // If user skipped, no need to test connectivity.
4737 $this->_has_api_connection = true;
4738 $this->_is_on = true;
4739 } else {
4740 if ( false === $this->has_api_connectivity() ) {
4741 return;
4742 } else {
4743 if ( $this->_anonymous_mode ) {
4744 // Simulate anonymous mode.
4745 $this->_is_anonymous = true;
4746 }
4747 }
4748 }
4749 }
4750
4751 /**
4752 * This should be executed even if Freemius is off for the core module,
4753 * otherwise, the add-ons dialog box won't work properly. This is especially
4754 * relevant when the developer decided to turn FS off for existing users.
4755 *
4756 * @author Vova Feldman (@svovaf)
4757 */
4758 if ( $this->is_user_in_admin() &&
4759 'plugin-information' === fs_request_get( 'tab', false ) &&
4760 $this->should_use_freemius_updater_and_dialog() &&
4761 (
4762 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4763 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4764 )
4765 ) {
4766 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4767
4768 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4769 }
4770
4771 // Check if Freemius is on for the current plugin.
4772 // This MUST be executed after all the plugin variables has been loaded.
4773 if ( ! $this->is_registered() && ! $this->is_on() ) {
4774 return;
4775 }
4776
4777 if ( $this->has_api_connectivity() ) {
4778 if ( self::is_cron() ) {
4779 $this->hook_callback_to_sync_cron();
4780 } else if ( $this->is_user_in_admin() ) {
4781 /**
4782 * Schedule daily data sync cron if:
4783 *
4784 * 1. User opted-in (for tracking).
4785 * 2. If skipped, but later upgraded (opted-in via upgrade).
4786 *
4787 * @author Vova Feldman (@svovaf)
4788 * @since 1.1.7.3
4789 *
4790 */
4791 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
4792 $this->maybe_schedule_sync_cron();
4793 }
4794
4795 /**
4796 * Check if requested for manual blocking background sync.
4797 */
4798 if ( fs_request_has( 'background_sync' ) ) {
4799 self::require_pluggable_essentials();
4800 self::wp_cookie_constants();
4801
4802 $this->run_manual_sync();
4803 }
4804 }
4805 }
4806
4807 if ( $this->is_registered() ) {
4808 FS_Clone_Manager::instance()->maybe_resolve_new_subsite_install_automatically( $this );
4809
4810 $this->hook_callback_to_install_sync();
4811 }
4812
4813 if ( $this->is_addon() ) {
4814 if ( $this->is_parent_plugin_installed() ) {
4815 // Link to parent FS.
4816 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4817
4818 // Get parent plugin reference.
4819 $this->_parent_plugin = $this->_parent->get_plugin();
4820 }
4821 }
4822
4823 if ( $this->is_user_in_admin() ) {
4824 if ( $this->is_registered() && fs_request_has( 'purchase_completed' ) ) {
4825 $this->_admin_notices->add_sticky(
4826 sprintf(
4827 /* translators: %s: License type (e.g. you have a professional license) */
4828 $this->get_text_inline( 'You have purchased a %s license.', 'you-have-x-license' ),
4829 fs_request_get( 'purchased_plan' )
4830 ) .
4831 sprintf(
4832 $this->get_text_inline(" The %s's %sdownload link%s, license key, and installation instructions have been sent to %s. If you can't find the email after 5 min, please check your spam box.", 'post-purchase-email-sent-message' ),
4833 $this->get_module_label( true ),
4834 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? "products' " : '' ),
4835 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? 's' : '' ),
4836 sprintf(
4837 '<strong>%s</strong>',
4838 fs_request_get( 'purchase_email' )
4839 )
4840 ),
4841 'plan_purchased',
4842 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
4843 );
4844 }
4845
4846 if ( $this->is_addon() ) {
4847 if ( ! $this->is_parent_plugin_installed() ) {
4848 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4849
4850 if ( isset( $plugin_info['parent'] ) ) {
4851 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4852 }
4853
4854 $this->_admin_notices->add(
4855 ( ! empty( $parent_name ) ?
4856 sprintf( $this->get_text_x_inline( '%s cannot run without %s.', 'addonX cannot run without pluginY', 'addon-x-cannot-run-without-y' ), $this->get_plugin_name(), $parent_name ) :
4857 sprintf( $this->get_text_x_inline( '%s cannot run without the plugin.', 'addonX cannot run...', 'addon-x-cannot-run-without-parent' ), $this->get_plugin_name() )
4858 ),
4859 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4860 'error'
4861 );
4862
4863 return;
4864 } else {
4865 $is_network_admin = fs_is_network_admin();
4866
4867 if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4868 // If add-on activated and parent not, automatically install parent for the user.
4869 $this->activate_parent_account( $this->_parent );
4870 } else if (
4871 $this->_parent->is_registered() &&
4872 ! $this->is_registered() &&
4873 /**
4874 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
4875 * * Network active and in network admin - network activate add-on account.
4876 * * Network active and not in network admin - activate add-on account for the current blog.
4877 * * Not network active and not in network admin - activate add-on account for the current blog.
4878 *
4879 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
4880 *
4881 * @author Leo Fajardo (@leorw)
4882 * @since 2.3.0
4883 */
4884 ( $this->is_network_active() || ! $is_network_admin )
4885 ) {
4886 $premium_license = null;
4887
4888 if (
4889 ! $this->has_free_plan() &&
4890 $this->is_bundle_license_auto_activation_enabled() &&
4891 $this->_parent->is_activated_with_bundle_license()
4892 ) {
4893 /**
4894 * If the add-on has no free plan, try to activate the account only when there's a bundle license.
4895 *
4896 * @author Leo Fajardo (@leorw)
4897 * @since 2.4.0
4898 */
4899 $bundle_license = $this->get_active_parent_license( $this->_parent->_get_license()->secret_key, false );
4900
4901 if (
4902 is_object( $bundle_license ) &&
4903 ! empty( $bundle_license->products ) &&
4904 in_array( $this->get_id(), $bundle_license->products )
4905 ) {
4906 $premium_license = $bundle_license;
4907 }
4908 }
4909
4910 if ( $this->has_free_plan() || is_object( $premium_license) ) {
4911 // If parent plugin activated, automatically install add-on for the user.
4912 $this->_activate_addon_account(
4913 $this->_parent,
4914 ( $this->is_network_active() && $is_network_admin ) ?
4915 true :
4916 get_current_blog_id(),
4917 $premium_license
4918 );
4919 }
4920 }
4921
4922 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4923 if ( $this->is_premium() ) {
4924 // Remove add-on download admin-notice.
4925 $this->_parent->_admin_notices->remove_sticky( array(
4926 'addon_plan_upgraded_' . $this->_slug,
4927 'no_addon_license_' . $this->_slug,
4928 ) );
4929 }
4930
4931 // $this->deactivate_premium_only_addon_without_license();
4932 }
4933 }
4934
4935 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
4936
4937 // if ( $this->is_registered() ||
4938 // $this->is_anonymous() ||
4939 // $this->is_pending_activation()
4940 // ) {
4941 // $this->_init_admin();
4942 // }
4943 }
4944
4945 /**
4946 * Should be called outside `$this->is_user_in_admin()` scope
4947 * because the updater has some logic that needs to be executed
4948 * during AJAX calls.
4949 *
4950 * Currently, we need to hook to the `http_request_host_is_external` filter.
4951 * In the future, there might be additional logic added.
4952 *
4953 * @author Vova Feldman
4954 * @since 1.2.1.6
4955 */
4956 if (
4957 $this->should_use_freemius_updater_and_dialog() &&
4958 (
4959 $this->is_premium() ||
4960 /**
4961 * If not premium but the premium version is installed, also instantiate the updater so that the
4962 * plugin information dialog of the premium version will have the information from the server.
4963 *
4964 * @author Leo Fajardo (@leorw)
4965 * @since 2.2.3
4966 */
4967 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
4968 ) &&
4969 $this->has_release_on_freemius() &&
4970 ( ! $this->is_unresolved_clone( true ) )
4971 ) {
4972 FS_Plugin_Updater::instance( $this );
4973 }
4974
4975 $this->do_action( 'initiated' );
4976
4977 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
4978 if ( isset( $this->_storage->prev_is_premium ) ) {
4979 $this->apply_filters(
4980 'after_code_type_change',
4981 // New code type.
4982 $this->_plugin->is_premium
4983 );
4984 } else {
4985 // Set for code type for the first time.
4986 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
4987 }
4988 }
4989
4990 if ( ! $this->is_addon() ) {
4991 if ( $this->is_registered() ) {
4992 // Fix for upgrade from versions < 1.0.9.
4993 if ( ! isset( $this->_storage->activation_timestamp ) ) {
4994 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
4995 }
4996
4997 $this->do_action( 'after_init_plugin_registered' );
4998 } else if ( $this->is_anonymous() ) {
4999 $this->do_action( 'after_init_plugin_anonymous' );
5000 } else if ( $this->is_pending_activation() ) {
5001 $this->do_action( 'after_init_plugin_pending_activations' );
5002 }
5003 } else {
5004 if ( $this->is_registered() ) {
5005 $this->do_action( 'after_init_addon_registered' );
5006 } else if ( $this->is_anonymous() ) {
5007 $this->do_action( 'after_init_addon_anonymous' );
5008 } else if ( $this->is_pending_activation() ) {
5009 $this->do_action( 'after_init_addon_pending_activations' );
5010 }
5011 }
5012 }
5013
5014 /**
5015 * @author Leo Fajardo (@leorw)
5016 * @since 2.2.3
5017 *
5018 * @return bool
5019 */
5020 private function should_use_freemius_updater_and_dialog() {
5021 return (
5022 /**
5023 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
5024 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
5025 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
5026 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
5027 * plugin details from .org).
5028 */
5029 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
5030 (
5031 ! self::is_plugin_install_page() &&
5032 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
5033 ( 'install-plugin' !== fs_request_get( 'action' ) )
5034 )
5035 );
5036 }
5037
5038 /**
5039 * @param string[] $permissions
5040 * @param bool $is_enabled
5041 * @param int|null $blog_id
5042 *
5043 * @return true|object `true` on success, API error object on failure.
5044 */
5045 private function update_site_permissions( array $permissions, $is_enabled, $blog_id = null ) {
5046 $this->_logger->entrance();
5047
5048 $params = array(
5049 'permissions' => implode( ',', $permissions ),
5050 'is_enabled' => $is_enabled,
5051 );
5052
5053 $current_blog_id = get_current_blog_id();
5054 $is_blog_switched = false;
5055 if ( is_numeric( $blog_id ) && $current_blog_id != $blog_id ) {
5056 $is_blog_switched = $this->switch_to_blog( $blog_id );
5057 }
5058
5059 $result = $this->api_site_call( '/permissions.json', 'put', $params );
5060
5061 if ( $is_blog_switched ) {
5062 $this->switch_to_blog( $current_blog_id );
5063 }
5064
5065 if (
5066 ! $this->is_api_result_object( $result ) ||
5067 ! isset( $result->install_id )
5068 ) {
5069 $this->_logger->api_error( $result );
5070
5071 return $result;
5072 }
5073
5074 return true;
5075 }
5076
5077 /**
5078 * @param string[] $permissions
5079 * @param bool $is_enabled
5080 * @param bool $has_site_delegated_connection
5081 *
5082 * @return true|object `true` on success, API error object on failure.
5083 */
5084 private function update_network_permissions(
5085 array $permissions,
5086 $is_enabled,
5087 &$has_site_delegated_connection
5088 ) {
5089 $this->_logger->entrance();
5090
5091 $install_id_2_blog_id = array();
5092 $install_by_blog_id = $this->get_blog_install_map();
5093
5094 $has_site_delegated_connection = false;
5095
5096 foreach ( $install_by_blog_id as $blog_id => $install ) {
5097 if ( $this->is_site_delegated_connection( $blog_id ) ) {
5098 // Only update permissions of non-delegated installs.
5099 $has_site_delegated_connection = true;
5100 continue;
5101 }
5102
5103 $install_id_2_blog_id[ $install->id ] = $blog_id;
5104 }
5105
5106 if ( empty( $install_id_2_blog_id ) ) {
5107 return true;
5108 }
5109
5110 $params = array(
5111 'permissions' => implode( ',', $permissions ),
5112 'is_enabled' => $is_enabled,
5113 'install_ids' => implode( ',', array_keys( $install_id_2_blog_id ) ),
5114 );
5115
5116 // Send update to FS.
5117 $result = $this->get_current_or_network_user_api_scope()->call(
5118 "/plugins/{$this->_module_id}/installs/permissions.json",
5119 'put',
5120 $params
5121 );
5122
5123 if ( ! $this->is_api_result_object( $result, 'installs_metadata' ) ) {
5124 $this->_logger->api_error( $result );
5125
5126 return $result;
5127 }
5128
5129 return true;
5130 }
5131
5132 /**
5133 * @param mixed $result
5134 *
5135 * @return string
5136 */
5137 private function get_api_error_message( $result ) {
5138 $error_message = sprintf( $this->get_text_inline( 'There was an unexpected API error while processing your request. Please try again in a few minutes and if it still doesn\'t work, contact the %s\'s author with the following:',
5139 'unexpected-api-error' ), $this->_module_type ) . ' ';
5140
5141 if (
5142 $this->is_api_error( $result ) &&
5143 isset( $result->error )
5144 ) {
5145 $code = empty( $result->error->code ) ? '' : " Code: {$result->error->code}";
5146
5147 $error_message .= "<b>{$result->error->message}{$code}</b>";
5148 } else {
5149 $error_message .= var_export( $result, true );
5150 }
5151
5152 return $error_message;
5153 }
5154
5155 /**
5156 * @author Vova Feldman (@svovaf)
5157 * @since 2.5.1
5158 */
5159 function _toggle_permission_tracking_callback() {
5160 $this->_logger->entrance();
5161
5162 $this->check_ajax_referer( 'toggle_permission_tracking' );
5163
5164 if ( ! $this->is_registered( true ) ) {
5165 self::shoot_ajax_failure( 'User never opted-in.' );
5166 }
5167
5168 $is_enabled = fs_request_get_bool( 'is_enabled' );
5169 $permissions = fs_request_get( 'permissions' );
5170
5171 if ( ! is_string( $permissions ) ) {
5172 self::shoot_ajax_failure( 'The permissions param must be a string.' );
5173 }
5174
5175 $permissions = explode( ',', $permissions );
5176
5177 $result = $this->toggle_permission_tracking( $permissions, $is_enabled );
5178
5179 if ( true !== $result ) {
5180 self::shoot_ajax_failure( $this->get_api_error_message( $result ) );
5181 }
5182
5183 self::shoot_ajax_success();
5184 }
5185
5186 /**
5187 * @param string[] $permissions
5188 * @param bool $is_enabled
5189 * @param int|null $blog_id
5190 *
5191 * @return bool|mixed `true` if updated successfully or no update is needed.
5192 */
5193 private function toggle_permission_tracking( $permissions, $is_enabled, $blog_id = null ) {
5194 if ( ! $this->is_registered( true ) ) {
5195 // User never opted-in.
5196 return false;
5197 }
5198
5199 // Check if permissions are already set as needed.
5200 if ( FS_Permission_Manager::instance( $this )->are_permissions( $permissions, $is_enabled, $blog_id ) ) {
5201 /**
5202 * Note:
5203 * When running on the network admin, there's no need to iterate through all the installs individually since network opt-in permissions are managed for ALL non-delegated installs through a single option (per permission) on the network-level storage.
5204 */
5205 return true;
5206 }
5207
5208 $api_managed_permissions = array_intersect(
5209 $permissions,
5210 FS_Permission_Manager::get_api_managed_permission_ids()
5211 );
5212
5213 if (
5214 in_array( FS_Permission_Manager::PERMISSION_ESSENTIALS, $permissions ) &&
5215 ! in_array( FS_Permission_Manager::PERMISSION_SITE, $permissions )
5216 ) {
5217 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_SITE;
5218 }
5219
5220 if ( ! empty( $api_managed_permissions ) ) {
5221 $has_site_delegated_connection = false;
5222
5223 if (
5224 ! $is_enabled &&
5225 ! in_array( FS_Permission_Manager::PERMISSION_EXTENSIONS, $api_managed_permissions ) &&
5226 false === FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed( $blog_id )
5227 ) {
5228 /**
5229 * If we are turning off a permission and the extensions permission is off too, enrich the permissions update request to also turn off extensions tracking, as currently when opting in with extensions tracking disabled the extensions tracking is off but the API isn't aware of it.
5230 *
5231 * @todo Remove this entire `if` after implementing granular opt-in that also sends the permissions to the API when opting in.
5232 */
5233 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_EXTENSIONS;
5234 }
5235
5236 if ( is_null( $blog_id ) && fs_is_network_admin() ) {
5237 $result = $this->update_network_permissions(
5238 $api_managed_permissions,
5239 $is_enabled,
5240 $has_site_delegated_connection
5241 );
5242 } else {
5243 $result = $this->update_site_permissions(
5244 $api_managed_permissions,
5245 $is_enabled,
5246 $blog_id
5247 );
5248 }
5249
5250 if ( true !== $result ) {
5251 return $result;
5252 }
5253
5254 if ( in_array( FS_Permission_Manager::PERMISSION_SITE, $api_managed_permissions ) ) {
5255 if ( $is_enabled ) {
5256 $this->schedule_sync_cron();
5257 } else {
5258 $this->clear_sync_cron( ! $has_site_delegated_connection );
5259 }
5260 }
5261
5262 if ( in_array( FS_Permission_Manager::PERMISSION_USER, $api_managed_permissions ) ) {
5263 $this->toggle_user_permission( $is_enabled, $blog_id );
5264 }
5265 }
5266
5267 $this->update_tracking_permissions(
5268 $permissions,
5269 $is_enabled,
5270 $blog_id
5271 );
5272
5273 return true;
5274 }
5275
5276 /**
5277 * @param bool $is_enabled
5278 * @param int|null $blog_id
5279 */
5280 private function toggle_user_permission( $is_enabled, $blog_id = null ) {
5281 $network_or_blog_ids = is_numeric( $blog_id ) ?
5282 $blog_id :
5283 fs_is_network_admin();
5284
5285 if ( $is_enabled ) {
5286 $this->reset_anonymous_mode( $network_or_blog_ids );
5287 } else {
5288 $this->skip_connection( $network_or_blog_ids );
5289 }
5290 }
5291
5292 /**
5293 * Opt-in back into usage tracking.
5294 *
5295 * Note: This will only work if the user opted-in previously.
5296 *
5297 * Returns:
5298 * 1. FALSE - If the user never opted-in.
5299 * 2. TRUE - If successfully opted-in back to usage tracking.
5300 * 3. object - API result on failure.
5301 *
5302 * @author Leo Fajardo (@leorw)
5303 * @since 1.2.1.5
5304 *
5305 * @bool $is_enabled
5306 *
5307 * @return bool|object
5308 */
5309 private function toggle_site_tracking( $is_enabled, $blog_id = null ) {
5310 $this->_logger->entrance();
5311
5312 return $this->toggle_permission_tracking(
5313 FS_Permission_Manager::instance( $this )->get_site_tracking_permission_names(),
5314 $is_enabled,
5315 $blog_id
5316 );
5317 }
5318
5319 /**
5320 * If user opted-in and later disabled usage-tracking,
5321 * re-allow tracking for licensing and updates.
5322 *
5323 * @author Leo Fajardo (@leorw)
5324 * @since 1.2.1.5
5325 *
5326 * @param bool $is_context_single_site
5327 */
5328 private function reconnect_locally( $is_context_single_site = false ) {
5329 $this->_logger->entrance();
5330
5331 if ( ! $this->is_registered() ) {
5332 return;
5333 }
5334
5335 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5336 if ( $this->is_tracking_prohibited() ) {
5337 FS_Permission_Manager::instance( $this )->update_site_tracking( true );
5338 }
5339 } else {
5340 $installs_map = $this->get_blog_install_map();
5341 foreach ( $installs_map as $blog_id => $install ) {
5342 /**
5343 * @var FS_Site $install
5344 */
5345 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
5346 FS_Permission_Manager::instance( $this )->update_site_tracking( true, $blog_id );
5347 }
5348 }
5349 }
5350 }
5351
5352 /**
5353 * Update permission tracking flags. When updating in a network context, in addition to updating the network-level flags, also update the permissions on the site-level for all non-delegated sites.
5354 *
5355 * @param string[] $permissions
5356 * @param bool $is_enabled
5357 * @param int|null $blog_id
5358 *
5359 * @return array
5360 */
5361 private function update_tracking_permissions( $permissions, $is_enabled, $blog_id = null ) {
5362 // Alias.
5363 $permission_manager = FS_Permission_Manager::instance( $this );
5364
5365 $network_or_blog_ids = is_numeric( $blog_id ) ?
5366 $blog_id :
5367 fs_is_network_admin();
5368
5369 if ( true === $network_or_blog_ids ) {
5370 // Update the permission for all non-delegated sub-sites.
5371 $blog_ids = $this->get_non_delegated_blog_ids();
5372
5373 // Add the network-level to the array, to update the permission on the network-level storage.
5374 array_unshift( $blog_ids, null );
5375 }
5376 else
5377 {
5378 if ( false === $network_or_blog_ids ) {
5379 $network_or_blog_ids = null;
5380 }
5381
5382 $blog_ids = is_array( $network_or_blog_ids ) ?
5383 $network_or_blog_ids :
5384 array( $network_or_blog_ids );
5385 }
5386
5387 $result = array();
5388 foreach ( $permissions as $permission ) {
5389 $permission = trim( $permission );
5390 $is_permission_supported = true;
5391
5392 foreach ( $blog_ids as $id ) {
5393 $is_permission_supported = $permission_manager->update_permission_tracking_flag(
5394 $permission,
5395 $is_enabled,
5396 $id
5397 );
5398 }
5399
5400 if ( ! $is_permission_supported ) {
5401 $permission = 'no_match';
5402 }
5403
5404 $result[ $permission ] = $is_enabled;
5405 }
5406
5407 return $result;
5408 }
5409
5410 /**
5411 * Parse plugin's settings (as defined by the plugin dev).
5412 *
5413 * @author Vova Feldman (@svovaf)
5414 * @since 1.1.7.3
5415 *
5416 * @param array $plugin_info
5417 *
5418 * @throws \Freemius_Exception
5419 */
5420 private function parse_settings( &$plugin_info ) {
5421 $this->_logger->entrance();
5422
5423 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5424 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5425 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5426 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5427 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5428
5429 /**
5430 * @author Vova Feldman (@svovaf)
5431 * @since 1.1.9 Try to pull secret key from external config.
5432 */
5433 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5434 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5435 }
5436
5437 if ( isset( $plugin_info['parent'] ) ) {
5438 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5439 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5440 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5441 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5442 }
5443
5444 if ( false === $id ) {
5445 throw new Freemius_Exception( array(
5446 'error' => array(
5447 'type' => 'ParameterNotSet',
5448 'message' => 'Plugin id parameter is not set.',
5449 'code' => 'plugin_id_not_set',
5450 'http' => 500,
5451 )
5452 ) );
5453 }
5454 if ( false === $public_key ) {
5455 throw new Freemius_Exception( array(
5456 'error' => array(
5457 'type' => 'ParameterNotSet',
5458 'message' => 'Plugin public_key parameter is not set.',
5459 'code' => 'plugin_public_key_not_set',
5460 'http' => 500,
5461 )
5462 ) );
5463 }
5464
5465 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5466 $this->_plugin :
5467 new FS_Plugin();
5468
5469 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5470
5471 $plugin->update( array(
5472 'id' => $id,
5473 'type' => $this->get_option( $plugin_info, 'type', $this->_module_type ),
5474 'public_key' => $public_key,
5475 'slug' => $this->_slug,
5476 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5477 'parent_plugin_id' => $parent_id,
5478 'version' => $this->get_plugin_version(),
5479 'title' => $this->get_plugin_name( $premium_suffix ),
5480 'file' => $this->_plugin_basename,
5481 'is_premium' => $this->get_bool_option( $plugin_info, 'is_premium', true ),
5482 'premium_suffix' => $premium_suffix,
5483 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5484 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5485 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5486 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ),
5487 'opt_in_moderation' => $this->get_option( $plugin_info, 'opt_in', null ),
5488 ) );
5489
5490 if ( $plugin->is_updated() ) {
5491 // Update plugin details.
5492 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5493 }
5494 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5495 $this->_plugin->secret_key = $secret_key;
5496
5497 /**
5498 * If the product is network integrated and activated and the current view is in the network level Admin dashboard, if the product's network-level menu located differently from the sub-site level, then use the network menu details (when set).
5499 *
5500 * @author Vova Feldman
5501 * @since 2.4.5
5502 */
5503 if ( $this->is_network_active() && fs_is_network_admin() ) {
5504 if ( isset( $plugin_info['menu_network'] ) &&
5505 is_array( $plugin_info['menu_network'] ) &&
5506 ! empty( $plugin_info['menu_network'] )
5507 ) {
5508 $plugin_info['menu'] = $plugin_info['menu_network'];
5509 }
5510 }
5511
5512 if ( ! isset( $plugin_info['menu'] ) ) {
5513 $plugin_info['menu'] = array();
5514
5515 if ( ! empty( $this->_storage->sdk_last_version ) &&
5516 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5517 ) {
5518 // Backward compatibility to 1.1.2
5519 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5520 $plugin_info['menu_slug'] :
5521 $this->_slug;
5522 }
5523 }
5524
5525 $this->_menu = FS_Admin_Menu_Manager::instance(
5526 $this->_module_id,
5527 $this->_module_type,
5528 $this->get_unique_affix()
5529 );
5530
5531 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5532
5533 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5534 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5535 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5536 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5537 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5538 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5539 if ( $this->_is_premium_only ) {
5540 // If premium only plugin, disable anonymous mode.
5541 $this->_enable_anonymous = false;
5542 $this->_anonymous_mode = false;
5543 } else {
5544 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5545 $this->_anonymous_mode = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
5546 }
5547 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5548 $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
5549
5550 if ( ! empty( $plugin_info['trial'] ) ) {
5551 $this->_trial_days = $this->get_numeric_option(
5552 $plugin_info['trial'],
5553 'days',
5554 // Default to 0 - trial without days specification.
5555 0
5556 );
5557
5558 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5559 }
5560
5561 $this->_navigation = $this->get_option(
5562 $plugin_info,
5563 'navigation',
5564 $this->is_free_wp_org_theme() ?
5565 self::NAVIGATION_TABS :
5566 self::NAVIGATION_MENU
5567 );
5568 }
5569
5570 /**
5571 * @param string[] $options
5572 * @param string $key
5573 * @param mixed $default
5574 *
5575 * @return bool
5576 */
5577 private function get_option( &$options, $key, $default = false ) {
5578 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5579 }
5580
5581 private function get_bool_option( &$options, $key, $default = false ) {
5582 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5583 }
5584
5585 private function get_numeric_option( &$options, $key, $default = false ) {
5586 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5587 }
5588
5589 /**
5590 * Gate keeper.
5591 *
5592 * @author Vova Feldman (@svovaf)
5593 * @since 1.1.7.3
5594 *
5595 * @return bool
5596 */
5597 private function should_stop_execution() {
5598 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5599 /**
5600 * Don't execute Freemius until plugin was fully loaded at least once,
5601 * to give the opportunity for the activation hook to run before pinging
5602 * the API for connectivity test. This logic is relevant for the
5603 * identification of new plugin install vs. plugin update.
5604 *
5605 * @author Vova Feldman (@svovaf)
5606 * @since 1.1.9
5607 */
5608 return true;
5609 }
5610
5611 if ( $this->is_activation_mode() ) {
5612 if ( ! is_admin() ) {
5613 /**
5614 * If in activation mode, don't execute Freemius outside the admin dashboard.
5615 *
5616 * @author Vova Feldman (@svovaf)
5617 * @since 1.1.7.3
5618 */
5619 return true;
5620 }
5621
5622 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5623 /**
5624 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5625 * then don't start Freemius.
5626 *
5627 * @author Vova Feldman (@svovaf)
5628 * @since 1.1.6.3
5629 *
5630 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5631 */
5632 return true;
5633 }
5634
5635 if ( self::is_cron() ) {
5636 /**
5637 * If in activation mode, don't execute Freemius during wp crons
5638 * (wp crons have HTTP context - called as HTTP request).
5639 *
5640 * @author Vova Feldman (@svovaf)
5641 * @since 1.1.7.3
5642 */
5643 return true;
5644 }
5645
5646 if ( self::is_ajax() ) {
5647 /**
5648 * During activation, if running in AJAX mode, unless there's a sticky
5649 * connectivity issue notice, don't run Freemius.
5650 *
5651 * @author Vova Feldman (@svovaf)
5652 * @since 1.1.7.3
5653 */
5654 return true;
5655 }
5656 }
5657
5658 return false;
5659 }
5660
5661 /**
5662 * Triggered after code type has changed.
5663 *
5664 * @author Vova Feldman (@svovaf)
5665 * @since 1.1.9.1
5666 */
5667 function _after_code_type_change() {
5668 $this->_logger->entrance();
5669
5670 if ( $this->is_theme() ) {
5671 // Expire the cache of the previous tabs since the theme may
5672 // have setting updates after code type has changed.
5673 $this->_cache->expire( 'tabs' );
5674 $this->_cache->expire( 'tabs_stylesheets' );
5675 }
5676
5677 if ( ! $this->is_addon() ) {
5678 add_action(
5679 is_admin() ? 'admin_init' : 'init',
5680 array( &$this, '_plugin_code_type_changed' )
5681 );
5682 }
5683
5684 if ( $this->is_registered() && $this->is_premium() ) {
5685 // Purge cached payments after switching to the premium version.
5686 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5687 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5688 }
5689 }
5690
5691 /**
5692 * Handles plugin's code type change (free <--> premium).
5693 *
5694 * @author Vova Feldman (@svovaf)
5695 * @since 1.0.9
5696 */
5697 function _plugin_code_type_changed() {
5698 $this->_logger->entrance();
5699
5700 if ( $this->is_premium() ) {
5701 $this->reconnect_locally();
5702
5703 // Activated premium code.
5704 $this->do_action( 'after_premium_version_activation' );
5705
5706 // Remove all sticky messages related to download of the premium version.
5707 $this->_admin_notices->remove_sticky( array(
5708 'trial_started',
5709 'plan_upgraded',
5710 'plan_changed',
5711 'license_activated',
5712 ) );
5713
5714 $notice = '';
5715 if ( ! $this->is_only_premium() ) {
5716 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5717 }
5718
5719 $license_notice = $this->get_license_network_activation_notice();
5720 if ( ! empty( $license_notice ) ) {
5721 $notice .= ' ' . $license_notice;
5722 }
5723
5724 if ( ! empty( $notice ) ) {
5725 $this->_admin_notices->add_sticky(
5726 trim( $notice ),
5727 'premium_activated',
5728 $this->get_text_x_inline( 'W00t',
5729 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5730 );
5731 }
5732 } else {
5733 // Remove sticky message related to premium code activation.
5734 $this->_admin_notices->remove_sticky( 'premium_activated' );
5735
5736 // Activated free code (after had the premium before).
5737 $this->do_action( 'after_free_version_reactivation' );
5738
5739 if ( $this->is_paying() && ! $this->is_premium() ) {
5740 $this->add_complete_upgrade_instructions_notice(
5741 sprintf(
5742 /* translators: %s: License type (e.g. you have a professional license) */
5743 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5744 $this->get_plan_title()
5745 ),
5746 'plan_upgraded'
5747 );
5748 }
5749 }
5750
5751 if ( $this->is_registered() ) {
5752 // Schedule code type changes event.
5753 $this->schedule_install_sync();
5754 }
5755
5756 /**
5757 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5758 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5759 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5760 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5761 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5762 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5763 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5764 */
5765 $this->unregister_uninstall_hook();
5766
5767 $this->clear_module_main_file_cache();
5768
5769 // Update is_premium of latest version.
5770 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5771 }
5772
5773 #endregion
5774
5775 #----------------------------------------------------------------------------------
5776 #region Add-ons
5777 #----------------------------------------------------------------------------------
5778
5779 /**
5780 * Check if add-on installed and activated on site.
5781 *
5782 * @author Vova Feldman (@svovaf)
5783 * @since 1.0.6
5784 *
5785 * @param string|number $id_or_slug
5786 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5787 *
5788 * @return bool
5789 */
5790 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5791 $this->_logger->entrance();
5792
5793 $addon_id = self::get_module_id( $id_or_slug );
5794 $is_activated = self::has_instance( $addon_id );
5795
5796 if ( ! $is_activated ) {
5797 return false;
5798 }
5799
5800 if ( is_bool( $is_premium ) ) {
5801 // Check if the specified code version is activate.
5802 $addon = $this->get_addon_instance( $addon_id );
5803 $is_activated = ( $is_premium === $addon->is_premium() );
5804 }
5805
5806 return $is_activated;
5807 }
5808
5809 /**
5810 * Check if add-on was connected to install
5811 *
5812 * @author Vova Feldman (@svovaf)
5813 * @since 1.1.7
5814 *
5815 * @param string|number $id_or_slug
5816 *
5817 * @return bool
5818 */
5819 function is_addon_connected( $id_or_slug ) {
5820 $this->_logger->entrance();
5821
5822 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5823
5824 $addon_id = self::get_module_id( $id_or_slug );
5825 $addon = $this->get_addon( $addon_id );
5826 $slug = $addon->slug;
5827 if ( ! isset( $sites[ $slug ] ) ) {
5828 return false;
5829 }
5830
5831 $site = $sites[ $slug ];
5832
5833 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5834
5835 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5836 // The given slug do NOT belong to any of the plugin's add-ons.
5837 return false;
5838 }
5839
5840 return ( is_object( $site ) &&
5841 is_numeric( $site->id ) &&
5842 is_numeric( $site->user_id ) &&
5843 FS_Plugin_Plan::is_valid_id( $site->plan_id )
5844 );
5845 }
5846
5847 /**
5848 * Determines if add-on installed.
5849 *
5850 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
5851 *
5852 * @author Vova Feldman (@svovaf)
5853 * @since 1.0.6
5854 *
5855 * @param string|number $id_or_slug
5856 *
5857 * @return bool
5858 */
5859 function is_addon_installed( $id_or_slug ) {
5860 $this->_logger->entrance();
5861
5862 $addon_id = self::get_module_id( $id_or_slug );
5863
5864 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
5865 }
5866
5867 /**
5868 * Get add-on basename.
5869 *
5870 * @author Vova Feldman (@svovaf)
5871 * @since 1.0.6
5872 *
5873 * @param string|number $id_or_slug
5874 *
5875 * @return string
5876 */
5877 function get_addon_basename( $id_or_slug ) {
5878 $addon_id = self::get_module_id( $id_or_slug );
5879
5880 if ( $this->is_addon_activated( $addon_id ) ) {
5881 return self::instance( $addon_id )->get_plugin_basename();
5882 }
5883
5884 $addon = $this->get_addon( $addon_id );
5885 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
5886
5887 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
5888 return $premium_basename;
5889 }
5890
5891 $all_plugins = $this->get_all_plugins();
5892
5893 foreach ( $all_plugins as $basename => $data ) {
5894 if ( $addon->slug === $data['slug'] ||
5895 $addon->premium_slug === $data['slug']
5896 ) {
5897 return $basename;
5898 }
5899 }
5900
5901 $free_basename = "{$addon->slug}/{$addon->slug}.php";
5902
5903 return $free_basename;
5904 }
5905
5906 /**
5907 * Get installed add-ons instances.
5908 *
5909 * @author Vova Feldman (@svovaf)
5910 * @since 1.0.6
5911 *
5912 * @return Freemius[]
5913 */
5914 function get_installed_addons() {
5915 if ( $this->is_addon() ) {
5916 // Add-on cannot have add-ons.
5917 return array();
5918 }
5919
5920 $installed_addons = array();
5921
5922 foreach ( self::$_instances as $instance ) {
5923 if ( $instance->is_addon_of( $this->_plugin->id ) ) {
5924 $installed_addons[] = $instance;
5925 }
5926 }
5927
5928 return $installed_addons;
5929 }
5930
5931 /**
5932 * Check if any add-ons of the plugin are installed.
5933 *
5934 * @author Leo Fajardo (@leorw)
5935 * @since 1.1.1
5936 *
5937 * @return bool
5938 */
5939 function has_installed_addons() {
5940 if ( ! $this->has_addons() ) {
5941 return false;
5942 }
5943
5944 foreach ( self::$_instances as $instance ) {
5945 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
5946 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
5947 return true;
5948 }
5949 }
5950 }
5951
5952 return false;
5953 }
5954
5955 /**
5956 * Tell Freemius that the current plugin is an add-on.
5957 *
5958 * @author Vova Feldman (@svovaf)
5959 * @since 1.0.6
5960 *
5961 * @param number $parent_plugin_id The parent plugin ID
5962 */
5963 function init_addon( $parent_plugin_id ) {
5964 $this->_plugin->parent_plugin_id = $parent_plugin_id;
5965 }
5966
5967 /**
5968 * @author Vova Feldman (@svovaf)
5969 * @since 1.0.6
5970 *
5971 * @return bool
5972 */
5973 function is_addon() {
5974 return (
5975 isset( $this->_plugin->parent_plugin_id ) &&
5976 is_numeric( $this->_plugin->parent_plugin_id )
5977 );
5978 }
5979
5980 /**
5981 * @author Vova Feldman (@svovaf)
5982 * @since 2.3.2
5983 *
5984 * @param number $parent_product_id
5985 *
5986 * @return bool
5987 */
5988 function is_addon_of( $parent_product_id ) {
5989 return (
5990 $this->is_addon() &&
5991 $parent_product_id == $this->_plugin->parent_plugin_id
5992 );
5993 }
5994
5995 /**
5996 * Deactivate add-on if it's premium only and the user does't have a valid license.
5997 *
5998 * @param bool $is_after_trial_cancel
5999 *
6000 * @return bool If add-on was deactivated.
6001 */
6002 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
6003 if ( ! $this->has_free_plan() &&
6004 ! $this->has_features_enabled_license() &&
6005 ! $this->_has_premium_license()
6006 ) {
6007 if ( $this->is_registered() ) {
6008 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
6009 // if (empty($this->_storage->activation_timestamp) ||
6010 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
6011 // ) {
6012 /**
6013 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
6014 *
6015 * Retry syncing the user add-on licenses.
6016 */
6017 // Sync licenses.
6018 $this->_sync_licenses();
6019 // }
6020
6021 // Try to activate premium license.
6022 $this->_activate_license( true );
6023 }
6024
6025 if ( ! $this->has_free_plan() &&
6026 ! $this->has_features_enabled_license() &&
6027 ! $this->_has_premium_license()
6028 ) {
6029 // @todo Check if deactivate plugins also call the deactivation hook.
6030
6031 $this->_parent->_admin_notices->add_sticky(
6032 sprintf(
6033 ( $is_after_trial_cancel ?
6034 $this->_parent->get_text_inline(
6035 '%s free trial was successfully cancelled. Since the add-on is premium only it was automatically deactivated. If you like to use it in the future, you\'ll have to purchase a license.',
6036 'addon-trial-cancelled-message'
6037 ) :
6038 $this->_parent->get_text_inline(
6039 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
6040 'addon-no-license-message'
6041 )
6042 ),
6043 '<b>' . $this->_plugin->title . '</b>'
6044 ) . ' ' . sprintf(
6045 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
6046 $this->_parent->addon_url( $this->_slug ),
6047 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
6048 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
6049 ),
6050 'no_addon_license_' . $this->_slug,
6051 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
6052 ( $is_after_trial_cancel ? 'success' : 'error' )
6053 );
6054
6055 deactivate_plugins( array( $this->_plugin_basename ), true );
6056
6057 return true;
6058 }
6059 }
6060
6061 return false;
6062 }
6063
6064 #endregion
6065
6066 #----------------------------------------------------------------------------------
6067 #region Sandbox
6068 #----------------------------------------------------------------------------------
6069
6070 /**
6071 * Set Freemius into sandbox mode for debugging.
6072 *
6073 * @author Vova Feldman (@svovaf)
6074 * @since 1.0.4
6075 *
6076 * @param string $secret_key
6077 */
6078 function init_sandbox( $secret_key ) {
6079 $this->_plugin->secret_key = $secret_key;
6080
6081 // Update plugin details.
6082 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
6083 }
6084
6085 /**
6086 * Check if running payments in sandbox mode.
6087 *
6088 * @author Vova Feldman (@svovaf)
6089 * @since 1.0.4
6090 *
6091 * @return bool
6092 */
6093 function is_payments_sandbox() {
6094 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
6095 }
6096
6097 #endregion
6098
6099 /**
6100 * Check if running test vs. live plugin.
6101 *
6102 * @author Vova Feldman (@svovaf)
6103 * @since 1.0.5
6104 *
6105 * @return bool
6106 */
6107 function is_live() {
6108 return $this->_plugin->is_live;
6109 }
6110
6111 /**
6112 * Check if super-admin skipped connection for all sites in the network.
6113 *
6114 * @author Vova Feldman (@svovaf)
6115 * @since 2.0.0
6116 */
6117 function is_network_anonymous() {
6118 if ( ! $this->_is_network_active ) {
6119 return false;
6120 }
6121
6122 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
6123
6124 if ( empty( $is_anonymous_ms ) ) {
6125 return false;
6126 }
6127
6128 return $is_anonymous_ms['is'];
6129 }
6130
6131 /**
6132 * Check if super-admin opted-in for all sites in the network.
6133 *
6134 * @author Vova Feldman (@svovaf)
6135 * @since 2.0.0
6136 */
6137 function is_network_connected() {
6138 if ( ! $this->_is_network_active ) {
6139 return false;
6140 }
6141
6142 return $this->_storage->get( 'is_network_connected' );
6143 }
6144
6145 /**
6146 * Check if the user skipped connecting the account with Freemius.
6147 *
6148 * @author Vova Feldman (@svovaf)
6149 * @since 1.0.7
6150 *
6151 * @return bool
6152 */
6153 function is_anonymous() {
6154 if ( ! isset( $this->_is_anonymous ) ) {
6155 if ( $this->is_network_anonymous() ) {
6156 $this->_is_anonymous = true;
6157 } else if ( fs_is_network_admin() ) {
6158 /**
6159 * When not-network-anonymous, yet, running in the network admin, consider as anonymous only when ALL non-delegated sites are set to anonymous.
6160 */
6161 $non_delegated_sites = $this->get_non_delegated_blog_ids();
6162
6163 foreach ( $non_delegated_sites as $blog_id ) {
6164 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6165
6166 if ( empty( $is_anonymous ) || false === $is_anonymous[ 'is' ] ) {
6167 $this->_is_anonymous = false;
6168 break;
6169 }
6170 }
6171
6172 if ( false !== $this->_is_anonymous ) {
6173 $this->_is_anonymous = true;
6174 }
6175 } else {
6176 if ( ! isset( $this->_storage->is_anonymous ) ) {
6177 // Not skipped.
6178 $this->_is_anonymous = false;
6179 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
6180 // For back compatibility, since the variable was boolean before.
6181 $this->_is_anonymous = $this->_storage->is_anonymous;
6182
6183 // Upgrade stored data format to 1.1.3 format.
6184 $this->set_anonymous_mode( $this->_storage->is_anonymous );
6185 } else {
6186 // Version 1.1.3 and later.
6187 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
6188 }
6189 }
6190 }
6191
6192 return $this->_is_anonymous;
6193 }
6194
6195 /**
6196 * Check if the user skipped the connection of a specified site.
6197 *
6198 * @author Vova Feldman (@svovaf)
6199 * @since 2.0.0
6200 *
6201 * @param int $blog_id
6202 *
6203 * @return bool
6204 */
6205 function is_anonymous_site( $blog_id = 0 ) {
6206 if ( $this->is_network_anonymous() ) {
6207 return true;
6208 }
6209
6210 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6211
6212 if ( empty( $is_anonymous ) ) {
6213 return false;
6214 }
6215
6216 return $is_anonymous['is'];
6217 }
6218
6219 /**
6220 * Check if user connected his account and install pending email activation.
6221 *
6222 * @author Vova Feldman (@svovaf)
6223 * @since 1.0.7
6224 *
6225 * @return bool
6226 */
6227 function is_pending_activation() {
6228 return $this->_storage->get( 'is_pending_activation', false );
6229 }
6230
6231 /**
6232 * @author Leo Fajardo (@leorw)
6233 * @since 2.5.0
6234 */
6235 private function clear_pending_activation_mode() {
6236 // Remove the pending activation sticky notice (if it still exists).
6237 $this->_admin_notices->remove_sticky( 'activation_pending' );
6238
6239 // Clear the plugin's pending activation mode.
6240 unset( $this->_storage->is_pending_activation );
6241 }
6242
6243 /**
6244 * Check if plugin must be WordPress.org compliant.
6245 *
6246 * @since 1.0.7
6247 *
6248 * @return bool
6249 */
6250 function is_org_repo_compliant() {
6251 return $this->_is_org_compliant;
6252 }
6253
6254 #--------------------------------------------------------------------------------
6255 #region WP Cron Common
6256 #--------------------------------------------------------------------------------
6257
6258 /**
6259 * @author Vova Feldman (@svovaf)
6260 * @since 2.0.0
6261 *
6262 * @param string $name Cron name.
6263 *
6264 * @return object
6265 */
6266 private function get_cron_data( $name ) {
6267 $this->_logger->entrance( $name );
6268
6269 /**
6270 * @var object $cron_data
6271 */
6272 return $this->_storage->get( "{$name}_cron", null );
6273 }
6274
6275 /**
6276 * @author Vova Feldman (@svovaf)
6277 * @since 2.0.0
6278 *
6279 * @param string $name Cron name.
6280 */
6281 private function clear_cron_data( $name ) {
6282 $this->_logger->entrance( $name );
6283
6284 $this->_storage->remove( "{$name}_cron" );
6285 }
6286
6287 /**
6288 * @author Vova Feldman (@svovaf)
6289 * @since 2.0.0
6290 *
6291 * @param string $name Cron name.
6292 * @param int $cron_blog_id The cron executing blog ID.
6293 */
6294 private function set_cron_data( $name, $cron_blog_id = 0 ) {
6295 $this->_logger->entrance( $name );
6296
6297 $this->_storage->store( "{$name}_cron", (object) array(
6298 'version' => $this->get_plugin_version(),
6299 'blog_id' => $cron_blog_id,
6300 'sdk_version' => $this->version,
6301 'timestamp' => WP_FS__SCRIPT_START_TIME,
6302 'on' => true,
6303 ) );
6304 }
6305
6306 /**
6307 * Get the cron's executing blog ID.
6308 *
6309 * @author Vova Feldman (@svovaf)
6310 * @since 2.0.0
6311 *
6312 * @param string $name Cron name.
6313 *
6314 * @return int
6315 */
6316 private function get_cron_blog_id( $name ) {
6317 $this->_logger->entrance( $name );
6318
6319 if ( ! is_multisite() ) {
6320 // Not a multisite.
6321 return 0;
6322 }
6323
6324 $cron_data = $this->get_cron_data( $name );
6325
6326 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6327 $cron_data->blog_id :
6328 0;
6329 }
6330
6331 /**
6332 * @author Vova Feldman (@svovaf)
6333 * @since 2.0.0
6334 *
6335 * @param string $name Cron name.
6336 *
6337 * @return bool
6338 */
6339 private function is_cron_on( $name ) {
6340 $this->_logger->entrance( $name );
6341
6342 /**
6343 * @var object $cron_data
6344 */
6345 $cron_data = $this->get_cron_data( $name );
6346
6347 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6348 }
6349
6350 /**
6351 * Unix timestamp for previous cron execution or false if never executed.
6352 *
6353 * @author Vova Feldman (@svovaf)
6354 * @since 2.0.0
6355 *
6356 * @param string $name Cron name.
6357 *
6358 * @return int|false
6359 */
6360 private function cron_last_execution( $name ) {
6361 $this->_logger->entrance( $name );
6362
6363 return $this->_storage->get( "{$name}_timestamp" );
6364 }
6365
6366 /**
6367 * Set cron execution time to now.
6368 *
6369 * @author Vova Feldman (@svovaf)
6370 * @since 2.0.0
6371 *
6372 * @param string $name Cron name.
6373 */
6374 private function set_cron_execution_timestamp( $name ) {
6375 $this->_logger->entrance( $name );
6376
6377 $this->_storage->store( "{$name}_timestamp", time() );
6378 }
6379
6380 /**
6381 * Sets the keepalive time to now.
6382 *
6383 * @author Leo Fajardo (@leorw)
6384 * @since 2.2.3
6385 *
6386 * @param bool|null $use_network_level_storage
6387 */
6388 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6389 $this->_logger->entrance();
6390
6391 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6392 }
6393
6394 /**
6395 * Check if cron was executed in the last $period of seconds.
6396 *
6397 * @author Vova Feldman (@svovaf)
6398 * @since 2.0.0
6399 *
6400 * @param string $name Cron name.
6401 * @param int $period In seconds
6402 *
6403 * @return bool
6404 */
6405 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6406 $this->_logger->entrance( $name );
6407
6408 $last_execution = $this->cron_last_execution( $name );
6409
6410 if ( ! is_numeric( $last_execution ) ) {
6411 return false;
6412 }
6413
6414 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6415 }
6416
6417 /**
6418 * WP Cron is executed on a site level. When running in a multisite network environment
6419 * with the network integration activated, for optimization reasons, we are consolidating
6420 * the installs data sync cron to be executed only from a single site.
6421 *
6422 * @author Vova Feldman (@svovaf)
6423 * @since 2.0.0
6424 *
6425 * @param int $except_blog_id Target any except the excluded blog ID.
6426 *
6427 * @return int
6428 */
6429 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6430 if ( ! is_multisite() ) {
6431 return 0;
6432 }
6433
6434 if ( $this->_is_network_active ) {
6435 $network_install_blog_id = $this->_storage->network_install_blog_id;
6436
6437 if (
6438 is_numeric( $network_install_blog_id ) &&
6439 $except_blog_id != $network_install_blog_id &&
6440 self::is_site_active( $network_install_blog_id )
6441 ) {
6442 // Try to run cron from the main network blog.
6443 $install = $this->get_install_by_blog_id( $network_install_blog_id );
6444
6445 if (
6446 is_object( $install ) &&
6447 $this->is_tracking_allowed( $network_install_blog_id, $install )
6448 ) {
6449 return $network_install_blog_id;
6450 }
6451 }
6452 }
6453
6454 // Get first opted-in blog ID with active tracking.
6455 $installs = $this->get_blog_install_map();
6456 foreach ( $installs as $blog_id => $install ) {
6457 if ( $except_blog_id != $blog_id &&
6458 self::is_site_active( $blog_id ) &&
6459 $this->is_tracking_allowed( $blog_id, $install )
6460 ) {
6461 return $blog_id;
6462 }
6463 }
6464
6465 return 0;
6466 }
6467
6468 /**
6469 * @author Vova Feldman (@svovaf)
6470 * @since 2.0.0
6471 *
6472 * @param string $name Cron name.
6473 * @param string $action_tag Callback action tag.
6474 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6475 */
6476 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6477 $this->_logger->entrance( $name );
6478
6479 if ( ! $this->is_cron_on( $name ) ) {
6480 return;
6481 }
6482
6483 $clear_cron = true;
6484 if ( ! $is_network_clear && $this->_is_network_active ) {
6485 $installs = $this->get_blog_install_map();
6486
6487 foreach ( $installs as $blog_id => $install ) {
6488 /**
6489 * @var FS_Site $install
6490 */
6491 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6492 $clear_cron = false;
6493 break;
6494 }
6495 }
6496 }
6497
6498 if ( ! $clear_cron ) {
6499 return;
6500 }
6501
6502 $cron_blog_id = $this->get_cron_blog_id( $name );
6503
6504 $this->clear_cron_data( $name );
6505
6506 if ( 0 < $cron_blog_id ) {
6507 switch_to_blog( $cron_blog_id );
6508 }
6509
6510 if ( empty( $action_tag ) ) {
6511 $action_tag = $name;
6512 }
6513
6514 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6515
6516 if ( 0 < $cron_blog_id ) {
6517 restore_current_blog();
6518 }
6519 }
6520
6521 /**
6522 * Unix timestamp for next cron execution or false if not scheduled.
6523 *
6524 * @author Vova Feldman (@svovaf)
6525 * @since 2.0.0
6526 *
6527 * @param string $name Cron name.
6528 * @param string $action_tag Callback action tag.
6529 *
6530 * @return int|false
6531 */
6532 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6533 $this->_logger->entrance( $name );
6534
6535 if ( ! $this->is_cron_on( $name ) ) {
6536 return false;
6537 }
6538
6539 $cron_blog_id = $this->get_cron_blog_id( $name );
6540
6541 if ( 0 < $cron_blog_id ) {
6542 switch_to_blog( $cron_blog_id );
6543 }
6544
6545 if ( empty( $action_tag ) ) {
6546 $action_tag = $name;
6547 }
6548
6549 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6550
6551 if ( 0 < $cron_blog_id ) {
6552 restore_current_blog();
6553 }
6554
6555 return $next_scheduled;
6556 }
6557
6558 /**
6559 * @author Vova Feldman (@svovaf)
6560 * @since 2.0.0
6561 *
6562 * @param string $name Cron name.
6563 * @param string $action_tag Callback action tag.
6564 * @param string $recurrence 'single' or 'daily'.
6565 * @param int $start_at Defaults to now.
6566 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6567 * @param int $except_blog_id Target any except the excluded blog ID.
6568 */
6569 private function schedule_cron(
6570 $name,
6571 $action_tag = '',
6572 $recurrence = 'single',
6573 $start_at = WP_FS__SCRIPT_START_TIME,
6574 $randomize_start = true,
6575 $except_blog_id = 0
6576 ) {
6577 $this->_logger->entrance( $name );
6578
6579 $this->clear_cron( $name, $action_tag, true );
6580
6581 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6582
6583 if ( is_multisite() && 0 == $cron_blog_id ) {
6584 // Don't schedule cron since couldn't find a target blog.
6585 return;
6586 }
6587
6588 if ( 0 < $cron_blog_id ) {
6589 switch_to_blog( $cron_blog_id );
6590 }
6591
6592 if ( 'daily' === $recurrence ) {
6593 if ( $randomize_start ) {
6594 // Schedule first sync with a random 12 hour time range from now.
6595 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6596 }
6597
6598 // Schedule daily WP cron.
6599 wp_schedule_event(
6600 $start_at,
6601 'daily',
6602 $this->get_action_tag( $action_tag )
6603 );
6604 } else if ( 'single' === $recurrence ) {
6605 // Schedule single cron.
6606 wp_schedule_single_event(
6607 $start_at,
6608 $this->get_action_tag( $action_tag )
6609 );
6610 }
6611
6612 $this->set_cron_data( $name, $cron_blog_id );
6613
6614 if ( 0 < $cron_blog_id ) {
6615 restore_current_blog();
6616 }
6617 }
6618
6619 /**
6620 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6621 * that doesn't halt page loading.
6622 *
6623 * @author Vova Feldman (@svovaf)
6624 * @since 2.0.0
6625 *
6626 * @param string $name Cron name.
6627 * @param callable $callable The function that should be executed.
6628 */
6629 private function execute_cron( $name, $callable ) {
6630 $this->_logger->entrance( $name );
6631
6632 // Store the last time data sync was executed.
6633 $this->set_cron_execution_timestamp( $name );
6634
6635 // Check if API is temporary down.
6636 if ( FS_Api::is_temporary_down() ) {
6637 return;
6638 }
6639
6640 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6641
6642 $users_2_blog_ids = array();
6643
6644 if ( ! is_multisite() ) {
6645 // Add dummy blog.
6646 $users_2_blog_ids[0] = array( 0 );
6647 } else {
6648 $installs = $this->get_blog_install_map();
6649 foreach ( $installs as $blog_id => $install ) {
6650 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6651 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6652 $users_2_blog_ids[ $install->user_id ] = array();
6653 }
6654
6655 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6656 }
6657 }
6658 }
6659
6660 $current_blog_id = get_current_blog_id();
6661
6662 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6663 if ( 0 < $blog_ids[0] ) {
6664 $this->switch_to_blog( $blog_ids[0] );
6665 }
6666
6667 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6668
6669 foreach ( $blog_ids as $blog_id ) {
6670 $this->do_action( "after_{$name}_cron", $blog_id );
6671 }
6672 }
6673
6674 if ( is_multisite() ) {
6675 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6676
6677 $this->do_action( "after_{$name}_cron_multisite" );
6678 }
6679 }
6680
6681 #endregion
6682
6683 #----------------------------------------------------------------------------------
6684 #region Daily Sync Cron
6685 #----------------------------------------------------------------------------------
6686
6687
6688 /**
6689 * @author Vova Feldman (@svovaf)
6690 * @since 2.0.0
6691 *
6692 * @return bool
6693 */
6694 private function is_sync_cron_scheduled() {
6695 return $this->is_cron_on( 'sync' );
6696 }
6697
6698 /**
6699 * Get the sync cron's executing blog ID.
6700 *
6701 * @author Vova Feldman (@svovaf)
6702 * @since 2.0.0
6703 *
6704 * @return int
6705 */
6706 private function get_sync_cron_blog_id() {
6707 return $this->get_cron_blog_id( 'sync' );
6708 }
6709
6710 /**
6711 * @author Vova Feldman (@svovaf)
6712 * @since 1.1.7.3
6713 */
6714 private function run_manual_sync() {
6715 if ( ! $this->is_user_admin() ) {
6716 return;
6717 }
6718
6719 // Run manual sync.
6720 $this->_sync_cron();
6721
6722 // Reschedule next cron to run 24 hours from now (performance optimization).
6723 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6724 }
6725
6726 /**
6727 * Data sync cron job. Replaces the background sync non blocking HTTP request
6728 * that doesn't halt page loading.
6729 *
6730 * @author Vova Feldman (@svovaf)
6731 * @since 1.1.7.3
6732 * @since 2.0.0 Consolidate all the data sync into the same cron for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6733 */
6734 function _sync_cron() {
6735 $this->_logger->entrance();
6736
6737 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6738 }
6739
6740 /**
6741 * The actual data sync cron logic.
6742 *
6743 * @author Vova Feldman (@svovaf)
6744 * @since 2.0.0
6745 *
6746 * @param int[] $blog_ids
6747 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6748 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6749 * updates for a single site in case `execute_cron` has switched to a different blog.
6750 */
6751 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6752 if ( $this->is_registered() ) {
6753 if ( $this->has_paid_plan() ) {
6754 // Initiate background plan sync.
6755 $this->_sync_license( true, false, $current_blog_id );
6756
6757 if ( $this->is_paying() ) {
6758 // Check for premium plugin updates.
6759 $this->check_updates( true );
6760 }
6761 } else {
6762 // Sync install(s) (only if something changed locally).
6763 if ( 1 < count( $blog_ids ) ) {
6764 $this->sync_installs();
6765 } else {
6766 $this->sync_install();
6767 }
6768
6769 $this->maybe_sync_install_user();
6770 }
6771 }
6772 }
6773
6774 /**
6775 * Check if sync was executed in the last $period of seconds.
6776 *
6777 * @author Vova Feldman (@svovaf)
6778 * @since 1.1.7.3
6779 *
6780 * @param int $period In seconds
6781 *
6782 * @return bool
6783 */
6784 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6785 return $this->is_cron_executed( 'sync', $period );
6786 }
6787
6788 /**
6789 * @author Vova Feldman (@svovaf)
6790 * @since 1.1.7.3
6791 *
6792 * @return bool
6793 */
6794 private function is_sync_cron_on() {
6795 return $this->is_cron_on( 'sync' );
6796 }
6797
6798 /**
6799 * @author Leo Fajardo (@leorw)
6800 * @since 2.5.0
6801 */
6802 private function maybe_schedule_sync_cron() {
6803 $next_schedule = $this->next_sync_cron();
6804
6805 // The event is properly scheduled, so no need to reschedule it.
6806 if (
6807 is_numeric( $next_schedule ) &&
6808 $next_schedule > time()
6809 ) {
6810 return;
6811 }
6812
6813 $this->schedule_sync_cron();
6814 }
6815
6816 /**
6817 * @author Vova Feldman (@svovaf)
6818 * @since 1.1.7.3
6819 *
6820 * @param int $start_at Defaults to now.
6821 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6822 * @param int $except_blog_id Since 2.0.0 when running in a multisite network environment, the cron execution is consolidated. This param allows excluding excluded specified blog ID from being the cron executor.
6823 */
6824 private function schedule_sync_cron(
6825 $start_at = WP_FS__SCRIPT_START_TIME,
6826 $randomize_start = true,
6827 $except_blog_id = 0
6828 ) {
6829 $this->schedule_cron(
6830 'sync',
6831 'data_sync',
6832 'daily',
6833 $start_at,
6834 $randomize_start,
6835 $except_blog_id
6836 );
6837 }
6838
6839 /**
6840 * Add the actual sync function to the cron job hook.
6841 *
6842 * @author Vova Feldman (@svovaf)
6843 * @since 1.1.7.3
6844 */
6845 private function hook_callback_to_sync_cron() {
6846 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6847 }
6848
6849 /**
6850 * @author Vova Feldman (@svovaf)
6851 * @since 1.1.7.3
6852 *
6853 * @param bool $is_network_clear Since 2.0.0 If set to TRUE, clear sync cron even if there are installs that are still connected.
6854 */
6855 private function clear_sync_cron( $is_network_clear = false ) {
6856 $this->_logger->entrance();
6857
6858 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6859 }
6860
6861 /**
6862 * Unix timestamp for next sync cron execution or false if not scheduled.
6863 *
6864 * @author Vova Feldman (@svovaf)
6865 * @since 1.1.7.3
6866 *
6867 * @return int|false
6868 */
6869 function next_sync_cron() {
6870 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6871 }
6872
6873 /**
6874 * Unix timestamp for previous sync cron execution or false if never executed.
6875 *
6876 * @author Vova Feldman (@svovaf)
6877 * @since 1.1.7.3
6878 *
6879 * @return int|false
6880 */
6881 function last_sync_cron() {
6882 return $this->cron_last_execution( 'sync' );
6883 }
6884
6885 #endregion Daily Sync Cron ------------------------------------------------------------------
6886
6887 #----------------------------------------------------------------------------------
6888 #region Async Install Sync
6889 #----------------------------------------------------------------------------------
6890
6891 /**
6892 * @author Vova Feldman (@svovaf)
6893 * @since 1.1.7.3
6894 *
6895 * @return bool
6896 */
6897 private function is_install_sync_scheduled() {
6898 return $this->is_cron_on( 'install_sync' );
6899 }
6900
6901 /**
6902 * Get the sync cron's executing blog ID.
6903 *
6904 * @author Vova Feldman (@svovaf)
6905 * @since 2.0.0
6906 *
6907 * @return int
6908 */
6909 private function get_install_sync_cron_blog_id() {
6910 return $this->get_cron_blog_id( 'install_sync' );
6911 }
6912
6913 /**
6914 * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
6915 *
6916 * @author Vova Feldman (@svovaf)
6917 * @since 1.1.7.3
6918 *
6919 * @param int $except_blog_id Since 2.0.0 when running in a multisite network environment, the cron execution is consolidated. This param allows excluding excluded specified blog ID from being the cron executor.
6920 */
6921 private function schedule_install_sync( $except_blog_id = 0 ) {
6922 if ( $this->is_clone() ) {
6923 return;
6924 }
6925
6926 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
6927 }
6928
6929 /**
6930 * Unix timestamp for previous install sync cron execution or false if never executed.
6931 *
6932 * @todo There's some very strange bug that $this->_storage->install_sync_timestamp value is not being updated. But for sure the sync event is working.
6933 *
6934 * @author Vova Feldman (@svovaf)
6935 * @since 1.1.7.3
6936 *
6937 * @return int|false
6938 */
6939 function last_install_sync() {
6940 return $this->cron_last_execution( 'install_sync' );
6941 }
6942
6943 /**
6944 * Unix timestamp for next install sync cron execution or false if not scheduled.
6945 *
6946 * @author Vova Feldman (@svovaf)
6947 * @since 1.1.7.3
6948 *
6949 * @return int|false
6950 */
6951 function next_install_sync() {
6952 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
6953 }
6954
6955 /**
6956 * Add the actual install sync function to the cron job hook.
6957 *
6958 * @author Vova Feldman (@svovaf)
6959 * @since 1.1.7.3
6960 */
6961 private function hook_callback_to_install_sync() {
6962 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
6963 }
6964
6965 /**
6966 * @author Vova Feldman (@svovaf)
6967 * @since 1.1.7.3
6968 *
6969 * @param bool $is_network_clear Since 2.0.0 If set to TRUE, clear sync cron even if there are installs that are still connected.
6970 */
6971 private function clear_install_sync_cron( $is_network_clear = false ) {
6972 $this->_logger->entrance();
6973
6974 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
6975 }
6976
6977 /**
6978 * @author Vova Feldman (@svovaf)
6979 * @since 1.1.7.3
6980 * @since 2.0.0 Consolidate all the data sync into the same cron for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6981 */
6982 public function _run_sync_install() {
6983 $this->_logger->entrance();
6984
6985 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
6986 }
6987
6988 /**
6989 * The actual install(s) sync cron logic.
6990 *
6991 * @author Vova Feldman (@svovaf)
6992 * @since 2.0.0
6993 *
6994 * @param int[] $blog_ids
6995 * @param int|null $current_blog_id
6996 */
6997 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
6998 if ( $this->is_registered() ) {
6999 if ( 1 < count( $blog_ids ) ) {
7000 $this->sync_installs( array(), true );
7001 } else {
7002 $this->sync_install( array(), true );
7003 }
7004
7005 $this->maybe_sync_install_user();
7006 }
7007 }
7008
7009 #endregion Async Install Sync ------------------------------------------------------------------
7010
7011 /**
7012 * Show a notice that activation is currently pending.
7013 *
7014 * @todo Add some sort of mechanism to allow users to update the email address they would like to opt-in with when $is_suspicious_email is true.
7015 *
7016 * @author Vova Feldman (@svovaf)
7017 * @since 1.0.7
7018 *
7019 * @param bool|string $email_address
7020 * @param bool $is_pending_trial Since 1.2.1.5
7021 * @param bool $is_suspicious_email Since 2.5.0 Set to true when there's an indication that email address the user opted in with is fake/dummy/placeholder.
7022 * @param bool $has_upgrade_context Since 2.5.3
7023 * @param bool $support_email_address Since 2.5.3
7024 */
7025 function _add_pending_activation_notice(
7026 $email_address = false,
7027 $is_pending_trial = false,
7028 $is_suspicious_email = false,
7029 $has_upgrade_context = false,
7030 $support_email_address = false
7031 ) {
7032 if ( ! is_string( $email_address ) ) {
7033 $current_user = self::_get_current_wp_user();
7034 $email_address = $current_user->user_email;
7035 }
7036
7037 $formatted_message_args = array(
7038 "<b>{$this->get_plugin_name()}</b>",
7039 "<b>{$email_address}</b>",
7040 );
7041
7042 if ( ! $has_upgrade_context || ! fs_is_network_admin() ) {
7043 /* translators: %3$s: action (e.g.: "start the trial" or "complete the opt-in") */
7044 $formatted_message = $this->get_text_inline( 'You should receive a confirmation email for %1$s to your mailbox at %2$s. Please make sure you click the button in that email to %3$s.', 'pending-activation-message' );
7045
7046 $formatted_message_args[] = $is_pending_trial ?
7047 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
7048 $this->get_text_inline( 'complete the opt-in', 'complete-the-opt-in' );
7049
7050 $notice_title = $this->get_text_inline( 'Thanks!', 'thanks' );
7051 } else {
7052 /* translators: %3$s: What the user is expected to receive via email (e.g.: "the installation instructions" or "a license key") */
7053 $formatted_message = $this->get_text_inline( 'You should receive %3$s for %1$s to your mailbox at %2$s in the next 5 minutes.' );
7054
7055 if ( $this->has_release_on_freemius() ) {
7056 $formatted_message_args[] = $this->get_text_x_inline(
7057 'the installation instructions',
7058 'Part of the message telling the user what they should receive via email.',
7059 'the-installation-instructions-phrase'
7060 );
7061 } else {
7062 $formatted_message_args[] = $this->get_text_x_inline(
7063 'a license key',
7064 'Part of the message telling the user what they should receive via email.',
7065 'a-license-key-phrase'
7066 );
7067
7068 $formatted_message .= ( ' ' . sprintf(
7069 /* translators: %s: activation link (e.g.: <a>Click here</a>) */
7070 $this->get_text_inline( '%s to activate the license once you get it.', 'license-activation-link-message' ),
7071 sprintf(
7072 '<b><a href="%s">%s</a></b>',
7073 $this->get_activation_url( array(
7074 'fs_action' => 'reset_pending_activation_mode',
7075 'require_license' => 'true',
7076 'fs_unique_affix' => $this->get_unique_affix(),
7077 ) ),
7078 $this->get_text_x_inline( 'Click here', 'Part of an activation link message.', 'click-here' )
7079 )
7080 ) );
7081 }
7082
7083 $formatted_message_args[] = ( ! empty( $support_email_address ) ) ?
7084 ( "<b>{$support_email_address}</b>" ) :
7085 $this->get_text_x_inline(
7086 "the product's support email address",
7087 'Part of the message that tells the user to check their spam folder for a specific email.',
7088 'product-support-email-address-phrase'
7089 );
7090
7091 $formatted_message .= ( ' ' . $this->get_text_inline( 'If you didn\'t get the email, try checking your spam folder or search for emails from %4$s.', 'check-spam-folder-message' ) );
7092
7093 $notice_title = $this->get_text_inline( 'Thanks for upgrading.', 'after-upgrade-thank-you-message' );
7094 }
7095
7096 $this->_admin_notices->add_sticky(
7097 vsprintf( $formatted_message, $formatted_message_args ),
7098 'activation_pending',
7099 $notice_title
7100 );
7101 }
7102
7103 /**
7104 * Check if currently in plugin activation.
7105 *
7106 * @author Vova Feldman (@svovaf)
7107 * @since 1.1.4
7108 *
7109 * @return bool
7110 */
7111 function is_plugin_activation() {
7112 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7113
7114 return !empty($result);
7115 }
7116
7117 /**
7118 *
7119 * NOTE: admin_menu action executed before admin_init.
7120 *
7121 * @author Vova Feldman (@svovaf)
7122 * @since 1.0.7
7123 */
7124 function _admin_init_action() {
7125 $is_migration = $this->is_migration();
7126
7127 /**
7128 * Automatically redirect to connect/activation page after plugin activation.
7129 *
7130 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
7131 */
7132 if ( $this->is_plugin_activation() ) {
7133 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7134
7135 if ( isset( $_GET['activate-multi'] ) ) {
7136 /**
7137 * Don't redirect if activating multiple plugins at once (bulk activation).
7138 */
7139 } else if (
7140 self::is_deactivation_snoozed() &&
7141 (
7142 // Either running the free code base.
7143 ! $this->is_premium() ||
7144 // Or if has a free version.
7145 ! $this->is_only_premium() ||
7146 // If premium only, don't redirect if license is activated.
7147 ( $this->is_registered() && ! $this->can_use_premium_code() )
7148 )
7149 ) {
7150 /**
7151 * Don't redirect if activating during the deactivation snooze period (aka troubleshooting), unless activating a paid product version that the admin didn't enter its license key yet.
7152 */
7153 } else if ( ! $is_migration ) {
7154 $this->_redirect_on_activation_hook();
7155 return;
7156 }
7157 }
7158
7159 if ( $is_migration ) {
7160 return;
7161 }
7162
7163 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
7164 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
7165
7166 $this->skip_connection( fs_is_network_admin() );
7167
7168 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
7169 }
7170
7171 if ( $this->is_network_activation_mode() &&
7172 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
7173 ) {
7174 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
7175
7176 $this->delegate_connection();
7177
7178 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
7179 }
7180
7181 $this->_add_upgrade_action_link();
7182
7183 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
7184 (
7185 ( true === $this->_storage->require_license_activation ) ||
7186 // Not registered nor anonymous.
7187 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
7188 // OR, network level and in network upgrade mode.
7189 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
7190 )
7191 ) {
7192 if ( ! $this->is_pending_activation() ) {
7193 if ( ! $this->is_activation_page() ) {
7194 /**
7195 * If a user visits any other admin page before activating the premium-only theme with a valid
7196 * license, reactivate the previous theme.
7197 *
7198 * @author Leo Fajardo (@leorw)
7199 * @since 1.2.2
7200 */
7201 if ( $this->is_theme() &&
7202 ! $this->has_settings_menu() &&
7203 ! isset( $_REQUEST['fs_action'] ) &&
7204 $this->can_activate_previous_theme()
7205 ) {
7206 if ( $this->is_only_premium() ) {
7207 $this->activate_previous_theme();
7208 return;
7209 }
7210
7211 if ( true === $this->_storage->require_license_activation ) {
7212 $this->_storage->require_license_activation = false;
7213 }
7214 }
7215
7216 if ( ! fs_is_network_admin() &&
7217 $this->is_network_activation_mode() &&
7218 ! $this->is_delegated_connection()
7219 ) {
7220 return;
7221 }
7222
7223 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
7224 if ( ! $this->_anonymous_mode &&
7225 ( ! $this->is_addon() || ! $this->_parent->is_anonymous() ) ) {
7226 // Show notice for new plugin installations.
7227 $this->_admin_notices->add(
7228 sprintf(
7229 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
7230 sprintf( '<b><a href="%s">%s</a></b>',
7231 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
7232 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
7233 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
7234 )
7235 ),
7236 '',
7237 'update-nag'
7238 );
7239 }
7240 } else {
7241 if ( $this->should_add_sticky_optin_notice() ) {
7242 $this->add_sticky_optin_admin_notice();
7243 }
7244
7245 if ( $this->has_filter( 'optin_pointer_element' ) ) {
7246 // Don't show admin nag if plugin update.
7247 wp_enqueue_script( 'wp-pointer' );
7248 wp_enqueue_style( 'wp-pointer' );
7249
7250 $this->_enqueue_connect_essentials();
7251
7252 add_action( 'admin_print_footer_scripts', array(
7253 $this,
7254 '_add_connect_pointer_script'
7255 ) );
7256 }
7257 }
7258 }
7259 }
7260
7261 if ( $this->show_opt_in_on_themes_page() &&
7262 $this->is_activation_page()
7263 ) {
7264 $this->_show_theme_activation_optin_dialog();
7265 }
7266 }
7267 }
7268
7269 /**
7270 * @author Vova Feldman (@svovaf)
7271 * @since 2.0.0
7272 *
7273 * @return bool
7274 */
7275 private function should_add_sticky_optin_notice() {
7276 if ( $this->is_addon() && $this->_parent->is_anonymous() ) {
7277 return false;
7278 }
7279
7280 if ( fs_is_network_admin() ) {
7281 if ( ! $this->_is_network_active ) {
7282 return false;
7283 }
7284
7285 if ( ! $this->is_network_activation_mode() ) {
7286 return false;
7287 }
7288
7289 return ! isset( $this->_storage->sticky_optin_added_ms );
7290 }
7291
7292 if ( ! $this->is_activation_mode() ) {
7293 return false;
7294 }
7295
7296 // If running from a blog admin and delegated the connection.
7297 return ! isset( $this->_storage->sticky_optin_added );
7298 }
7299
7300 /**
7301 * @author Leo Fajardo (@leorw)
7302 * @since 2.0.0
7303 */
7304 private function add_sticky_optin_admin_notice() {
7305 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7306 $this->_storage->sticky_optin_added = true;
7307 } else {
7308 $this->_storage->sticky_optin_added_ms = true;
7309 }
7310
7311 // Show notice for new plugin installations.
7312 $this->_admin_notices->add_sticky(
7313 sprintf(
7314 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7315 $this->_module_type,
7316 sprintf( '<b><a href="%s">%s</a></b>',
7317 $this->get_activation_url(),
7318 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7319 )
7320 ),
7321 'connect_account',
7322 '',
7323 'update-nag'
7324 );
7325 }
7326
7327 /**
7328 * Enqueue connect requires scripts and styles.
7329 *
7330 * @author Vova Feldman (@svovaf)
7331 * @since 1.1.4
7332 */
7333 function _enqueue_connect_essentials() {
7334 wp_enqueue_script( 'jquery' );
7335 wp_enqueue_script( 'json2' );
7336
7337 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.min.js' );
7338 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7339 }
7340
7341 /**
7342 * Add connect / opt-in pointer.
7343 *
7344 * @author Vova Feldman (@svovaf)
7345 * @since 1.1.4
7346 */
7347 function _add_connect_pointer_script() {
7348 $vars = array( 'id' => $this->_module_id );
7349 $pointer_content = fs_get_template( 'connect.php', $vars );
7350 ?>
7351 <script type="text/javascript">// <![CDATA[
7352 jQuery(document).ready(function ($) {
7353 if ('undefined' !== typeof(jQuery().pointer)) {
7354
7355 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7356
7357 if (element.length > 0) {
7358 var optin = $(element).pointer($.extend(true, {}, {
7359 content : <?php echo json_encode( $pointer_content ) ?>,
7360 position : {
7361 edge : 'left',
7362 align: 'center'
7363 },
7364 buttons : function () {
7365 // Don't show pointer buttons.
7366 return '';
7367 },
7368 pointerWidth: 482
7369 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7370
7371 <?php
7372 echo $this->apply_filters( 'optin_pointer_execute', "
7373
7374 optin.pointer('open');
7375
7376 // Tag the opt-in pointer with custom class.
7377 $('.wp-pointer #fs_connect')
7378 .parents('.wp-pointer.wp-pointer-top')
7379 .addClass('fs-opt-in-pointer');
7380
7381 ", 'element', 'optin' ) ?>
7382 }
7383 }
7384 });
7385 // ]]></script>
7386 <?php
7387 }
7388
7389 /**
7390 * Return current page's URL.
7391 *
7392 * @author Vova Feldman (@svovaf)
7393 * @since 1.0.7
7394 *
7395 * @return string
7396 */
7397 static function current_page_url() {
7398 $url = 'http';
7399
7400 if ( isset( $_SERVER["HTTPS"] ) ) {
7401 if ( $_SERVER["HTTPS"] == "on" ) {
7402 $url .= "s";
7403 }
7404 }
7405 $url .= "://";
7406 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7407 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7408 } else {
7409 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7410 }
7411
7412 return esc_url( $url );
7413 }
7414
7415 /**
7416 * Check if the current page is the plugin's main admin settings page.
7417 *
7418 * @author Vova Feldman (@svovaf)
7419 * @since 1.0.7
7420 *
7421 * @return bool
7422 */
7423 function _is_plugin_page() {
7424 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7425 fs_is_plugin_page( $this->_slug );
7426 }
7427
7428 /* Events
7429 ------------------------------------------------------------------------------------------------------------------*/
7430 /**
7431 * Delete site install from Database.
7432 *
7433 * @author Vova Feldman (@svovaf)
7434 * @since 1.0.1
7435 *
7436 * @param bool $store
7437 * @param int|null $blog_id Since 2.0.0
7438 *
7439 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7440 */
7441 function _delete_site( $store = true, $blog_id = null ) {
7442 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7443 }
7444
7445 /**
7446 * Delete site install from Database.
7447 *
7448 * @author Vova Feldman (@svovaf)
7449 * @since 1.2.2.7
7450 *
7451 * @param string $slug
7452 * @param string $module_type
7453 * @param bool $store
7454 * @param int|null $blog_id Since 2.0.0
7455 *
7456 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7457 */
7458 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7459 $sites = self::get_all_sites( $module_type, $blog_id );
7460
7461 $install_id = false;
7462
7463 if ( isset( $sites[ $slug ] ) ) {
7464 if ( is_object( $sites[ $slug ] ) ) {
7465 $install_id = $sites[ $slug ]->id;
7466 }
7467
7468 unset( $sites[ $slug ] );
7469
7470 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7471 }
7472
7473 return $install_id;
7474 }
7475
7476 /**
7477 * Delete user.
7478 *
7479 * @author Vova Feldman (@svovaf)
7480 * @since 2.0.0
7481 *
7482 * @param number $user_id
7483 * @param bool $store
7484 *
7485 * @return false|int The user ID if deleted. Otherwise, FALSE (when install not exist).
7486 */
7487 private static function delete_user( $user_id, $store = true ) {
7488 $users = self::get_all_users();
7489
7490 if ( ! is_array( $users ) || ! isset( $users[ $user_id ] ) ) {
7491 return false;
7492 }
7493
7494 unset( $users[ $user_id ] );
7495
7496 self::$_accounts->set_option( 'users', $users, $store );
7497
7498 return $user_id;
7499 }
7500
7501 /**
7502 * Delete plugin's plans information.
7503 *
7504 * @param bool $store Flush to Database if true.
7505 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7506 *
7507 * @author Vova Feldman (@svovaf)
7508 * @since 1.0.9
7509 */
7510 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7511 $this->_logger->entrance();
7512
7513 $plans = self::get_all_plans( $this->_module_type );
7514
7515 $plans_to_keep = array();
7516
7517 if ( $keep_associated_plans ) {
7518 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7519 foreach ( $plans_ids_to_keep as $plan_id ) {
7520 $plan = self::_get_plan_by_id( $plan_id );
7521 if ( is_object( $plan ) ) {
7522 $plans_to_keep[] = self::_encrypt_entity( $plan );
7523 }
7524 }
7525 }
7526
7527 if ( ! empty( $plans_to_keep ) ) {
7528 $plans[ $this->_slug ] = $plans_to_keep;
7529 } else {
7530 unset( $plans[ $this->_slug ] );
7531 }
7532
7533 $this->set_account_option( 'plans', $plans, $store );
7534 }
7535
7536 /**
7537 * Delete all plugin licenses.
7538 *
7539 * @author Vova Feldman (@svovaf)
7540 * @since 1.0.9
7541 *
7542 * @param bool $store
7543 */
7544 private function _delete_licenses( $store = true ) {
7545 $this->_logger->entrance();
7546
7547 $all_licenses = self::get_all_licenses();
7548
7549 unset( $all_licenses[ $this->_module_id ] );
7550
7551 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7552 }
7553
7554 /**
7555 * Check if Freemius was added on new plugin installation.
7556 *
7557 * @author Vova Feldman (@svovaf)
7558 * @since 1.1.5
7559 *
7560 * @return bool
7561 */
7562 function is_plugin_new_install() {
7563 return isset( $this->_storage->is_plugin_new_install ) &&
7564 $this->_storage->is_plugin_new_install;
7565 }
7566
7567 /**
7568 * Check if it's the first plugin release that is running Freemius.
7569 *
7570 * @author Vova Feldman (@svovaf)
7571 * @since 1.2.1.5
7572 *
7573 * @return bool
7574 */
7575 function is_first_freemius_powered_version() {
7576 return empty( $this->_storage->plugin_last_version );
7577 }
7578
7579 /**
7580 * @author Leo Fajardo (@leorw)
7581 * @since 1.2.2
7582 *
7583 * @return bool|string
7584 */
7585 private function get_previous_theme_slug() {
7586 return isset( $this->_storage->previous_theme ) ?
7587 $this->_storage->previous_theme :
7588 false;
7589 }
7590
7591 /**
7592 * @author Leo Fajardo (@leorw)
7593 * @since 1.2.2
7594 *
7595 * @return bool
7596 */
7597 private function can_activate_previous_theme() {
7598 return $this->can_activate_theme( $this->get_previous_theme_slug() );
7599 }
7600
7601 /**
7602 * @author Leo Fajardo (@leorw)
7603 * @since 2.5.0
7604 *
7605 * @return bool
7606 */
7607 private function can_activate_theme( $slug ) {
7608 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7609 $theme_instance = wp_get_theme( $slug );
7610
7611 return $theme_instance->exists();
7612 }
7613
7614 return false;
7615 }
7616
7617 /**
7618 * @author Leo Fajardo (@leorw)
7619 * @since 1.2.2
7620 */
7621 private function activate_previous_theme() {
7622 switch_theme( $this->get_previous_theme_slug() );
7623 unset( $this->_storage->previous_theme );
7624
7625 global $pagenow;
7626 if ( 'themes.php' === $pagenow ) {
7627 /**
7628 * Refresh the active theme information.
7629 *
7630 * @author Leo Fajardo (@leorw)
7631 * @since 1.2.2
7632 */
7633 fs_redirect( $this->admin_url( $pagenow ) );
7634 }
7635 }
7636
7637 /**
7638 * @author Leo Fajardo (@leorw)
7639 * @since 1.2.2
7640 *
7641 * @return string
7642 */
7643 function get_previous_theme_activation_url() {
7644 if ( ! $this->can_activate_previous_theme() ) {
7645 return '';
7646 }
7647
7648 /**
7649 * Activation URL
7650 *
7651 * @author Leo Fajardo (@leorw)
7652 * @since 1.2.2
7653 */
7654 return wp_nonce_url(
7655 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7656 'switch-theme_' . $this->get_previous_theme_slug()
7657 );
7658 }
7659
7660 /**
7661 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7662 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7663 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7664 * theme doesn't exist, then there will be no close button.
7665 *
7666 * @author Leo Fajardo (@leorw)
7667 * @since 1.2.2
7668 *
7669 * @param string $slug_or_name Old theme's slug or name.
7670 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7671 */
7672 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7673 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7674 $old_theme->get_stylesheet() :
7675 $slug_or_name;
7676
7677 $this->_activate_plugin_event_hook();
7678 }
7679
7680 /**
7681 * Plugin activated hook.
7682 *
7683 * @author Vova Feldman (@svovaf)
7684 * @since 1.0.1
7685 *
7686 * @uses FS_Api
7687 */
7688 function _activate_plugin_event_hook() {
7689 $this->_logger->entrance( 'slug = ' . $this->_slug );
7690
7691 if ( ! $this->is_user_admin() ) {
7692 return;
7693 }
7694
7695 $this->unregister_uninstall_hook();
7696
7697 // Clear API cache on activation.
7698 FS_Api::clear_cache();
7699
7700 $is_premium_version_activation = $this->is_plugin() ?
7701 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7702 $this->is_premium();
7703
7704 if ( $is_premium_version_activation && $this->is_pending_activation() ) {
7705 $this->clear_pending_activation_mode();
7706 }
7707
7708 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7709
7710 if ( $this->is_plugin() ) {
7711 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7712 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7713 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7714 $other_version_basename = $is_premium_version_activation ?
7715 $this->_free_plugin_basename :
7716 $this->premium_plugin_basename();
7717
7718 if ( ! $this->_is_network_active ) {
7719 /**
7720 * Themes are always network activated, but the ACTUAL activation is per site.
7721 *
7722 * During the activation, the plugin isn't yet active, therefore,
7723 * _is_network_active will be set to false even if it's a network level
7724 * activation. So we need to fix that by looking at the is_network_admin() value.
7725 *
7726 * @author Vova Feldman
7727 */
7728 $this->_is_network_active = (
7729 $this->_is_multisite_integrated &&
7730 fs_is_network_admin()
7731 );
7732 }
7733
7734 /**
7735 * If the other module version is active, deactivate it.
7736 *
7737 * is_plugin_active() checks if the plugin is active on the site or the network level and
7738 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7739 *
7740 * @author Leo Fajardo (@leorw)
7741 * @since 1.2.2
7742 */
7743 if (
7744 is_plugin_active( $other_version_basename ) &&
7745 $this->apply_filters( 'deactivate_on_activation', true )
7746 ) {
7747 deactivate_plugins( $other_version_basename );
7748 }
7749 }
7750
7751 if ( $this->is_registered() ) {
7752 if ( $is_premium_version_activation ) {
7753 $this->reconnect_locally();
7754 }
7755
7756
7757 // Schedule re-activation event and sync.
7758 // $this->sync_install( array(), true );
7759 $this->schedule_install_sync();
7760
7761 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7762 if ( $is_premium_version_activation ) {
7763 $this->_admin_notices->add(
7764 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7765 $this->get_text_x_inline( 'W00t',
7766 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7767 );
7768 }
7769 } else if ( $this->is_anonymous() ) {
7770 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7771 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7772 $network = true;
7773 } else {
7774 $plugin_version = isset( $this->_storage->is_anonymous ) ?
7775 $this->_storage->is_anonymous['version'] :
7776 null;
7777 $network = false;
7778 }
7779
7780 /**
7781 * Reset "skipped" click cache on the following:
7782 * 1. Freemius DEV mode.
7783 * 2. WordPress DEBUG mode.
7784 * 3. If a plugin and the user skipped the exact same version before.
7785 *
7786 * @since 1.2.2.7 Ulrich Pogson (@grapplerulrich) asked to not reset the SKIPPED flag if the exact same THEME version was activated before unless the developer is running with WP_DEBUG on, or Freemius debug mode on (WP_FS__DEV_MODE).
7787 *
7788 * @todo 4. If explicitly asked to retry after every activation.
7789 */
7790 if ( WP_FS__DEV_MODE ||
7791 (
7792 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7793 $this->get_plugin_version() == $plugin_version
7794 )
7795 ) {
7796 $this->reset_anonymous_mode( $network );
7797 }
7798 }
7799
7800 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7801
7802 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7803 /**
7804 * When activating an add-on, try to also activate a license.
7805 *
7806 * @author Leo Fajardo (@leorw)
7807 * @since 2.3.0
7808 */
7809 if ( ! $this->_is_network_active ) {
7810 $this->maybe_activate_addon_license();
7811 } else {
7812 $this->maybe_network_activate_addon_license();
7813 }
7814
7815 /**
7816 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7817 *
7818 * @author Leo Fajardo (@leorw)
7819 * @since 2.3.0
7820 */
7821 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7822
7823 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7824 $this->_storage->require_license_activation = false;
7825 }
7826 }
7827
7828 if (
7829 $is_premium_version_activation &&
7830 (
7831 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7832 (
7833 $this->is_registered() &&
7834 ! $is_trial_or_has_features_enabled_license
7835 )
7836 )
7837 ) {
7838 $this->_storage->require_license_activation = true;
7839 }
7840
7841 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7842 /**
7843 * If no previous version of plugin's version exist, it means that it's either
7844 * the first time that the plugin installed on the site, or the plugin was installed
7845 * before but didn't have Freemius integrated.
7846 *
7847 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7848 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7849 * only after immediate activation.
7850 *
7851 * @since 1.1.4
7852 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7853 */
7854 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7855 }
7856
7857 /**
7858 * Also flush when activating the premium version so that even if Freemius was off before, the API
7859 * connectivity test can be run again.
7860 *
7861 * @author Leo Fajardo (@leorw)
7862 * @since 2.2.3.1
7863 */
7864 $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation );
7865
7866 if ( ! $this->_anonymous_mode &&
7867 ( false !== $has_api_connectivity ) &&
7868 ! $this->_isAutoInstall
7869 ) {
7870 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7871 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7872 }
7873
7874 /**
7875 * Activation hook is executed after the plugin's main file is loaded, therefore,
7876 * after the plugin was loaded. The logic is located at activate_plugin()
7877 * ./wp-admin/includes/plugin.php.
7878 *
7879 * @author Vova Feldman (@svovaf)
7880 * @since 1.1.9
7881 */
7882 $this->_storage->was_plugin_loaded = true;
7883 }
7884
7885 /**
7886 * @author Leo Fajardo (@leorw)
7887 * @since 2.3.0
7888 */
7889 private function maybe_activate_addon_license() {
7890 $parent_fs = $this->get_parent_instance();
7891
7892 if (
7893 ! is_object( $parent_fs ) ||
7894 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7895 ) {
7896 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7897 return;
7898 }
7899
7900 $license = $this->get_active_parent_license();
7901 if ( ! is_object( $license ) ) {
7902 return;
7903 }
7904
7905 if (
7906 $this->is_bundle_license_auto_activation_enabled() &&
7907 ! empty( $license->products )
7908 ) {
7909 $this->activate_bundle_license( $license );
7910
7911 return;
7912 }
7913
7914 if ( ! $this->is_registered() ) {
7915 // Opt in with a license key.
7916 $this->opt_in(
7917 $parent_fs->get_current_or_network_user()->email,
7918 false,
7919 false,
7920 $license->secret_key
7921 );
7922 } else {
7923 // Activate the license.
7924 $install = $this->api_site_call(
7925 '/',
7926 'put',
7927 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7928 );
7929
7930 if ( ! FS_Api::is_api_error( $install ) ) {
7931 $this->_sync_addon_license( $this->get_id(), true );
7932 }
7933 }
7934 }
7935
7936 /**
7937 * @author Leo Fajardo (@leorw)
7938 * @since 2.3.0
7939 *
7940 * @param FS_Plugin_License $license
7941 */
7942 private function maybe_network_activate_addon_license( $license = null ) {
7943 $parent_fs = $this->get_parent_instance();
7944 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7945 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7946 return;
7947 }
7948
7949 $license = ( ! is_null( $license ) ) ?
7950 $license :
7951 $this->get_active_parent_license();
7952
7953 if ( ! is_object( $license ) ) {
7954 return;
7955 }
7956
7957 if (
7958 $this->is_bundle_license_auto_activation_enabled() &&
7959 ! empty( $license->products )
7960 ) {
7961 $this->activate_bundle_license( $license );
7962
7963 return;
7964 }
7965
7966 if ( ! $this->is_network_registered() ) {
7967 $sites = $this->get_sites_for_network_level_optin();
7968
7969 if ( count( $sites ) > $license->left() ) {
7970 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
7971 return;
7972 }
7973
7974 // Opt in with a license key.
7975 $this->opt_in(
7976 $parent_fs->get_user()->email,
7977 false,
7978 false,
7979 $license->secret_key,
7980 false,
7981 false,
7982 false,
7983 null,
7984 $sites
7985 );
7986 } else {
7987 $blog_2_install_map = array();
7988 $site_ids = array();
7989
7990 $all_sites = Freemius::get_sites();
7991
7992 foreach ( $all_sites as $site ) {
7993 $blog_id = Freemius::get_site_blog_id( $site );
7994 $install = $this->get_install_by_blog_id( $blog_id );
7995
7996 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
7997 // Skip license activation for installs that are already associated with a license.
7998 continue;
7999 }
8000
8001 if ( is_object( $install ) ) {
8002 $blog_2_install_map[ $blog_id ] = $install;
8003 } else {
8004 $site_ids[] = $blog_id;
8005 }
8006 }
8007
8008 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
8009 return;
8010 }
8011
8012 $user = $this->get_current_or_network_user();
8013
8014 if ( ! empty( $blog_2_install_map ) ) {
8015 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
8016
8017 if ( true !== $result ) {
8018 return;
8019 }
8020 }
8021
8022 if ( ! empty( $site_ids ) ) {
8023 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
8024 }
8025 }
8026 }
8027
8028 /**
8029 * Tries to activate a bundle license for all supported products if the current product is activated with a bundle license. This is called after activating an available license (not via the license activation dialog but by clicking on a license activation button) for a product via its "Account" page.
8030 *
8031 * @author Leo Fajardo (@leorw)
8032 * @since 2.4.0
8033 *
8034 * @param FS_Plugin_License $license
8035 * @param array $sites
8036 * @param int $blog_id
8037 */
8038 private function maybe_activate_bundle_license( FS_Plugin_License $license = null, $sites = array(), $blog_id = 0 ) {
8039 if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
8040 $license = $this->_license;
8041 }
8042
8043 if ( ! is_object( $license ) ) {
8044 return;
8045 }
8046
8047 $parent_license = ( ! empty( $license->products ) ) ?
8048 $license :
8049 $this->get_active_parent_license( $license->secret_key );
8050
8051 if ( is_object( $parent_license ) ) {
8052 $this->activate_bundle_license( $parent_license, $sites, $blog_id );
8053 }
8054 }
8055
8056 /**
8057 * Try to activate a bundle license for all the bundle products installed on the site.
8058 * (1) If a child product install already has a license, the bundle license won't be activated.
8059 * (2) On multi-site networks, if the attempt to activate the bundle license is triggered from the network admin, the bundle license activation will only work for non-delegated sites and only if none of them is associated with a license. Even if one of the sites has the product installed with a license key, skip the bundle license activation for the product.
8060 * (3) On multi-site networks, if the attempt to activate the bundle license is triggered from a site-level admin, only activate the license if the product is site-level activated or delegated, and the product installation is not yet associated with a license.
8061 *
8062 * @author Leo Fajardo (@leorw)
8063 * @since 2.4.0
8064 *
8065 * @param FS_Plugin_License $license
8066 * @param array $sites
8067 * @param int $current_blog_id
8068 */
8069 private function activate_bundle_license( $license, $sites = array(), $current_blog_id = 0 ) {
8070 $is_network_admin = fs_is_network_admin();
8071
8072 $installs_by_blog_map = array();
8073 $site_info_by_blog_map = array();
8074
8075 /**
8076 * Try to activate the license for all supported products.
8077 *
8078 * @author Leo Fajardo
8079 */
8080 foreach ( $license->products as $product_id ) {
8081 $fs = self::get_instance_by_id( $product_id );
8082
8083 if ( ! is_object( $fs ) ) {
8084 continue;
8085 }
8086
8087 if ( ! $fs->has_paid_plan() ) {
8088 continue;
8089 }
8090
8091 if (
8092 ! $fs->is_addon() &&
8093 ! FS_Plan_Manager::instance()->has_paid_plan( $fs->_plans )
8094 ) {
8095 /**
8096 * The parent product can be free-only but can have its `has_paid_plan` flag set to `true` when
8097 * there is a context bundle.
8098 */
8099 continue;
8100 }
8101
8102 if ( $current_blog_id > 0 ) {
8103 $fs->switch_to_blog( $current_blog_id );
8104 }
8105
8106 if ( $fs->has_active_valid_license() ) {
8107 continue;
8108 }
8109
8110 if ( ! $is_network_admin || $current_blog_id > 0 ) {
8111 if ( $fs->is_network_active() && ! $fs->is_delegated_connection( $current_blog_id ) ) {
8112 // Do not try to activate the license in the site level if the product is network active and the connection was not delegated.
8113 continue;
8114 }
8115 } else {
8116 if ( ! $fs->is_network_active() ) {
8117 // Do not try to activate the license in the network level if the product is not network active.
8118 continue;
8119 }
8120
8121 if ( $fs->is_network_delegated_connection() ) {
8122 // Do not try to activate the license in the network level if the activation has been delegated to site admins.
8123 continue;
8124 }
8125
8126 $has_install_with_license = false;
8127
8128 // Collection of sites that have an install entity that is not activated with a license or non-delegated sites that have no install entity, or both types of site.
8129 $filtered_sites = array();
8130
8131 if ( empty( $sites ) ) {
8132 $all_sites = self::get_sites();
8133
8134 foreach ( $all_sites as $site ) {
8135 $sites[] = array( 'blog_id' => self::get_site_blog_id( $site ) );
8136 }
8137 } else {
8138 // Populate the map here to avoid calling `$fs->get_site_info( $site );` in the other `for` loop below.
8139 foreach ( $sites as $site ) {
8140 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8141 continue;
8142 }
8143
8144 $site_info_by_blog_map[ $site['blog_id'] ] = $site;
8145 }
8146 }
8147
8148 foreach ( $sites as $site ) {
8149 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8150 continue;
8151 }
8152
8153 $blog_id = $site['blog_id'];
8154
8155 if ( ! isset( $installs_by_blog_map[ $blog_id ] ) ) {
8156 $installs_by_blog_map[ $blog_id ] = self::get_all_sites( $fs->get_module_type(), $blog_id );
8157 }
8158
8159 $installs = $installs_by_blog_map[ $blog_id ];
8160 $install = null;
8161
8162 if ( isset( $installs[ $fs->get_slug() ] ) ) {
8163 $install = $installs[ $fs->get_slug() ];
8164
8165 if (
8166 is_object( $install ) &&
8167 (
8168 ! FS_Site::is_valid_id( $install->id ) ||
8169 ! FS_User::is_valid_id( $install->user_id ) ||
8170 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
8171 )
8172 ) {
8173 $install = null;
8174 }
8175 }
8176
8177 if (
8178 is_object( $install ) &&
8179 FS_Plugin_License::is_valid_id( $install->license_id )
8180 ) {
8181 $has_install_with_license = true;
8182 break;
8183 }
8184
8185 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
8186 // Site activation delegated, don't activate bundle license on the site in the network admin.
8187 continue;
8188 }
8189
8190 if ( ! isset( $site_info_by_blog_map[ $blog_id ] ) ) {
8191 $site_info_by_blog_map[ $blog_id ] = $fs->get_site_info( $site );
8192 }
8193
8194 $filtered_sites[] = $site_info_by_blog_map[ $blog_id ];
8195 }
8196
8197 if ( $has_install_with_license || empty( $filtered_sites ) ) {
8198 // Do not try to activate the license at the network level if there's any install with a license or there's no site to activate the license on.
8199 continue;
8200 }
8201
8202 $sites = $filtered_sites;
8203 }
8204
8205 $fs->activate_migrated_license(
8206 $license->secret_key,
8207 null,
8208 null,
8209 $sites,
8210 ( $current_blog_id > 0 ? $current_blog_id : null )
8211 );
8212 }
8213 }
8214
8215 /**
8216 * Returns a parent license that can be activated for the context product.
8217 *
8218 * @author Leo Fajardo (@leorw)
8219 * @since 2.3.0
8220 *
8221 * @param string|null $license_key
8222 * @param bool $flush
8223 *
8224 * @return FS_Plugin_License
8225 */
8226 function get_active_parent_license( $license_key = null, $flush = true ) {
8227 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
8228
8229 $fs = $this;
8230
8231 if ( $this->is_addon() ) {
8232 $parent_instance = $this->get_parent_instance();
8233
8234 if ( is_object( $parent_instance ) && $parent_instance->is_registered() ) {
8235 $fs = $parent_instance;
8236 }
8237 }
8238
8239 $foreign_licenses = $fs->get_foreign_licenses_info(
8240 self::get_all_licenses( $this->get_parent_id() )
8241 );
8242
8243 if ( ! empty ( $foreign_licenses ) ) {
8244 $foreign_licenses = array(
8245 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
8246 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
8247 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
8248 );
8249
8250 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
8251 }
8252
8253 $result = $fs->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, $flush );
8254
8255 if (
8256 ! $this->is_api_result_object( $result, 'licenses' ) ||
8257 ! is_array( $result->licenses ) ||
8258 empty( $result->licenses )
8259 ) {
8260 return null;
8261 }
8262
8263 $parent_license = null;
8264
8265 if ( empty( $license_key ) ) {
8266 $parent_license = $result->licenses[0];
8267 } else {
8268 foreach ( $result->licenses as $license ) {
8269 if ( $license_key === $license->secret_key ) {
8270 $parent_license = $license;
8271 break;
8272 }
8273 }
8274 }
8275
8276 if ( ! is_null( $parent_license ) ) {
8277 $parent_license = new FS_Plugin_License( $parent_license );
8278 }
8279
8280 return $parent_license;
8281 }
8282
8283 /**
8284 * @author Leo Fajardo (@leorw)
8285 * @since 2.3.0
8286 *
8287 * @return array
8288 */
8289 function get_sites_for_network_level_optin() {
8290 $sites = array();
8291 $all_sites = self::get_sites();
8292
8293 foreach ( $all_sites as $site ) {
8294 $blog_id = self::get_site_blog_id( $site );
8295
8296 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
8297 ! $this->is_installed_on_site( $blog_id )
8298 ) {
8299 $sites[] = $this->get_site_info( $site );
8300 }
8301 }
8302
8303 return $sites;
8304 }
8305
8306 /**
8307 * Delete account.
8308 *
8309 * @author Vova Feldman (@svovaf)
8310 * @since 1.0.3
8311 *
8312 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8313 */
8314 function delete_account_event( $check_user = true ) {
8315 $this->_logger->entrance( 'slug = ' . $this->_slug );
8316
8317 if ( $check_user && ! $this->is_user_admin() ) {
8318 return;
8319 }
8320
8321 $this->do_action( 'before_account_delete' );
8322
8323 // Clear all admin notices.
8324 $this->_admin_notices->clear_all_sticky( false );
8325
8326 $this->_delete_site( false );
8327
8328 $delete_network_common_data = true;
8329
8330 if ( $this->_is_network_active ) {
8331 $installs = $this->get_blog_install_map();
8332
8333 // Don't delete common network data unless no other installs left.
8334 $delete_network_common_data = empty( $installs );
8335 }
8336
8337 if ( $delete_network_common_data ) {
8338 $this->_delete_plans( false );
8339
8340 $this->_delete_licenses( false );
8341
8342 // Delete add-ons related to plugin's account.
8343 $this->_delete_account_addons( false );
8344 }
8345
8346 // @todo Delete plans and licenses of add-ons.
8347
8348 self::$_accounts->store();
8349
8350 /**
8351 * IMPORTANT:
8352 * Clear crons must be executed before clearing all storage.
8353 * Otherwise, the cron will not be cleared.
8354 */
8355 if ( $delete_network_common_data ) {
8356 $this->clear_sync_cron();
8357 }
8358
8359 $this->clear_install_sync_cron();
8360
8361 // Clear all storage data.
8362 $this->_storage->clear_all( true, array(
8363 'is_delegated_connection',
8364 'connectivity_test',
8365 'is_on',
8366 ), false );
8367
8368 // Send delete event.
8369 $this->get_api_site_scope()->call( '/', 'delete' );
8370
8371 $this->do_action( 'after_account_delete' );
8372 }
8373
8374 /**
8375 * Delete network level account.
8376 *
8377 * @author Vova Feldman (@svovaf)
8378 * @since 2.0.0
8379 *
8380 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8381 */
8382 function delete_network_account_event( $check_user = true ) {
8383 $this->_logger->entrance( 'slug = ' . $this->_slug );
8384
8385 if ( $check_user && ! $this->is_user_admin() ) {
8386 return;
8387 }
8388
8389 $this->do_action( 'before_network_account_delete' );
8390
8391 // Clear all admin notices.
8392 $this->_admin_notices->clear_all_sticky();
8393
8394 $this->_delete_plans( false, false );
8395
8396 $this->_delete_licenses( false );
8397
8398 // Delete add-ons related to plugin's account.
8399 $this->_delete_account_addons( false );
8400
8401 // @todo Delete plans and licenses of add-ons.
8402
8403 self::$_accounts->store( true );
8404
8405 /**
8406 * IMPORTANT:
8407 * Clear crons must be executed before clearing all storage.
8408 * Otherwise, the cron will not be cleared.
8409 */
8410 $this->clear_sync_cron( true );
8411 $this->clear_install_sync_cron( true );
8412
8413 $sites = self::get_sites();
8414
8415 $install_ids = array();
8416 foreach ( $sites as $site ) {
8417 $blog_id = self::get_site_blog_id( $site );
8418
8419 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8420 continue;
8421 }
8422
8423 $install_id = $this->_delete_site( true, $blog_id );
8424
8425 // Clear all storage data.
8426 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8427
8428 if ( FS_Site::is_valid_id( $install_id ) ) {
8429 $install_ids[] = $install_id;
8430 }
8431
8432 switch_to_blog( $blog_id );
8433
8434 $this->do_action( 'after_account_delete' );
8435
8436 restore_current_blog();
8437 }
8438
8439 $this->_storage->clear_all( true, array(
8440 'connectivity_test',
8441 'is_on',
8442 ), true );
8443
8444 // Send delete event.
8445 if ( ! empty( $install_ids ) ) {
8446 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8447 }
8448
8449 $this->do_action( 'after_network_account_delete' );
8450 }
8451
8452 /**
8453 * Plugin deactivation hook.
8454 *
8455 * @author Vova Feldman (@svovaf)
8456 * @since 1.0.1
8457 */
8458 function _deactivate_plugin_hook() {
8459 $this->_logger->entrance( 'slug = ' . $this->_slug );
8460
8461 if ( ! $this->is_user_admin() ) {
8462 return;
8463 }
8464
8465 $is_network_deactivation = fs_is_network_admin();
8466 $storage_keys_for_removal = array();
8467
8468 $this->_admin_notices->clear_all_sticky();
8469
8470 $storage_keys_for_removal[] = 'sticky_optin_added';
8471 if ( isset( $this->_storage->sticky_optin_added ) ) {
8472 unset( $this->_storage->sticky_optin_added );
8473 }
8474
8475 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8476 // Remember that plugin was already installed.
8477 $this->_storage->is_plugin_new_install = false;
8478 }
8479
8480 // Hook to plugin uninstall.
8481 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8482
8483 $this->clear_module_main_file_cache();
8484 $this->clear_sync_cron( $this->_is_network_active );
8485 $this->clear_install_sync_cron();
8486
8487 if ( $this->is_registered() ) {
8488 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8489 FS_Plugin_Updater::instance( $this )->delete_update_data();
8490 }
8491
8492 if ( $is_network_deactivation ) {
8493 // Send deactivation event.
8494 $this->sync_installs( array(
8495 'is_active' => false,
8496 ) );
8497 } else {
8498 // Send deactivation event.
8499 $this->sync_install( array(
8500 'is_active' => false,
8501 ) );
8502 }
8503 } else {
8504 if ( false === $this->has_api_connectivity() && ! $this->is_premium() ) {
8505 // Reset connectivity test cache.
8506 $this->clear_connectivity_info();
8507
8508 $storage_keys_for_removal[] = 'connectivity_test';
8509 }
8510 }
8511
8512 if ( $is_network_deactivation ) {
8513 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8514 unset( $this->_storage->sticky_optin_added_ms );
8515 }
8516
8517 if ( ! empty( $storage_keys_for_removal ) ) {
8518 $sites = self::get_sites();
8519
8520 foreach ( $sites as $site ) {
8521 $blog_id = self::get_site_blog_id( $site );
8522
8523 foreach ( $storage_keys_for_removal as $key ) {
8524 $this->_storage->remove( $key, false, $blog_id );
8525 }
8526
8527 $this->_storage->save( $blog_id );
8528 }
8529 }
8530 }
8531
8532 // Clear API cache on deactivation.
8533 FS_Api::clear_cache();
8534
8535 $this->remove_sdk_reference();
8536 }
8537
8538 /**
8539 * @author Vova Feldman (@svovaf)
8540 * @since 1.1.6
8541 */
8542 private function remove_sdk_reference() {
8543 global $fs_active_plugins;
8544
8545 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8546 if ( $this->_plugin_basename == $data->plugin_path ) {
8547 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8548 break;
8549 }
8550 }
8551
8552 fs_fallback_to_newest_active_sdk();
8553 }
8554
8555 /**
8556 * @author Vova Feldman (@svovaf)
8557 * @since 1.1.3
8558 *
8559 * @param bool $is_anonymous
8560 * @param bool|int $network_or_blog_id Since 2.0.0
8561 */
8562 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8563 // Store information regarding skip to try and opt-in the user
8564 // again in the future.
8565 $skip_info = array(
8566 'is' => $is_anonymous,
8567 'timestamp' => WP_FS__SCRIPT_START_TIME,
8568 'version' => $this->get_plugin_version(),
8569 );
8570
8571 if ( true === $network_or_blog_id ) {
8572 $this->_storage->is_anonymous_ms = $skip_info;
8573 } else {
8574 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8575 }
8576
8577 $this->network_upgrade_mode_completed();
8578
8579 // Update anonymous mode cache.
8580 $this->_is_anonymous = $is_anonymous;
8581 }
8582
8583 /**
8584 * @author Vova Feldman (@svovaf)
8585 * @since 2.5.1
8586 *
8587 * @param bool|int $network_or_blog_id
8588 */
8589 private function unset_anonymous_mode( $network_or_blog_id = 0 ) {
8590 if ( true === $network_or_blog_id ) {
8591 unset( $this->_storage->is_anonymous_ms );
8592 } else {
8593 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8594 }
8595 }
8596
8597 /**
8598 * @author Vova Feldman (@svovaf)
8599 * @since 2.0.0
8600 *
8601 * @param int $blog_id Site ID.
8602 * @param int $user_id User ID.
8603 * @param string $domain Site domain.
8604 * @param string $path Site path.
8605 * @param int $network_id Network ID. Only relevant on multi-network installations.
8606 * @param array $meta Metadata. Used to set initial site options.
8607 *
8608 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8609 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8610 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8611 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8612 */
8613 public function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8614 $this->_logger->entrance();
8615
8616 if ( ! $this->_is_network_active ) {
8617 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
8618 return;
8619 }
8620
8621 $site = null;
8622 $new_blog_id = $blog_id;
8623
8624 if ( $this->is_premium() &&
8625 $this->is_network_connected() &&
8626 is_object( $this->_license ) &&
8627 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8628 $this->is_license_network_active( $blog_id )
8629 ) {
8630 /**
8631 * Running the premium version, the license was network activated, and the license can also be activated on the current site -> so try to opt-in with the license key.
8632 */
8633 $current_blog_id = get_current_blog_id();
8634 $license = clone $this->_license;
8635
8636 $this->switch_to_blog( $blog_id );
8637
8638 // Opt-in with network user.
8639 $this->install_with_user(
8640 $this->get_network_user(),
8641 $license->secret_key,
8642 false,
8643 false,
8644 false
8645 );
8646
8647 if ( is_object( $this->_site ) ) {
8648 if ( $this->_site->license_id == $license->id ) {
8649 /**
8650 * If the license was activated successfully, sync the license data from the remote server.
8651 */
8652 $this->_license = $license;
8653 $this->sync_site_license();
8654 }
8655 }
8656
8657 $site = $this->_site;
8658
8659 $this->switch_to_blog( $current_blog_id );
8660
8661 if ( is_object( $site ) ) {
8662 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id, $site );
8663
8664 // Already connected (with or without a license), so no need to continue.
8665 return;
8666 }
8667 }
8668
8669 if ( $this->is_network_anonymous() ) {
8670 /**
8671 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8672 */
8673 $this->skip_site_connection( $blog_id );
8674 } else if ( $this->is_network_delegated_connection() ) {
8675 /**
8676 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8677 */
8678 $this->delegate_site_connection( $blog_id );
8679 } else if ( $this->is_network_connected() ) {
8680 /**
8681 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8682 */
8683 $current_blog_id = get_current_blog_id();
8684
8685 $this->switch_to_blog( $blog_id );
8686
8687 // Opt-in with network user.
8688 $this->install_with_user(
8689 $this->get_network_user(),
8690 false,
8691 false,
8692 false,
8693 false
8694 );
8695
8696 $site = $this->_site;
8697
8698 $this->switch_to_blog( $current_blog_id );
8699 } else {
8700 /**
8701 * If the super-admin mixed different options (connect, skip, delegated):
8702 * a) If at least one site connection was delegated, then automatically delegate connection.
8703 * b) Otherwise, it means that at least one site was skipped and at least one site was connected. For a simplified UX in the initial release of the multisite network integration, skip the connection for the newly created site. If the super-admin will want to opt-in they can still do that from the network level Account page.
8704 */
8705 $has_delegated_site = false;
8706
8707 $sites = self::get_sites();
8708 foreach ( $sites as $wp_site ) {
8709 $blog_id = self::get_site_blog_id( $wp_site );
8710
8711 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8712 $has_delegated_site = true;
8713 break;
8714 }
8715 }
8716
8717 if ( $has_delegated_site ) {
8718 $this->delegate_site_connection( $blog_id );
8719 } else {
8720 $this->skip_site_connection( $blog_id );
8721 }
8722 }
8723
8724 /**
8725 * Store the new blog's information even if there's no install so that when a clone install is stored in the new blog's storage, we can try to resolve it automatically.
8726 *
8727 * @author Leo Fajardo (@leorw)
8728 * @since 2.5.0
8729 */
8730 FS_Clone_Manager::instance()->store_blog_install_info( $new_blog_id, $site );
8731 }
8732
8733 /**
8734 * @author Vova Feldman (@svovaf)
8735 * @since 2.5.0
8736 *
8737 * @param \WP_Site $new_site
8738 * @param array $args
8739 */
8740 public function _after_wp_initialize_site_callback( WP_Site $new_site, $args ) {
8741 $this->_logger->entrance();
8742
8743 $this->_after_new_blog_callback(
8744 $new_site->id,
8745 // Dummy user ID (not in use).
8746 0,
8747 $new_site->domain,
8748 $new_site->path,
8749 $new_site->network_id,
8750 // Dummy meta, not in use.
8751 array()
8752 );
8753 }
8754
8755 /**
8756 * @author Vova Feldman (@svovaf)
8757 * @since 1.1.3
8758 *
8759 * @param bool|int|int[] $network_or_blog_ids Since 2.0.0.
8760 */
8761 private function reset_anonymous_mode( $network_or_blog_ids = false ) {
8762 if ( true === $network_or_blog_ids ) {
8763 $this->unset_anonymous_mode( true );
8764
8765 if ( fs_is_network_admin() ) {
8766 $this->_is_anonymous = null;
8767 }
8768
8769 // Rest anonymous mode for all non-delegated sub-sites.
8770 $blog_ids = $this->get_non_delegated_blog_ids();
8771 }
8772 else
8773 {
8774 if ( false === $network_or_blog_ids ) {
8775 $network_or_blog_ids = 0;
8776 }
8777
8778 $blog_ids = is_array( $network_or_blog_ids ) ?
8779 $network_or_blog_ids :
8780 array( $network_or_blog_ids );
8781
8782 foreach ( $blog_ids as $blog_id ) {
8783 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8784 $this->_is_anonymous = null;
8785 }
8786 }
8787 }
8788
8789 foreach ( $blog_ids as $blog_id ) {
8790 $this->unset_anonymous_mode( $blog_id );
8791 }
8792
8793 /**
8794 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8795 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8796 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8797 *
8798 * @author Leo Fajardo (@leorw)
8799 * @since 1.2.2
8800 */
8801 if ( ! $this->_is_network_active ) {
8802 $this->_is_anonymous = null;
8803 }
8804 }
8805
8806 /**
8807 * @author Leo Fajardo (@leorw)
8808 * @since 2.5.3
8809 */
8810 private function update_license_required_permissions_if_anonymous() {
8811 if ( ! $this->is_anonymous() ) {
8812 return;
8813 }
8814
8815 $this->reset_anonymous_mode( fs_is_network_admin() );
8816
8817 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
8818 'essentials' => true,
8819 'events' => true,
8820 'diagnostic' => false,
8821 'extensions' => false,
8822 'site' => false,
8823 ) );
8824 }
8825
8826 /**
8827 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8828 * deleting the account in the network level, the URL of the page to redirect to is correct.
8829 *
8830 * @author Leo Fajardo (@leorw)
8831 *
8832 * @since 2.1.3
8833 */
8834 private function maybe_set_slug_and_network_menu_exists_flag() {
8835 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8836 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8837 $this->_menu->get_slug() :
8838 $this->_slug
8839 );
8840 }
8841 }
8842
8843 /**
8844 * Clears the anonymous mode and redirects to the opt-in screen.
8845 *
8846 * @author Vova Feldman (@svovaf)
8847 * @since 1.1.7
8848 */
8849 function connect_again() {
8850 if ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) {
8851 return;
8852 }
8853
8854 if ( $this->is_anonymous() ) {
8855 $this->reset_anonymous_mode( fs_is_network_admin() );
8856 }
8857
8858 $activation_url_params = array();
8859
8860 if ( $this->is_pending_activation() ) {
8861 $this->clear_pending_activation_mode();
8862
8863 if ( fs_request_get_bool( 'require_license' ) ) {
8864 $activation_url_params['require_license'] = true;
8865 }
8866 }
8867
8868 $this->maybe_set_slug_and_network_menu_exists_flag();
8869
8870 fs_redirect( $this->get_activation_url( $activation_url_params ) );
8871 }
8872
8873 /**
8874 * Skip account connect, and set anonymous mode.
8875 *
8876 * @author Vova Feldman (@svovaf)
8877 * @since 1.1.1
8878 *
8879 * @param bool|int|int[] $network_or_blog_ids Since 2.5.1
8880 */
8881 function skip_connection( $network_or_blog_ids = false ) {
8882 $this->_logger->entrance();
8883
8884 $this->_admin_notices->remove_sticky( 'connect_account' );
8885
8886 if ( true === $network_or_blog_ids ) {
8887 $this->set_anonymous_mode( true, true );
8888
8889 if ( fs_is_network_admin() ) {
8890 $this->_is_anonymous = null;
8891 }
8892
8893 // Rest anonymous mode for all non-delegated sub-sites.
8894 $blog_ids = $this->get_non_delegated_blog_ids();
8895 }
8896 else
8897 {
8898 if ( false === $network_or_blog_ids ) {
8899 $network_or_blog_ids = 0;
8900 }
8901
8902 $blog_ids = is_array( $network_or_blog_ids ) ?
8903 $network_or_blog_ids :
8904 array( $network_or_blog_ids );
8905
8906 foreach ( $blog_ids as $blog_id ) {
8907 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8908 $this->_is_anonymous = null;
8909 }
8910 }
8911 }
8912
8913 foreach ( $blog_ids as $blog_id ) {
8914 $this->skip_site_connection( $blog_id );
8915 }
8916
8917 $this->network_upgrade_mode_completed();
8918 }
8919
8920 /**
8921 * Skip connection for specific site in the network.
8922 *
8923 * @author Vova Feldman (@svovaf)
8924 * @since 2.0.0
8925 *
8926 * @param int|null $blog_id
8927 * @param bool $send_skip
8928 */
8929 private function skip_site_connection( $blog_id = null ) {
8930 $this->_logger->entrance();
8931
8932 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8933
8934 $this->set_anonymous_mode( true, $blog_id );
8935 }
8936
8937 /**
8938 * Plugin version update hook.
8939 *
8940 * @author Vova Feldman (@svovaf)
8941 * @since 1.0.4
8942 */
8943 private function update_plugin_version_event() {
8944 $this->_logger->entrance();
8945
8946 if ( ! $this->is_registered() ) {
8947 return;
8948 }
8949
8950 $this->schedule_install_sync();
8951 // $this->sync_install( array(), true );
8952 }
8953
8954 /**
8955 * Generate an MD5 signature of a plugins collection.
8956 * This helper methods used to identify changes in a plugins collection.
8957 *
8958 * @author Vova Feldman (@svovaf)
8959 * @since 2.0.0
8960 *
8961 * @param array [string]array $plugins
8962 *
8963 * @return string
8964 */
8965 private function get_plugins_thumbprint( $plugins ) {
8966 ksort( $plugins );
8967
8968 $thumbprint = '';
8969 foreach ( $plugins as $basename => $data ) {
8970 $thumbprint .= $data['slug'] . ',' .
8971 $data['Version'] . ',' .
8972 ( $data['is_active'] ? '1' : '0' ) . ';';
8973 }
8974
8975 return md5( $thumbprint );
8976 }
8977
8978 /**
8979 * Return a list of modified plugins since the last sync.
8980 *
8981 * Note:
8982 * There's no point to store a plugins counter since even if the number of
8983 * plugins didn't change, we still need to check if the versions are all the
8984 * same and the activity state is similar.
8985 *
8986 * @author Vova Feldman (@svovaf)
8987 * @since 1.1.8
8988 *
8989 * @return array|false
8990 */
8991 private function get_plugins_data_for_api() {
8992 // Alias.
8993 $site_active_plugins_option_name = 'active_plugins';
8994 $network_plugins_option_name = 'all_plugins';
8995
8996 /**
8997 * Collection of all site level active plugins.
8998 */
8999 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
9000
9001 if ( ! is_object( $site_active_plugins_cache ) ) {
9002 $site_active_plugins_cache = (object) array(
9003 'timestamp' => '',
9004 'md5' => '',
9005 'plugins' => array(),
9006 );
9007 }
9008
9009 $time = time();
9010
9011 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
9012 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
9013 ) {
9014 // Don't send plugin updates if last update was in the past 5 min.
9015 return false;
9016 }
9017
9018 // Write timestamp to lock the logic.
9019 $site_active_plugins_cache->timestamp = $time;
9020 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
9021
9022 // Reload options from DB.
9023 self::$_accounts->load( true );
9024 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
9025
9026 if ( $time != $site_active_plugins_cache->timestamp ) {
9027 // If timestamp is different, then another thread captured the lock.
9028 return false;
9029 }
9030
9031 /**
9032 * Collection of all plugins (network level).
9033 */
9034 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
9035
9036 if ( ! is_object( $network_plugins_cache ) ) {
9037 $network_plugins_cache = (object) array(
9038 'timestamp' => '',
9039 'md5' => '',
9040 'plugins' => array(),
9041 );
9042 }
9043
9044 // Check if there's a change in plugins.
9045 $network_plugins = self::get_network_plugins();
9046 $site_active_plugins = self::get_site_active_plugins();
9047
9048 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
9049 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
9050
9051 // Check if plugins status changed (version or active/inactive).
9052 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
9053 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
9054
9055 if ( ! $network_plugins_changed &&
9056 ! $site_active_plugins_changed
9057 ) {
9058 // No changes.
9059 return array();
9060 }
9061
9062 $plugins_update_data = array();
9063
9064 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
9065 if ( ! isset( $network_plugins[ $basename ] ) ) {
9066 // Plugin uninstalled.
9067 $uninstalled_plugin_data = $data;
9068 $uninstalled_plugin_data['is_active'] = false;
9069 $uninstalled_plugin_data['is_uninstalled'] = true;
9070 $plugins_update_data[] = $uninstalled_plugin_data;
9071
9072 unset( $network_plugins[ $basename ] );
9073
9074 unset( $network_plugins_cache->plugins[ $basename ] );
9075 unset( $site_active_plugins_cache->plugins[ $basename ] );
9076
9077 continue;
9078 }
9079
9080 $was_active = $data['is_active'] ||
9081 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9082 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
9083 $is_active = $network_plugins[ $basename ]['is_active'] ||
9084 ( isset( $site_active_plugins[ $basename ] ) &&
9085 $site_active_plugins[ $basename ]['is_active'] );
9086
9087 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9088 isset( $site_active_plugins[ $basename ] )
9089 ) {
9090 // Plugin was site level activated.
9091 $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
9092 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
9093 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9094 ! isset( $site_active_plugins[ $basename ] )
9095 ) {
9096 // Plugin was site level deactivated.
9097 unset( $site_active_plugins_cache->plugins[ $basename ] );
9098 }
9099
9100 $prev_version = $data['version'];
9101 $current_version = $network_plugins[ $basename ]['Version'];
9102
9103 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
9104 // Plugin activated or deactivated, or version changed.
9105
9106 if ( $was_active !== $is_active ) {
9107 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
9108 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
9109 }
9110 }
9111
9112 if ( $prev_version !== $current_version ) {
9113 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
9114 }
9115
9116 $updated_plugin_data = $data;
9117 $updated_plugin_data['is_active'] = $is_active;
9118 $updated_plugin_data['version'] = $current_version;
9119 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
9120 $plugins_update_data[] = $updated_plugin_data;
9121 }
9122 }
9123
9124 // Find new plugins that weren't yet seen before.
9125 foreach ( $network_plugins as $basename => $data ) {
9126 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
9127 // New plugin.
9128 $new_plugin = array(
9129 'slug' => $data['slug'],
9130 'version' => $data['Version'],
9131 'title' => $data['Name'],
9132 'is_active' => $data['is_active'],
9133 'is_uninstalled' => false,
9134 );
9135
9136 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
9137
9138 $is_site_level_active = (
9139 isset( $site_active_plugins[ $basename ] ) &&
9140 $site_active_plugins[ $basename ]['is_active']
9141 );
9142
9143 /**
9144 * If not network active, set the activity status based on the site-level plugin status.
9145 */
9146 if ( ! $new_plugin['is_active'] ) {
9147 $new_plugin['is_active'] = $is_site_level_active;
9148 }
9149
9150 $plugins_update_data[] = $new_plugin;
9151
9152 if ( isset( $site_active_plugins[ $basename ] ) ) {
9153 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
9154 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
9155 }
9156 }
9157 }
9158
9159 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
9160 $site_active_plugins_cache->timestamp = $time;
9161 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
9162
9163 $network_plugins_cache->md5 = $network_plugins_thumbprint;
9164 $network_plugins_cache->timestamp = $time;
9165 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
9166
9167 return $plugins_update_data;
9168 }
9169
9170 /**
9171 * Return a list of modified themes since the last sync.
9172 *
9173 * Note:
9174 * There's no point to store a themes counter since even if the number of
9175 * themes didn't change, we still need to check if the versions are all the
9176 * same and the activity state is similar.
9177 *
9178 * @author Vova Feldman (@svovaf)
9179 * @since 1.1.8
9180 *
9181 * @return array|false
9182 */
9183 private function get_themes_data_for_api() {
9184 // Alias.
9185 $option_name = 'all_themes';
9186
9187 $all_cached_themes = self::$_accounts->get_option( $option_name );
9188
9189 if ( ! is_object( $all_cached_themes ) ) {
9190 $all_cached_themes = (object) array(
9191 'timestamp' => '',
9192 'md5' => '',
9193 'themes' => array(),
9194 );
9195 }
9196
9197 $time = time();
9198
9199 if ( ! empty( $all_cached_themes->timestamp ) &&
9200 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
9201 ) {
9202 // Don't send theme updates if last update was in the past 5 min.
9203 return false;
9204 }
9205
9206 // Write timestamp to lock the logic.
9207 $all_cached_themes->timestamp = $time;
9208 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9209
9210 // Reload options from DB.
9211 self::$_accounts->load( true );
9212 $all_cached_themes = self::$_accounts->get_option( $option_name );
9213
9214 if ( $time != $all_cached_themes->timestamp ) {
9215 // If timestamp is different, then another thread captured the lock.
9216 return false;
9217 }
9218
9219 // Get active theme.
9220 $active_theme = wp_get_theme();
9221 $active_theme_stylesheet = $active_theme->get_stylesheet();
9222
9223 // Check if there's a change in themes.
9224 $all_themes = wp_get_themes();
9225
9226 // Check if themes changed.
9227 ksort( $all_themes );
9228
9229 $themes_signature = '';
9230 foreach ( $all_themes as $slug => $data ) {
9231 $is_active = ( $slug === $active_theme_stylesheet );
9232 $themes_signature .= $slug . ',' .
9233 $data->version . ',' .
9234 ( $is_active ? '1' : '0' ) . ';';
9235 }
9236
9237 // Check if themes status changed (version or active/inactive).
9238 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
9239
9240 $themes_update_data = array();
9241
9242 if ( $themes_changed ) {
9243 // Change in themes, report changes.
9244
9245 // Update existing themes info.
9246 foreach ( $all_cached_themes->themes as $slug => $data ) {
9247 $is_active = ( $slug === $active_theme_stylesheet );
9248
9249 if ( ! isset( $all_themes[ $slug ] ) ) {
9250 // Plugin uninstalled.
9251 $uninstalled_theme_data = $data;
9252 $uninstalled_theme_data['is_active'] = false;
9253 $uninstalled_theme_data['is_uninstalled'] = true;
9254 $themes_update_data[] = $uninstalled_theme_data;
9255
9256 unset( $all_themes[ $slug ] );
9257 unset( $all_cached_themes->themes[ $slug ] );
9258 } else if ( $data['is_active'] !== $is_active ||
9259 $data['version'] !== $all_themes[ $slug ]->version
9260 ) {
9261 // Plugin activated or deactivated, or version changed.
9262
9263 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
9264 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
9265
9266 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
9267 }
9268 }
9269
9270 // Find new themes that weren't yet seen before.
9271 foreach ( $all_themes as $slug => $data ) {
9272 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
9273 $is_active = ( $slug === $active_theme_stylesheet );
9274
9275 // New plugin.
9276 $new_plugin = array(
9277 'slug' => $slug,
9278 'version' => $data->version,
9279 'title' => $data->name,
9280 'is_active' => $is_active,
9281 'is_uninstalled' => false,
9282 );
9283
9284 $themes_update_data[] = $new_plugin;
9285 $all_cached_themes->themes[ $slug ] = $new_plugin;
9286 }
9287 }
9288
9289 $all_cached_themes->md5 = md5( $themes_signature );
9290 $all_cached_themes->timestamp = time();
9291 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9292 }
9293
9294 return $themes_update_data;
9295 }
9296
9297 /**
9298 * Get site data for API install request.
9299 *
9300 * @author Vova Feldman (@svovaf)
9301 * @since 1.1.2
9302 *
9303 * @param string[] $override
9304 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9305 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9306 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, title, and URL).
9307 *
9308 * @return array
9309 */
9310 private function get_install_data_for_api(
9311 array $override,
9312 $include_plugins = true,
9313 $include_themes = true,
9314 $include_blog_data = true
9315 ) {
9316 // Alias.
9317 $permissions = FS_Permission_Manager::instance( $this );
9318
9319 if ( $permissions->is_extensions_tracking_allowed() ) {
9320 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
9321 /**
9322 * @since 1.1.8 Also send plugin updates.
9323 */
9324 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9325 $plugins = $this->get_plugins_data_for_api();
9326 if ( ! empty( $plugins ) ) {
9327 $override['plugins'] = $plugins;
9328 }
9329 }
9330 }
9331
9332 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
9333 /**
9334 * @since 1.1.8 Also send themes updates.
9335 */
9336 if ( $include_themes && ! isset( $override['themes'] ) ) {
9337 $themes = $this->get_themes_data_for_api();
9338 if ( ! empty( $themes ) ) {
9339 $override['themes'] = $themes;
9340 }
9341 }
9342 }
9343 }
9344
9345 $versions = $this->get_versions();
9346
9347 $blog_data = array();
9348 if ( $include_blog_data ) {
9349 $blog_data['url'] = self::get_unfiltered_site_url();
9350
9351 if ( $permissions->is_diagnostic_tracking_allowed() ) {
9352 $blog_data = array_merge( $blog_data, array(
9353 'language' => self::get_sanitized_language(),
9354 'title' => get_bloginfo( 'name' ),
9355 ) );
9356 }
9357 }
9358
9359 return array_merge( $versions, $blog_data, array(
9360 'version' => $this->get_plugin_version(),
9361 'is_premium' => $this->is_premium(),
9362 // Special params.
9363 'is_active' => true,
9364 'is_uninstalled' => false,
9365 ), $override );
9366 }
9367
9368 /**
9369 * Update installs details.
9370 *
9371 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
9372 *
9373 * @author Vova Feldman (@svovaf)
9374 * @since 2.0.0
9375 *
9376 * @param string[] string $override
9377 * @param bool $only_diff
9378 * @param bool $is_keepalive
9379 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9380 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9381 *
9382 * @return array
9383 */
9384 private function get_installs_data_for_api(
9385 array $override,
9386 $only_diff = false,
9387 $is_keepalive = false,
9388 $include_plugins = true,
9389 $include_themes = true
9390 ) {
9391 /**
9392 * @since 1.1.8 Also send plugin updates.
9393 */
9394 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9395 // $plugins = $this->get_plugins_data_for_api();
9396 // if ( ! empty( $plugins ) ) {
9397 // $override['plugins'] = $plugins;
9398 // }
9399 // }
9400 /**
9401 * @since 1.1.8 Also send themes updates.
9402 */
9403 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9404 // $themes = $this->get_themes_data_for_api();
9405 // if ( ! empty( $themes ) ) {
9406 // $override['themes'] = $themes;
9407 // }
9408 // }
9409
9410 // Common properties.
9411 $versions = $this->get_versions();
9412 $common = array_merge( $versions, array(
9413 'version' => $this->get_plugin_version(),
9414 'is_premium' => $this->is_premium(),
9415 ), $override );
9416
9417
9418 $is_common_diff_for_any_site = false;
9419 $common_diff_union = array();
9420
9421 $installs_data = array();
9422
9423 $sites = self::get_sites();
9424
9425 $subsite_data_for_api_by_install_id = array();
9426 $install_url_by_install_id = array();
9427 $subsite_registration_date_by_install_id = array();
9428
9429 foreach ( $sites as $site ) {
9430 $blog_id = self::get_site_blog_id( $site );
9431
9432 $install = $this->get_install_by_blog_id( $blog_id );
9433
9434 if ( is_object( $install ) ) {
9435 if ( $install->user_id != $this->_user->id ) {
9436 // Install belongs to a different owner.
9437 continue;
9438 }
9439
9440 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
9441 // Don't send updates regarding opted-out installs.
9442 continue;
9443 }
9444
9445 $install_data = $this->get_site_info( $site, true );
9446
9447 if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $install_data['blog_id'] ) ) {
9448 continue;
9449 }
9450
9451 $uid = $install_data['uid'];
9452 $url = $install_data['url'];
9453 $registration_date = $install_data['registration_date'];
9454
9455 if ( isset( $subsite_data_for_api_by_install_id[ $install->id ] ) ) {
9456 $clone_subsite_data = $subsite_data_for_api_by_install_id[ $install->id ];
9457 $clone_install_url = $install_url_by_install_id[ $install->id ];
9458 $clone_subsite_registration_date = $subsite_registration_date_by_install_id[ $install->id ];
9459
9460 $skip = false;
9461
9462 if (
9463 ! empty( $install_data['registration_date'] ) &&
9464 ! empty( $clone_subsite_registration_date )
9465 ) {
9466 /**
9467 * If the current subsite was created after the other subsite that is also linked to the same install ID, we assume that it's a clone (not the original), and therefore, would skip its processing.
9468 *
9469 * @author Leo Fajardo (@leorw)
9470 * @since 2.5.1
9471 */
9472 $skip = ( strtotime( $install_data['registration_date'] ) > strtotime( $clone_subsite_registration_date ) );
9473 } else if (
9474 /**
9475 * If we already have an install with the same URL as the subsite it's stored in, skip the current subsite. Otherwise, replace the existing install's data with the current subsite's install's data if the URLs match.
9476 *
9477 * @author Leo Fajardo (@leorw)
9478 * @since 2.5.0
9479 */
9480 fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) ||
9481 fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $url ) )
9482 ) {
9483 $skip = true;
9484 }
9485
9486 if ( $skip ) {
9487 // Store the skipped subsite's ID so that the clone resolution manager can try to resolve the clone install that is stored in that subsite later on.
9488 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
9489 continue;
9490 }
9491 }
9492
9493 unset( $install_data['blog_id'] );
9494 unset( $install_data['uid'] );
9495 unset( $install_data['url'] );
9496 unset( $install_data['registration_date'] );
9497
9498 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9499 $install_data['is_uninstalled'] = $install->is_uninstalled;
9500
9501 $common_diff = null;
9502 $is_common_diff = false;
9503 if ( $only_diff ) {
9504 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9505 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9506
9507 $is_common_diff = ! empty( $common_diff );
9508
9509 if ( $is_common_diff ) {
9510 foreach ( $common_diff as $k => $v ) {
9511 if ( ! isset( $common_diff_union[ $k ] ) ) {
9512 $common_diff_union[ $k ] = $v;
9513 }
9514 }
9515 }
9516
9517 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9518 }
9519
9520 if ( ! empty( $install_data ) || $is_common_diff || $is_keepalive ) {
9521 // Add install ID and site unique ID.
9522 $install_data['id'] = $install->id;
9523 $install_data['uid'] = $uid;
9524 $install_data['url'] = $url;
9525
9526 $subsite_data_for_api_by_install_id[ $install->id ] = $install_data;
9527 $install_url_by_install_id[ $install->id ] = $install->url;
9528 $subsite_registration_date_by_install_id[ $install->id ] = $registration_date;
9529 }
9530 }
9531 }
9532
9533 restore_current_blog();
9534
9535 $installs_data = array_merge(
9536 $installs_data,
9537 array_values( $subsite_data_for_api_by_install_id )
9538 );
9539
9540 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9541 if ( ! $only_diff ) {
9542 $installs_data[] = $common;
9543 } else if ( ! empty( $common_diff_union ) ) {
9544 $installs_data[] = $common_diff_union;
9545 }
9546 }
9547
9548 foreach ( $installs_data as &$data ) {
9549 $data = (object) $data;
9550 }
9551
9552 return $installs_data;
9553 }
9554
9555 /**
9556 * Compare site actual data to the stored install data and return the differences for an API data sync.
9557 *
9558 * @author Vova Feldman (@svovaf)
9559 * @since 2.0.0
9560 *
9561 * @param array $site
9562 * @param FS_Site $install
9563 * @param string[] string $override
9564 *
9565 * @return array
9566 */
9567 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9568 $diff = array();
9569 $special = array();
9570 $special_override = false;
9571
9572 foreach ( $site as $p => $v ) {
9573 if ( property_exists( $install, $p ) ) {
9574 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9575 $install->{$p} != $v
9576 ) {
9577 $val = self::get_api_sanitized_property( $p, $v );
9578
9579 if ( $install->{$p} != $val ) {
9580 $install->{$p} = $val;
9581 $diff[ $p ] = $val;
9582 }
9583 }
9584 } else {
9585 $special[ $p ] = $v;
9586
9587 if ( isset( $override[ $p ] ) ||
9588 'plugins' === $p ||
9589 'themes' === $p
9590 ) {
9591 $special_override = true;
9592 }
9593 }
9594 }
9595
9596 if ( $special_override || 0 < count( $diff ) ) {
9597 // Add special params only if has at least one
9598 // standard param, or if explicitly requested to
9599 // override a special param or a param which is not exist
9600 // in the install object.
9601 $diff = array_merge( $diff, $special );
9602 }
9603
9604 return $diff;
9605 }
9606
9607 /**
9608 * @author Leo Fajardo (@leorw)
9609 * @since 2.5.1
9610 */
9611 private function send_pending_clone_update_once() {
9612 $this->_logger->entrance();
9613
9614 if ( ! empty( $this->_storage->clone_id ) ) {
9615 return;
9616 }
9617
9618 $install_clone = $this->get_api_site_scope()->call(
9619 '/clones',
9620 'post',
9621 array( 'site_url' => self::get_unfiltered_site_url() )
9622 );
9623
9624 if ( $this->is_api_result_entity( $install_clone ) ) {
9625 $this->_storage->clone_id = $install_clone->id;
9626 }
9627 }
9628
9629 /**
9630 * @author Leo Fajardo (@leorw)
9631 * @since 2.5.1
9632 *
9633 * @param string $resolution_type
9634 * @param FS_Site $clone_context_install
9635 */
9636 function send_clone_resolution_update( $resolution_type, $clone_context_install ) {
9637 $this->_logger->entrance();
9638
9639 if ( empty( $this->_storage->clone_id ) ) {
9640 return;
9641 }
9642
9643 $new_install_id = null;
9644 $current_site = null;
9645
9646 $flush = false;
9647
9648 /**
9649 * If the current site is now different from the context install before the clone resolution, we need to override `$this->_site` so that the API call below will be made with the right install scope entity.
9650 */
9651 if ( $clone_context_install->id != $this->_site->id ) {
9652 $new_install_id = $this->_site->id;
9653 $current_site = $this->_site;
9654 $this->_site = $clone_context_install;
9655
9656 $flush = true;
9657 }
9658
9659 $this->get_api_site_scope( $flush )->call(
9660 "/clones/{$this->_storage->clone_id}",
9661 'put',
9662 array(
9663 'resolution' => $resolution_type,
9664 'new_install_id' => $new_install_id,
9665 )
9666 );
9667
9668 if ( is_object( $current_site ) ) {
9669 /**
9670 * Ensure that the install scope entity is updated back to the previous install entity.
9671 */
9672 $this->_site = $current_site;
9673
9674 // Restore the previous install scope entity of the API.
9675 $this->get_api_site_scope( true );
9676 }
9677 }
9678
9679 /**
9680 * Update install only if changed.
9681 *
9682 * @author Vova Feldman (@svovaf)
9683 * @since 1.0.9
9684 *
9685 * @param string[] string $override
9686 * @param bool $flush
9687 * @param bool $is_two_way_sync @since 2.5.0 If true and there's a successful API request, the install sync cron will be cleared.
9688 *
9689 * @return false|object|string
9690 */
9691 private function send_install_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9692 $this->_logger->entrance();
9693
9694 $check_properties = $this->get_install_data_for_api( $override );
9695
9696 if ( $flush ) {
9697 $params = $check_properties;
9698 } else {
9699 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9700 }
9701
9702 if ( empty( $params ) ) {
9703 $keepalive_only_update = $this->should_send_keepalive_update();
9704
9705 if ( ! $keepalive_only_update ) {
9706 /**
9707 * There are no updates to send including keepalive.
9708 *
9709 * @author Leo Fajardo (@leorw)
9710 * @since 2.2.3
9711 */
9712 return false;
9713 }
9714 }
9715
9716 if ( $is_two_way_sync ) {
9717 /**
9718 * Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9719 *
9720 * @author Leo Fajardo (@leorw)
9721 * @since 2.2.3
9722 */
9723 if ( ! is_multisite() ) {
9724 // Update last install sync timestamp.
9725 $this->set_cron_execution_timestamp( 'install_sync' );
9726 }
9727
9728 $params['uid'] = $this->get_anonymous_id();
9729 }
9730
9731 $this->set_keepalive_timestamp();
9732
9733 // Send updated values to FS.
9734 $site = $this->api_site_call( '/', 'put', $params, true );
9735
9736 if ( $is_two_way_sync && $this->is_api_result_entity( $site ) ) {
9737 /**
9738 * Clear scheduled install sync after a two-way sync call.
9739 *
9740 * @author Leo Fajardo (@leorw)
9741 * @since 2.2.3
9742 */
9743 if ( ! is_multisite() ) {
9744 // I successfully sent install update, clear scheduled sync if exist.
9745 $this->clear_install_sync_cron();
9746 }
9747 }
9748
9749 return $site;
9750 }
9751
9752 /**
9753 * Update installs only if changed.
9754 *
9755 * @author Vova Feldman (@svovaf)
9756 * @since 2.0.0
9757 *
9758 * @param string[] string $override
9759 * @param bool $flush
9760 * @param bool $is_two_way_sync @since 2.5.0 If true and there's a successful API request, the install sync cron will be cleared.
9761 *
9762 * @return false|object|string
9763 */
9764 private function send_installs_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9765 $this->_logger->entrance();
9766
9767 /**
9768 * Pass `true` to use the network level storage since the update is for many installs.
9769 *
9770 * @author Leo Fajardo (@leorw)
9771 * @since 2.2.3
9772 */
9773 $should_send_keepalive = $this->should_send_keepalive_update( true );
9774
9775 $installs_data = $this->get_installs_data_for_api( $override, ! $flush, $should_send_keepalive );
9776
9777 if ( empty( $installs_data ) ) {
9778 return false;
9779 }
9780
9781 if ( $is_two_way_sync ) {
9782 // Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9783 $this->set_cron_execution_timestamp( 'install_sync' );
9784 }
9785
9786 /**
9787 * Pass `true` to use the network level storage since the update is for many installs.
9788 *
9789 * @author Leo Fajardo (@leorw)
9790 * @since 2.2.3
9791 */
9792 $this->set_keepalive_timestamp( true );
9793
9794 // Send updated values to FS.
9795 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9796
9797 if ( $is_two_way_sync && $this->is_api_result_object( $result, 'installs' ) ) {
9798 // I successfully sent a two-way installs update, clear the scheduled install sync if it exists.
9799 $this->clear_install_sync_cron();
9800 }
9801
9802 return $result;
9803 }
9804
9805 /**
9806 * @author Leo Fajardo (@leorw)
9807 *
9808 * @param bool|null $use_network_level_storage
9809 *
9810 * @return bool
9811 */
9812 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9813 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9814
9815 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9816 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9817 return true;
9818 } else {
9819 // If updated 7 days ago or less, "flip a coin", if the value is 7 trigger a keepalive and update the last time it was triggered.
9820 return ( 7 == rand( 1, 7 ) );
9821 }
9822 }
9823
9824 /**
9825 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9826 *
9827 * @author Leo Fajardo (@leorw)
9828 * @since 2.3.2
9829 */
9830 private function maybe_sync_install_user() {
9831 if ( $this->_user->id == $this->_site->user_id ) {
9832 return;
9833 }
9834
9835 // Fetch user data and store if found.
9836 $this->sync_user_by_current_install();
9837 }
9838
9839 /**
9840 * Update install only if changed.
9841 *
9842 * @author Vova Feldman (@svovaf)
9843 * @since 1.0.9
9844 *
9845 * @param string[] string $override
9846 * @param bool $flush
9847 */
9848 function sync_install( $override = array(), $flush = false ) {
9849 $this->_logger->entrance();
9850
9851 $site = $this->send_install_update( $override, $flush, true );
9852
9853 if ( false === $site ) {
9854 // No sync required.
9855 return;
9856 }
9857
9858 if ( ! $this->is_api_result_entity( $site ) ) {
9859 // Failed to sync, don't update locally.
9860 return;
9861 }
9862
9863 $this->_site = new FS_Site( $site );
9864
9865 $this->_store_site( true );
9866 }
9867
9868 /**
9869 * Update install only if changed.
9870 *
9871 * @author Vova Feldman (@svovaf)
9872 * @since 1.0.9
9873 *
9874 * @param string[] string $override
9875 * @param bool $flush
9876 */
9877 private function sync_installs( $override = array(), $flush = false ) {
9878 $this->_logger->entrance();
9879
9880 $result = $this->send_installs_update( $override, $flush, true );
9881
9882 if ( false === $result ) {
9883 // No sync required.
9884 return;
9885 }
9886
9887 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9888 // Failed to sync, don't update locally.
9889 return;
9890 }
9891
9892 $address_to_blog_map = $this->get_address_to_blog_map();
9893
9894 foreach ( $result->installs as $install ) {
9895 $this->_site = new FS_Site( $install );
9896
9897 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9898 $blog_id = $address_to_blog_map[ $address ];
9899
9900 $this->_store_site( true, $blog_id );
9901 }
9902 }
9903
9904 /**
9905 * Track install's custom event.
9906 *
9907 * IMPORTANT:
9908 * Custom event tracking is currently only supported for specific clients.
9909 * If you are not one of them, please don't use this method. If you will,
9910 * the API will simply ignore your request based on the plugin ID.
9911 *
9912 * Need custom tracking for your plugin or theme?
9913 * If you are interested in custom event tracking please contact yo@freemius.com
9914 * for further details.
9915 *
9916 * @author Vova Feldman (@svovaf)
9917 * @since 1.2.1
9918 *
9919 * @param string $name Event name.
9920 * @param array $properties Associative key/value array with primitive values only
9921 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9922 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9923 *
9924 * @return object|false Event data or FALSE on failure.
9925 *
9926 * @throws \Freemius_InvalidArgumentException
9927 */
9928 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9929 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9930
9931 if ( ! $this->is_registered() ) {
9932 return false;
9933 }
9934
9935 $event = array( 'type' => $name );
9936
9937 if ( is_numeric( $process_at ) && $process_at > time() ) {
9938 $event['process_at'] = $process_at;
9939 }
9940
9941 if ( $once ) {
9942 $event['once'] = true;
9943 }
9944
9945 if ( ! empty( $properties ) ) {
9946 // Verify associative array values are primitive.
9947 foreach ( $properties as $k => $v ) {
9948 if ( ! is_scalar( $v ) ) {
9949 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9950 }
9951 }
9952
9953 $event['properties'] = $properties;
9954 }
9955
9956 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9957
9958 return $this->is_api_error( $result ) ?
9959 false :
9960 $result;
9961 }
9962
9963 /**
9964 * Track install's custom event only once, but it still triggers the API call.
9965 *
9966 * IMPORTANT:
9967 * Custom event tracking is currently only supported for specific clients.
9968 * If you are not one of them, please don't use this method. If you will,
9969 * the API will simply ignore your request based on the plugin ID.
9970 *
9971 * Need custom tracking for your plugin or theme?
9972 * If you are interested in custom event tracking please contact yo@freemius.com
9973 * for further details.
9974 *
9975 * @author Vova Feldman (@svovaf)
9976 * @since 1.2.1
9977 *
9978 * @param string $name Event name.
9979 * @param array $properties Associative key/value array with primitive values only
9980 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9981 *
9982 * @return object|false Event data or FALSE on failure.
9983 *
9984 * @throws \Freemius_InvalidArgumentException
9985 *
9986 * @user Freemius::track_event()
9987 */
9988 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9989 return $this->track_event( $name, $properties, $process_at, true );
9990 }
9991
9992 /**
9993 * Plugin uninstall hook.
9994 *
9995 * @author Vova Feldman (@svovaf)
9996 * @since 1.0.1
9997 *
9998 * @param bool $check_user Enforce checking if user have plugins activation privileges.
9999 */
10000 function _uninstall_plugin_event( $check_user = true ) {
10001 $this->_logger->entrance( 'slug = ' . $this->_slug );
10002
10003 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
10004 return;
10005 }
10006
10007 $params = array();
10008 $uninstall_reason = null;
10009 if ( isset( $this->_storage->uninstall_reason ) ) {
10010 $uninstall_reason = $this->_storage->uninstall_reason;
10011 $params['reason_id'] = $uninstall_reason->id;
10012 $params['reason_info'] = $uninstall_reason->info;
10013 }
10014
10015 if ( ! $this->is_registered() ) {
10016 // Send anonymous uninstall event only if user submitted a feedback.
10017 if ( isset( $uninstall_reason ) ) {
10018 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
10019 $this->opt_in( false, false, false, false, true );
10020 } else {
10021 $params['uid'] = $this->get_anonymous_id();
10022 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
10023 }
10024 }
10025 } else {
10026 $params = array_merge( $params, array(
10027 'is_active' => false,
10028 'is_uninstalled' => true,
10029 ) );
10030
10031 if ( $this->_is_network_active ) {
10032 // Send uninstall event.
10033 $this->send_installs_update( $params );
10034 } else {
10035 // Send uninstall event and handle the result.
10036 $this->sync_install( $params );
10037 }
10038 }
10039
10040 // @todo Decide if we want to delete plugin information from db.
10041 }
10042
10043 /**
10044 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
10045 *
10046 * @author Vova Feldman (@svovaf)
10047 * @since 2.2.1
10048 *
10049 * @param string $is_premium
10050 * @param string $caller
10051 *
10052 * @return void
10053 */
10054 function set_basename( $is_premium, $caller ) {
10055 $basename = plugin_basename( $caller );
10056
10057 $current_basename = $is_premium ?
10058 $this->_premium_plugin_basename :
10059 $this->_free_plugin_basename;
10060
10061 if ( $current_basename == $basename ) {
10062 // Basename value set correctly.
10063 return;
10064 }
10065
10066 if ( $is_premium ) {
10067 $this->_premium_plugin_basename = $basename;
10068 } else {
10069 $this->_free_plugin_basename = $basename;
10070 }
10071
10072 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
10073
10074 register_activation_hook(
10075 $plugin_dir . $basename,
10076 array( &$this, '_activate_plugin_event_hook' )
10077 );
10078 }
10079
10080 /**
10081 * @author Vova Feldman (@svovaf)
10082 * @since 1.1.1
10083 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
10084 *
10085 * @return string
10086 */
10087 function premium_plugin_basename() {
10088 if ( ! isset( $this->_premium_plugin_basename ) ) {
10089 $this->_premium_plugin_basename = $this->is_premium() ?
10090 // The product is premium, so use the current basename.
10091 $this->_plugin_basename :
10092 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
10093 }
10094
10095 return $this->_premium_plugin_basename;
10096 }
10097
10098 /**
10099 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
10100 *
10101 * @author Vova Feldman (@svovaf)
10102 * @since 1.0.2
10103 */
10104 public static function _uninstall_plugin_hook() {
10105 self::_load_required_static();
10106
10107 self::$_static_logger->entrance();
10108
10109 if ( ! current_user_can( 'activate_plugins' ) ) {
10110 return;
10111 }
10112
10113 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
10114
10115 self::$_static_logger->info( 'plugin = ' . $plugin_file );
10116
10117 define( 'WP_FS__UNINSTALL_MODE', true );
10118
10119 $fs = self::get_instance_by_file( $plugin_file );
10120
10121 if ( is_object( $fs ) ) {
10122 $fs->remove_sdk_reference();
10123
10124 self::require_plugin_essentials();
10125
10126 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
10127 is_plugin_active( $fs->premium_plugin_basename() )
10128 ) {
10129 // Deleting Free or Premium plugin version while the other version still installed.
10130 return;
10131 }
10132
10133 if (
10134 ! $fs->is_clone() &&
10135 /**
10136 * If there's a context install, run this method only when there's also a context user (e.g., when cloning a subsite of a multisite network into a single-site installation, it's possible for an install to be associated with a non-existing user entity; we want Freemius to be off in this case, while we are trying to recover the user).
10137 *
10138 * @author Leo Fajardo
10139 */
10140 ( ! is_object( $fs->_site ) || $fs->is_registered() )
10141 ) {
10142 $fs->_uninstall_plugin_event();
10143 }
10144
10145 $fs->do_action( 'after_uninstall' );
10146 }
10147 }
10148
10149 #----------------------------------------------------------------------------------
10150 #region Plugin Information
10151 #----------------------------------------------------------------------------------
10152
10153 /**
10154 * Load WordPress core plugin.php essential module.
10155 *
10156 * @author Vova Feldman (@svovaf)
10157 * @since 1.1.1
10158 */
10159 private static function require_plugin_essentials() {
10160 if ( ! function_exists( 'get_plugins' ) ) {
10161 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
10162
10163 require_once ABSPATH . 'wp-admin/includes/plugin.php';
10164 }
10165 }
10166
10167 /**
10168 * Load WordPress core pluggable.php module.
10169 *
10170 * @author Vova Feldman (@svovaf)
10171 * @since 1.1.2
10172 */
10173 private static function require_pluggable_essentials() {
10174 if ( ! function_exists( 'wp_get_current_user' ) ) {
10175 require_once ABSPATH . 'wp-includes/pluggable.php';
10176 }
10177 }
10178
10179 /**
10180 * Return plugin data.
10181 *
10182 * @author Vova Feldman (@svovaf)
10183 * @since 1.0.1
10184 *
10185 * @param bool $reparse_plugin_metadata
10186 *
10187 * @return array
10188 */
10189 function get_plugin_data( $reparse_plugin_metadata = false ) {
10190 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
10191 self::require_plugin_essentials();
10192
10193 if ( $this->is_plugin() ) {
10194 /**
10195 * @author Vova Feldman (@svovaf)
10196 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
10197 *
10198 * @link https://github.com/Freemius/wordpress-sdk/issues/77
10199 */
10200 $plugin_data = get_plugin_data(
10201 $this->_plugin_main_file_path,
10202 false,
10203 false
10204 );
10205 } else {
10206 $theme_data = wp_get_theme();
10207
10208 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
10209 $parent_theme = $theme_data->parent();
10210
10211 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
10212 $theme_data = $parent_theme;
10213 }
10214 }
10215
10216 $plugin_data = array(
10217 'Name' => $theme_data->get( 'Name' ),
10218 'Version' => $theme_data->get( 'Version' ),
10219 'Author' => $theme_data->get( 'Author' ),
10220 'Description' => $theme_data->get( 'Description' ),
10221 'PluginURI' => $theme_data->get( 'ThemeURI' ),
10222 );
10223 }
10224
10225 $this->_plugin_data = $plugin_data;
10226 }
10227
10228 return $this->_plugin_data;
10229 }
10230
10231 /**
10232 * @author Vova Feldman (@svovaf)
10233 * @since 1.0.1
10234 * @since 1.2.2.5 If slug not set load slug by module ID.
10235 *
10236 * @return string Plugin slug.
10237 */
10238 function get_slug() {
10239 if ( ! isset( $this->_slug ) ) {
10240 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
10241 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
10242 }
10243
10244 return $this->_slug;
10245 }
10246
10247 /**
10248 * @author Leo Fajardo (@leorw)
10249 * @since 2.2.1
10250 *
10251 * @return string
10252 */
10253 function get_premium_slug() {
10254 return ( is_object( $this->_plugin ) && ! empty( $this->_plugin->premium_slug ) ) ?
10255 $this->_plugin->premium_slug :
10256 "{$this->_slug}-premium";
10257 }
10258
10259 /**
10260 * Retrieve the desired folder name for the product.
10261 *
10262 * @author Vova Feldman (@svovaf)
10263 * @since 1.2.1.7
10264 *
10265 * @return string Plugin slug.
10266 */
10267 function get_target_folder_name() {
10268 return $this->can_use_premium_code() ?
10269 $this->_plugin->premium_slug :
10270 $this->_slug;
10271 }
10272
10273 /**
10274 * @author Vova Feldman (@svovaf)
10275 * @since 1.0.1
10276 *
10277 * @return number Plugin ID.
10278 */
10279 function get_id() {
10280 return $this->_plugin->id;
10281 }
10282
10283 /**
10284 * @author Leo Fajardo (@leorw)
10285 * @since 2.2.4
10286 *
10287 * @return number|null Bundle ID.
10288 */
10289 function get_bundle_id() {
10290 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
10291 $this->_plugin->bundle_id :
10292 null;
10293 }
10294
10295 /**
10296 * @author Vova Feldman (@svovaf)
10297 * @since 2.3.1
10298 *
10299 * @return string|null Bundle public key.
10300 */
10301 function get_bundle_public_key() {
10302 return isset( $this->_plugin->bundle_public_key ) ?
10303 $this->_plugin->bundle_public_key :
10304 null;
10305 }
10306
10307 /**
10308 * Get whether the SDK has been initiated in the context of a Bundle.
10309 *
10310 * This will return true, if `bundle_id` is present in the SDK init parameters.
10311 *
10312 * ```php
10313 * $my_fs = fs_dynamic_init( array(
10314 * // ...
10315 * 'bundle_id' => 'XXXX', // Will return true since we have bundle id.
10316 * 'bundle_public_key' => 'pk_XXXX',
10317 * ) );
10318 * ```
10319 *
10320 * @author Swashata Ghosh (@swashata)
10321 * @since 2.5.0
10322 *
10323 * @return bool True if we are running in bundle context, false otherwise.
10324 */
10325 private function has_bundle_context() {
10326 return ! is_null( $this->get_bundle_id() );
10327 }
10328
10329 /**
10330 * @author Vova Feldman (@svovaf)
10331 * @since 1.2.1.5
10332 *
10333 * @return string Freemius SDK version
10334 */
10335 function get_sdk_version() {
10336 return $this->version;
10337 }
10338
10339 /**
10340 * @author Vova Feldman (@svovaf)
10341 * @since 1.2.1.5
10342 *
10343 * @return number Parent plugin ID (if parent exist).
10344 */
10345 function get_parent_id() {
10346 return $this->is_addon() ?
10347 $this->get_parent_instance()->get_id() :
10348 $this->_plugin->id;
10349 }
10350
10351 /**
10352 * @author Vova Feldman (@svovaf)
10353 * @since 2.3.1
10354 *
10355 * @return string
10356 */
10357 function get_usage_tracking_terms_url() {
10358 return $this->apply_filters(
10359 'usage_tracking_terms_url',
10360 "https://freemius.com/product/opt-in/{$this->_plugin->id}/{$this->_slug}/"
10361 );
10362 }
10363
10364 /**
10365 * @todo (For LiteSDK) We can refactor this and other related functions giving links to several landing pages on freemius.com to come from a separate class like `FS_Terms_Pages`. This would get a `FS_WP_Hook` (hypothetical) instance as a dependency and use it to hook into the `license_activation_terms_url` or related filters. The entry level instance from `ms_fs()` would hold a public read-only variable `my_fs()->terms_pages` which would be an instance of `FS_Terms_Pages` and would hold all the links to the terms pages.
10366 * @since 2.5.8
10367 *
10368 * @return string
10369 */
10370 function get_license_activation_terms_url() {
10371 return $this->apply_filters(
10372 'license_activation_terms_url',
10373 "https://freemius.com/product/license-activation/{$this->_plugin->id}/{$this->_slug}/"
10374 );
10375 }
10376
10377 /**
10378 * @author Vova Feldman (@svovaf)
10379 * @since 2.3.1
10380 *
10381 * @return string
10382 */
10383 function get_eula_url() {
10384 return $this->apply_filters(
10385 'eula_url',
10386 "https://freemius.com/product/{$this->_plugin->id}/{$this->_slug}/legal/eula/"
10387 );
10388 }
10389
10390 /**
10391 * @author Vova Feldman (@svovaf)
10392 * @since 1.0.1
10393 *
10394 * @return string Plugin public key.
10395 */
10396 function get_public_key() {
10397 return $this->_plugin->public_key;
10398 }
10399
10400 /**
10401 * Will be available only on sandbox mode.
10402 *
10403 * @author Vova Feldman (@svovaf)
10404 * @since 1.0.4
10405 *
10406 * @return mixed Plugin secret key.
10407 */
10408 function get_secret_key() {
10409 return $this->_plugin->secret_key;
10410 }
10411
10412 /**
10413 * @author Vova Feldman (@svovaf)
10414 * @since 1.1.1
10415 *
10416 * @return bool
10417 */
10418 function has_secret_key() {
10419 return ! empty( $this->_plugin->secret_key );
10420 }
10421
10422 /**
10423 * @author Vova Feldman (@svovaf)
10424 * @since 1.0.9
10425 *
10426 * @param string|bool $premium_suffix
10427 *
10428 * @return string
10429 */
10430 function get_plugin_name( $premium_suffix = false ) {
10431 $this->_logger->entrance();
10432
10433 /**
10434 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10435 *
10436 * @author Vova Feldman
10437 */
10438 if ( ! isset( $this->_plugin_name ) ) {
10439 // Name is not yet set.
10440 $this->set_name( $premium_suffix );
10441 } else if (
10442 ! empty( $premium_suffix ) &&
10443 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10444 ) {
10445 // Name is already set, but there's a change in the premium suffix.
10446 $this->set_name( $premium_suffix );
10447 }
10448
10449 return $this->_plugin_name;
10450 }
10451
10452 /**
10453 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10454 *
10455 * @author Vova Feldman (@svovaf)
10456 * @since 2.2.1
10457 *
10458 * @param string $premium_suffix
10459 */
10460 private function set_name( $premium_suffix = '' ) {
10461 $plugin_data = $this->get_plugin_data();
10462
10463 // Get name.
10464 $this->_plugin_name = $plugin_data['Name'];
10465
10466 if ( is_string( $premium_suffix ) ) {
10467 $premium_suffix = trim( $premium_suffix );
10468
10469 if ( ! empty( $premium_suffix ) ) {
10470 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10471 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10472 $suffix_len = strlen( $suffix );
10473
10474 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10475 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10476 ) {
10477 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10478 }
10479 }
10480 }
10481
10482 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10483 }
10484
10485 /**
10486 * @author Vova Feldman (@svovaf)
10487 * @since 1.0.0
10488 *
10489 * @param bool $reparse_plugin_metadata
10490 *
10491 * @return string
10492 */
10493 function get_plugin_version( $reparse_plugin_metadata = false ) {
10494 $this->_logger->entrance();
10495
10496 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10497
10498 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10499
10500 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10501 }
10502
10503 /**
10504 * @author Vova Feldman (@svovaf)
10505 * @since 1.2.1.7
10506 *
10507 * @return string
10508 */
10509 function get_plugin_title() {
10510 $this->_logger->entrance();
10511
10512 $title = $this->_plugin->title;
10513
10514 return $this->apply_filters( 'plugin_title', $title );
10515 }
10516
10517 /**
10518 * @author Vova Feldman (@svovaf)
10519 * @since 1.2.2.7
10520 *
10521 * @param bool $lowercase
10522 *
10523 * @return string
10524 */
10525 function get_module_label( $lowercase = false ) {
10526 $label = $this->is_addon() ?
10527 $this->get_text_inline( 'Add-On', 'addon' ) :
10528 ( $this->is_plugin() ?
10529 $this->get_text_inline( 'Plugin', 'plugin' ) :
10530 $this->get_text_inline( 'Theme', 'theme' ) );
10531
10532 if ( $lowercase ) {
10533 $label = strtolower( $label );
10534 }
10535
10536 return $label;
10537 }
10538
10539 /**
10540 * @author Vova Feldman (@svovaf)
10541 * @since 1.0.4
10542 *
10543 * @return string
10544 */
10545 function get_plugin_basename() {
10546 if ( ! isset( $this->_plugin_basename ) ) {
10547 if ( $this->is_plugin() ) {
10548 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10549 } else {
10550 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10551 }
10552 }
10553
10554 return $this->_plugin_basename;
10555 }
10556
10557 function get_plugin_folder_name() {
10558 $this->_logger->entrance();
10559
10560 $plugin_folder = $this->_plugin_basename;
10561
10562 while ( '.' !== dirname( $plugin_folder ) ) {
10563 $plugin_folder = dirname( $plugin_folder );
10564 }
10565
10566 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10567
10568 return $plugin_folder;
10569 }
10570
10571 #endregion ------------------------------------------------------------------
10572
10573 /* Account
10574 ------------------------------------------------------------------------------------------------------------------*/
10575
10576 /**
10577 * Find plugin's slug by plugin's basename.
10578 *
10579 * @author Vova Feldman (@svovaf)
10580 * @since 1.0.9
10581 *
10582 * @param string $plugin_base_name
10583 *
10584 * @return false|string
10585 */
10586 private static function find_slug_by_basename( $plugin_base_name ) {
10587 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10588
10589 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10590 return false;
10591 }
10592
10593 return $file_slug_map[ $plugin_base_name ];
10594 }
10595
10596 /**
10597 * Store the map between the plugin's basename to the slug.
10598 *
10599 * @author Vova Feldman (@svovaf)
10600 * @since 1.0.9
10601 */
10602 private function store_file_slug_map() {
10603 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10604
10605 if ( ! array( $file_slug_map ) ) {
10606 $file_slug_map = array();
10607 }
10608
10609 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10610 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10611 ) {
10612 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10613 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10614 }
10615 }
10616
10617 /**
10618 * @return array[number]FS_User
10619 */
10620 static function get_all_users() {
10621 $users = self::maybe_get_entities_account_option( 'users', array() );
10622
10623 if ( ! is_array( $users ) ) {
10624 $users = array();
10625 }
10626
10627 return $users;
10628 }
10629
10630 /**
10631 * @param string $module_type
10632 * @param null|int $blog_id Since 2.0.0
10633 *
10634 * @return array[string]FS_Site
10635 */
10636 private static function get_all_sites(
10637 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10638 $blog_id = null,
10639 $is_backup = false
10640 ) {
10641 $sites = self::get_account_option(
10642 ( $is_backup ? 'prev_' : '' ) . 'sites',
10643 $module_type,
10644 $blog_id
10645 );
10646
10647 if ( ! is_array( $sites ) ) {
10648 $sites = array();
10649 }
10650
10651 return $sites;
10652 }
10653
10654 /**
10655 * @author Leo Fajardo (@leorw)
10656 *
10657 * @since 1.2.2
10658 *
10659 * @param string $option_name
10660 * @param string $module_type
10661 * @param null|int $network_level_or_blog_id Since 2.0.0
10662 *
10663 * @return mixed
10664 */
10665 private static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10666 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10667 $option_name = $module_type . '_' . $option_name;
10668 }
10669
10670 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10671 }
10672
10673 /**
10674 * @author Leo Fajardo (@leorw)
10675 *
10676 * @since 1.2.2
10677 *
10678 * @param string $option_name
10679 * @param mixed $option_value
10680 * @param bool $store
10681 * @param null|int $network_level_or_blog_id Since 2.0.0
10682 */
10683 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10684 self::set_account_option_by_module(
10685 $this->_module_type,
10686 $option_name,
10687 $option_value,
10688 $store,
10689 $network_level_or_blog_id
10690 );
10691 }
10692
10693 /**
10694 * @author Vova Feldman (@svovaf)
10695 *
10696 * @since 1.2.2.7
10697 *
10698 * @param string $module_type
10699 * @param string $option_name
10700 * @param mixed $option_value
10701 * @param bool $store
10702 * @param null|int $network_level_or_blog_id Since 2.0.0
10703 */
10704 private static function set_account_option_by_module(
10705 $module_type,
10706 $option_name,
10707 $option_value,
10708 $store,
10709 $network_level_or_blog_id = null
10710 ) {
10711 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10712 $option_name = $module_type . '_' . $option_name;
10713 }
10714
10715 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10716 }
10717
10718 /**
10719 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10720 *
10721 * @author Leo Fajardo (@leorw)
10722 * @since 2.3.1
10723 *
10724 * @param string $option_name
10725 * @param mixed $default
10726 * @param null|bool|int $network_level_or_blog_id When an integer, use the given blog storage. When `true` use the multisite storage (if there's a network). When `false`, use the current context blog storage. When `null`, the decision which storage to use (MS vs. Current S) will be handled internally and determined based on the $option (based on self::$_SITE_LEVEL_PARAMS).
10727 *
10728 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10729 */
10730 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10731 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10732
10733 $class_name = '';
10734
10735 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10736 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10737 }
10738
10739 switch ( $option_name ) {
10740 case 'plugins':
10741 case 'themes':
10742 case 'addons':
10743 $class_name = FS_Plugin::get_class_name();
10744 break;
10745 case 'users':
10746 $class_name = FS_User::get_class_name();
10747 break;
10748 case 'sites':
10749 $class_name = FS_Site::get_class_name();
10750 break;
10751 case 'licenses':
10752 case 'all_licenses':
10753 $class_name = FS_Plugin_License::get_class_name();
10754 break;
10755 case 'plans':
10756 $class_name = FS_Plugin_Plan::get_class_name();
10757 break;
10758 case 'updates':
10759 $class_name = FS_Plugin_Tag::get_class_name();
10760 break;
10761 }
10762
10763 if ( empty( $class_name ) ) {
10764 return $option;
10765 }
10766
10767 return fs_get_entities( $option, $class_name );
10768 }
10769
10770 /**
10771 * @author Vova Feldman (@svovaf)
10772 * @since 1.0.6
10773 *
10774 * @param number|null $module_id
10775 *
10776 * @return FS_Plugin_License[]
10777 */
10778 private static function get_all_licenses( $module_id = null ) {
10779 $licenses = self::get_account_option( 'all_licenses' );
10780
10781 if ( ! is_array( $licenses ) ) {
10782 $licenses = array();
10783 }
10784
10785 if ( is_null( $module_id ) ) {
10786 return $licenses;
10787 }
10788
10789 $licenses = isset( $licenses[ $module_id ] ) ?
10790 $licenses[ $module_id ] :
10791 array();
10792
10793 return $licenses;
10794 }
10795
10796 /**
10797 * @author Leo Fajardo (@leorw)
10798 * @since 2.0.0
10799 *
10800 * @return array
10801 */
10802 private static function get_all_licenses_by_module_type() {
10803 $licenses = self::get_account_option( 'all_licenses' );
10804
10805 $licenses_by_module_type = array(
10806 WP_FS__MODULE_TYPE_PLUGIN => array(),
10807 WP_FS__MODULE_TYPE_THEME => array()
10808 );
10809
10810 if ( ! is_array( $licenses ) ) {
10811 return $licenses_by_module_type;
10812 }
10813
10814 foreach ( $licenses as $module_id => $module_licenses ) {
10815 $fs = self::get_instance_by_id( $module_id );
10816 if ( false === $fs ) {
10817 continue;
10818 }
10819
10820 $licenses_by_module_type[ $fs->_module_type ] = array_merge( $licenses_by_module_type[ $fs->_module_type ], $module_licenses );
10821 }
10822
10823 return $licenses_by_module_type;
10824 }
10825
10826 /**
10827 * @author Leo Fajardo (@leorw)
10828 * @since 2.0.0
10829 *
10830 * @param number $module_id
10831 * @param number|null $user_id
10832 *
10833 * @return array
10834 */
10835 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10836 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10837
10838 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10839 $all_modules_user_id_license_ids_map = array();
10840 }
10841
10842 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10843 $all_modules_user_id_license_ids_map[ $module_id ] :
10844 array();
10845
10846 if ( FS_User::is_valid_id( $user_id ) ) {
10847 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10848 $user_id_license_ids_map[ $user_id ] :
10849 array();
10850 }
10851
10852 return $user_id_license_ids_map;
10853 }
10854
10855 /**
10856 * @author Leo Fajardo (@leorw)
10857 * @since 2.0.0
10858 *
10859 * @param array $new_user_id_license_ids_map
10860 * @param number $module_id
10861 * @param number|null $user_id
10862 */
10863 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10864 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10865 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10866 $all_modules_user_id_license_ids_map = array();
10867 }
10868
10869 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10870 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10871 }
10872
10873 if ( FS_User::is_valid_id( $user_id ) ) {
10874 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10875 } else {
10876 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10877 }
10878
10879 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10880 }
10881
10882 /**
10883 * Get a collection of the user's linked license IDs.
10884 *
10885 * @author Vova Feldman (@svovaf)
10886 * @since 2.0.0
10887 *
10888 * @param number $user_id
10889 *
10890 * @return number[]
10891 */
10892 private function get_user_linked_license_ids( $user_id ) {
10893 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10894 }
10895
10896 /**
10897 * Override the user's linked license IDs with a new IDs collection.
10898 *
10899 * @author Vova Feldman (@svovaf)
10900 * @since 2.0.0
10901 *
10902 * @param number $user_id
10903 * @param number[] $license_ids
10904 */
10905 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10906 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10907 }
10908
10909 /**
10910 * Link a specified license ID to a given user.
10911 *
10912 * @author Vova Feldman (@svovaf)
10913 * @since 2.0.0
10914 *
10915 * @param number $license_id
10916 * @param number $user_id
10917 */
10918 private function link_license_2_user( $license_id, $user_id ) {
10919 $license_ids = $this->get_user_linked_license_ids( $user_id );
10920
10921 if ( in_array( $license_id, $license_ids ) ) {
10922 // License already linked.
10923 return;
10924 }
10925
10926 $license_ids[] = $license_id;
10927
10928 $this->set_user_linked_license_ids( $user_id, $license_ids );
10929 }
10930
10931 /**
10932 * @param string|bool $module_type
10933 *
10934 * @return FS_Plugin_Plan[]
10935 */
10936 private static function get_all_plans( $module_type = false ) {
10937 $plans = self::get_account_option( 'plans', $module_type );
10938
10939 if ( ! is_array( $plans ) ) {
10940 $plans = array();
10941 }
10942
10943 return $plans;
10944 }
10945
10946 /**
10947 * @author Vova Feldman (@svovaf)
10948 * @since 1.0.4
10949 *
10950 * @return FS_Plugin_Tag[]
10951 */
10952 private static function get_all_updates() {
10953 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10954
10955 if ( ! is_array( $updates ) ) {
10956 $updates = array();
10957 }
10958
10959 return $updates;
10960 }
10961
10962 /**
10963 * @author Vova Feldman (@svovaf)
10964 * @since 1.0.6
10965 *
10966 * @return array<number,FS_Plugin[]>|false
10967 */
10968 private static function get_all_addons() {
10969 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10970
10971 if ( ! is_array( $addons ) ) {
10972 $addons = array();
10973 }
10974
10975 return $addons;
10976 }
10977
10978 /**
10979 * @author Vova Feldman (@svovaf)
10980 * @since 1.0.6
10981 *
10982 * @return number[]|false
10983 */
10984 private static function get_all_account_addons() {
10985 $addons = self::$_accounts->get_option( 'account_addons', array() );
10986
10987 if ( ! is_array( $addons ) ) {
10988 $addons = array();
10989 }
10990
10991 return $addons;
10992 }
10993
10994 /**
10995 * Check if user has connected his account (opted-in).
10996 *
10997 * Note:
10998 * If the user opted-in and opted-out on a later stage,
10999 * this will still return true. If you want to check if the
11000 * user is currently opted-in, use:
11001 * `$fs->is_registered() && $fs->is_tracking_allowed()`
11002 *
11003 * @author Vova Feldman (@svovaf)
11004 * @since 1.0.1
11005 *
11006 * @param bool $ignore_anonymous_state Since 2.5.1
11007 *
11008 * @return bool
11009 */
11010 function is_registered( $ignore_anonymous_state = false ) {
11011 return (
11012 is_object( $this->_user ) &&
11013 (
11014 $this->is_premium() ||
11015 $ignore_anonymous_state ||
11016 ! $this->is_anonymous()
11017 )
11018 );
11019 }
11020
11021 /**
11022 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
11023 *
11024 * @author Leo Fajardo (@leorw)
11025 * @since 1.2.1.5
11026 *
11027 * @return bool
11028 */
11029 function is_tracking_allowed( $blog_id = null, $install = null ) {
11030 if ( is_null( $install ) ) {
11031 $install = is_null( $blog_id ) ?
11032 $this->_site :
11033 $this->get_install_by_blog_id( $blog_id );
11034 }
11035
11036 return (
11037 is_object( $install ) &&
11038 FS_Permission_Manager::instance( $this )->is_homepage_url_tracking_allowed( $blog_id )
11039 );
11040 }
11041
11042 /**
11043 * Returns TRUE if the user never opted-in or manually opted-out.
11044 *
11045 * @author Vova Feldman (@svovaf)
11046 * @since 1.2.1.5
11047 *
11048 * @param int|null $blog_id
11049 *
11050 * @return bool
11051 */
11052 function is_tracking_prohibited( $blog_id = null ) {
11053 return (
11054 ! $this->is_registered( true ) ||
11055 ! $this->is_tracking_allowed( $blog_id )
11056 );
11057 }
11058
11059 /**
11060 * @author Leo Fajardo (@leorw)
11061 * @since 2.4.0
11062 *
11063 * @return bool
11064 */
11065 function is_bundle_license_auto_activation_enabled() {
11066 return $this->is_addon() ?
11067 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
11068 $this->_is_bundle_license_auto_activation_enabled;
11069 }
11070
11071 /**
11072 * @author Vova Feldman (@svovaf)
11073 * @since 1.0.4
11074 *
11075 * @return FS_Plugin
11076 */
11077 function get_plugin() {
11078 return $this->_plugin;
11079 }
11080
11081 /**
11082 * @author Vova Feldman (@svovaf)
11083 * @since 1.0.3
11084 *
11085 * @return FS_User
11086 */
11087 function get_user() {
11088 return $this->_user;
11089 }
11090
11091 /**
11092 * @author Vova Feldman (@svovaf)
11093 * @since 1.0.3
11094 *
11095 * @return FS_Site
11096 */
11097 function get_site() {
11098 return $this->_site;
11099 }
11100
11101 /**
11102 * @author Leo Fajardo (@leorw)
11103 * @since 2.5.0
11104 */
11105 function store_site( $site ) {
11106 $this->_site = $site;
11107 $this->_store_site( true );
11108 }
11109
11110 /**
11111 * Deletes the current install with an option to back it up in case restoration will be needed (e.g., if the automatic clone resolution attempt fails).
11112 *
11113 * @author Leo Fajardo (@leorw)
11114 * @since 2.5.0
11115 */
11116 function delete_current_install( $back_up ) {
11117 // Back up and delete the unique ID.
11118 if ( $back_up ) {
11119 self::$_accounts->set_option( 'prev_unique_id', $this->get_anonymous_id() );
11120 }
11121
11122 self::$_accounts->set_option( 'unique_id', null );
11123
11124 if ( $back_up ) {
11125 // Back up the install before deleting it so that it can be restored later on if necessary (e.g., if the automatic clone resolution attempt fails).
11126 $this->back_up_site();
11127 }
11128
11129 $this->_delete_site();
11130 $this->_site = null;
11131 }
11132
11133 /**
11134 * @author Leo Fajardo (@leorw)
11135 * @since 2.5.0
11136 */
11137 function restore_backup_site() {
11138 self::$_accounts->set_option(
11139 'unique_id',
11140 self::$_accounts->get_option( 'prev_unique_id' )
11141 );
11142
11143 $sites = self::get_all_sites( $this->_module_type, null, true );
11144 $this->store_site( clone $sites[ $this->_slug ] );
11145 }
11146
11147 /**
11148 * Get plugin add-ons.
11149 *
11150 * @author Vova Feldman (@svovaf)
11151 * @since 1.0.6
11152 *
11153 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
11154 *
11155 * @param bool $flush
11156 *
11157 * @return FS_Plugin[]|false
11158 */
11159 function get_addons( $flush = false ) {
11160 $this->_logger->entrance();
11161
11162 if ( ! $this->_has_addons ) {
11163 return false;
11164 }
11165
11166 $addons = $this->sync_addons( $flush );
11167
11168 return ( ! is_array( $addons ) || empty( $addons ) ) ?
11169 false :
11170 $addons;
11171 }
11172
11173 /**
11174 * @author Vova Feldman (@svovaf)
11175 * @since 1.0.6
11176 *
11177 * @return number[]|false
11178 */
11179 function get_account_addons() {
11180 $this->_logger->entrance();
11181
11182 $addons = self::get_all_account_addons();
11183
11184 if ( ! is_array( $addons ) ||
11185 ! isset( $addons[ $this->_plugin->id ] ) ||
11186 ! is_array( $addons[ $this->_plugin->id ] ) ||
11187 0 === count( $addons[ $this->_plugin->id ] )
11188 ) {
11189 return false;
11190 }
11191
11192 return $addons[ $this->_plugin->id ];
11193 }
11194
11195 /**
11196 * Check if user has any
11197 *
11198 * @author Vova Feldman (@svovaf)
11199 * @since 1.1.6
11200 *
11201 * @return bool
11202 */
11203 function has_account_addons() {
11204 $addons = $this->get_account_addons();
11205
11206 return is_array( $addons ) && ( 0 < count( $addons ) );
11207 }
11208
11209
11210 /**
11211 * Get add-on by ID (from local data).
11212 *
11213 * @author Vova Feldman (@svovaf)
11214 * @since 1.0.6
11215 *
11216 * @param number $id
11217 *
11218 * @return FS_Plugin|false
11219 */
11220 function get_addon( $id ) {
11221 $this->_logger->entrance();
11222
11223 $addons = $this->get_addons();
11224
11225 if ( is_array( $addons ) ) {
11226 foreach ( $addons as $addon ) {
11227 if ( $id == $addon->id ) {
11228 return $addon;
11229 }
11230 }
11231 }
11232
11233 return false;
11234 }
11235
11236 /**
11237 * Get add-on by slug (from local data).
11238 *
11239 * @author Vova Feldman (@svovaf)
11240 * @since 1.0.6
11241 *
11242 * @param string $slug
11243 *
11244 * @param bool $flush
11245 *
11246 * @return FS_Plugin|false
11247 */
11248 function get_addon_by_slug( $slug, $flush = false ) {
11249 $this->_logger->entrance();
11250
11251 $addons = $this->get_addons( $flush );
11252
11253 if ( is_array( $addons ) ) {
11254 foreach ( $addons as $addon ) {
11255 if ( $slug === $addon->slug ) {
11256 return $addon;
11257 }
11258 }
11259 }
11260
11261 return false;
11262 }
11263
11264 /**
11265 * @var array<number,object[]> {
11266 * @key number Add-on ID.
11267 * @val object[] The add-on's plans and prices object.
11268 * }
11269 */
11270 private $plans_and_pricing_by_addon_id;
11271
11272 /**
11273 * @author Leo Fajardo (@leorw)
11274 * @since 2.3.0
11275 *
11276 * @return array<number,object[]> {
11277 * @key number Add-on ID.
11278 * @val object[] The add-on's plans and prices object.
11279 * }
11280 */
11281 function _get_addons_plans_and_pricing_map_by_id() {
11282 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
11283 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
11284
11285 $plans_and_pricing_by_addon_id = array();
11286 if ( $this->is_api_result_object( $result, 'addons' ) ) {
11287 foreach ( $result->addons as $addon ) {
11288 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
11289 }
11290 }
11291
11292 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
11293 }
11294
11295 return $this->plans_and_pricing_by_addon_id;
11296 }
11297
11298 /**
11299 * @author Leo Fajardo (@leorw)
11300 * @since 2.3.0
11301 *
11302 * @param number $addon_id
11303 * @param bool $is_installed
11304 *
11305 * @return array
11306 */
11307 function _get_addon_info( $addon_id, $is_installed ) {
11308 $addon = $this->get_addon( $addon_id );
11309
11310 if ( ! is_object( $addon ) ) {
11311 // Unexpected call.
11312 return array();
11313 }
11314
11315 $slug = $addon->slug;
11316
11317 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
11318
11319 if ( ! fs_is_network_admin() ) {
11320 // Get blog-level activated installations.
11321 $sites = self::maybe_get_entities_account_option( 'sites', array() );
11322 } else {
11323 $sites = null;
11324
11325 if ( $this->is_addon_activated( $addon_id ) &&
11326 $this->get_addon_instance( $addon_id )->is_network_active()
11327 ) {
11328 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
11329 // Get network-level activated installations.
11330 $sites = self::maybe_get_entities_account_option(
11331 'sites',
11332 array(),
11333 $addon_storage->network_install_blog_id
11334 );
11335 }
11336 }
11337 }
11338
11339 $addon_info = array(
11340 'is_connected' => false,
11341 'slug' => $slug,
11342 'title' => $addon->title,
11343 'is_whitelabeled' => $addon_storage->is_whitelabeled
11344 );
11345
11346 if ( ! $is_installed ) {
11347 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
11348
11349 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
11350 $has_paid_plan = false;
11351 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
11352
11353 if ( is_array( $plans ) && count( $plans ) > 0 ) {
11354 foreach ( $plans as $plan ) {
11355 if ( isset( $plan->pricing ) &&
11356 is_array( $plan->pricing ) &&
11357 count( $plan->pricing ) > 0
11358 ) {
11359 $has_paid_plan = true;
11360 break;
11361 }
11362 }
11363 }
11364
11365 $addon_info['has_paid_plan'] = $has_paid_plan;
11366 }
11367 }
11368
11369 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
11370 return $addon_info;
11371 }
11372
11373 $site = $sites[ $slug ];
11374
11375 $addon_info['is_connected'] = (
11376 ( $addon->parent_plugin_id == $this->get_id() ) &&
11377 is_object( $site ) &&
11378 FS_Site::is_valid_id( $site->id ) &&
11379 FS_User::is_valid_id( $site->user_id ) &&
11380 FS_Plugin_Plan::is_valid_id( $site->plan_id )
11381 );
11382
11383 if ( $addon_info['is_connected'] && $is_installed ) {
11384 return $addon_info;
11385 }
11386
11387 $addon_info['site'] = $site;
11388
11389 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
11390 if ( isset( $plugins_data[ $slug ] ) ) {
11391 $plugin_data = $plugins_data[ $slug ];
11392
11393 $addon_info['version'] = $plugin_data->version;
11394 }
11395
11396 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
11397 if ( isset( $all_plans[ $slug ] ) ) {
11398 $plans = $all_plans[ $slug ];
11399
11400 foreach ( $plans as $plan ) {
11401 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
11402 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
11403 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
11404 break;
11405 }
11406 }
11407 }
11408
11409 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
11410 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
11411 foreach ( $licenses[ $addon_id ] as $license ) {
11412 if ( $license->id == $site->license_id ) {
11413 $addon_info['license'] = $license;
11414 break;
11415 }
11416 }
11417 }
11418
11419 if ( isset( $addon_info['license'] ) ) {
11420 if ( isset( $addon_storage->subscriptions ) &&
11421 ! empty( $addon_storage->subscriptions )
11422 ) {
11423 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11424
11425 foreach ( $addon_subscriptions as $subscription ) {
11426 if ( $subscription->license_id == $site->license_id ) {
11427 $addon_info['subscription'] = $subscription;
11428 break;
11429 }
11430 }
11431 }
11432 }
11433
11434 return $addon_info;
11435 }
11436
11437 /**
11438 * @author Vova Feldman (@svovaf)
11439 * @since 2.0.0
11440 *
11441 * @param number $user_id
11442 *
11443 * @return FS_User
11444 */
11445 static function _get_user_by_id( $user_id ) {
11446 self::$_static_logger->entrance( "user_id = {$user_id}" );
11447
11448 $users = self::get_all_users();
11449
11450 if ( is_array( $users ) ) {
11451 if ( isset( $users[ $user_id ] ) &&
11452 $users[ $user_id ] instanceof FS_User &&
11453 $user_id == $users[ $user_id ]->id
11454 ) {
11455 return $users[ $user_id ];
11456 }
11457
11458 // If user wasn't found by the key, iterate over all the users collection.
11459 foreach ( $users as $user ) {
11460 /**
11461 * @var FS_User $user
11462 */
11463 if ( $user_id == $user->id ) {
11464 return $user;
11465 }
11466 }
11467 }
11468
11469 return null;
11470 }
11471
11472 /**
11473 * Checks if a Freemius user_id is associated with a super-admin.
11474 *
11475 * @author Vova Feldman (@svovaf)
11476 * @since 2.0.0
11477 *
11478 * @param number $user_id
11479 *
11480 * @return bool
11481 */
11482 private static function is_super_admin( $user_id ) {
11483 $is_super_admin = false;
11484
11485 $user = self::_get_user_by_id( $user_id );
11486
11487 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11488 self::require_pluggable_essentials();
11489
11490 $wp_user = get_user_by( 'email', $user->email );
11491
11492 if ( $wp_user instanceof WP_User ) {
11493 $super_admins = get_super_admins();
11494 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11495 }
11496 }
11497
11498 return $is_super_admin;
11499 }
11500
11501 #----------------------------------------------------------------------------------
11502 #region Plans & Licensing
11503 #----------------------------------------------------------------------------------
11504
11505 /**
11506 * Check if running premium plugin code.
11507 *
11508 * @author Vova Feldman (@svovaf)
11509 * @since 1.0.5
11510 *
11511 * @return bool
11512 */
11513 function is_premium() {
11514 /**
11515 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11516 * `register_constructor_hooks` method.
11517 *
11518 * @author Leo Fajardo (@leorw)
11519 * @since 2.2.3
11520 */
11521 return is_object( $this->_plugin ) ?
11522 $this->_plugin->is_premium :
11523 false;
11524 }
11525
11526 /**
11527 * Get site's plan ID.
11528 *
11529 * @author Vova Feldman (@svovaf)
11530 * @since 1.0.2
11531 *
11532 * @return number
11533 */
11534 function get_plan_id() {
11535 return $this->_site->plan_id;
11536 }
11537
11538 /**
11539 * Get site's plan title.
11540 *
11541 * @author Vova Feldman (@svovaf)
11542 * @since 1.0.2
11543 *
11544 * @return string
11545 */
11546 function get_plan_title() {
11547 $plan = $this->get_plan();
11548
11549 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11550 }
11551
11552 /**
11553 * Get site's plan name.
11554 *
11555 * @author Vova Feldman (@svovaf)
11556 * @since 2.0.0
11557 *
11558 * @return string
11559 */
11560 function get_plan_name() {
11561 $plan = $this->get_plan();
11562
11563 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11564 }
11565
11566 /**
11567 * @author Vova Feldman (@svovaf)
11568 * @since 1.0.9
11569 *
11570 * @return FS_Plugin_Plan|false
11571 */
11572 function get_plan() {
11573 if ( ! is_object( $this->_site ) ) {
11574 return false;
11575 }
11576
11577 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11578 $this->_get_plan_by_id( $this->_site->plan_id ) :
11579 false;
11580 }
11581
11582 /**
11583 * @author Vova Feldman (@svovaf)
11584 * @since 1.0.3
11585 *
11586 * @return bool
11587 */
11588 function is_trial() {
11589 $this->_logger->entrance();
11590
11591 if ( ! $this->is_registered( true ) || ! is_object( $this->_site ) ) {
11592 return false;
11593 }
11594
11595 return $this->_site->is_trial();
11596 }
11597
11598 /**
11599 * Check if currently in a trial with payment method (credit card or paypal).
11600 *
11601 * @author Vova Feldman (@svovaf)
11602 * @since 1.1.7
11603 *
11604 * @return bool
11605 */
11606 function is_paid_trial() {
11607 $this->_logger->entrance();
11608
11609 if ( ! $this->is_trial() ) {
11610 return false;
11611 }
11612
11613 if ( ! $this->has_active_valid_license() ) {
11614 return false;
11615 }
11616
11617 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11618 return false;
11619 }
11620
11621 /**
11622 * @var FS_Subscription $subscription
11623 */
11624 $subscription = $this->_get_subscription( $this->_license->id );
11625
11626 return ( is_object( $subscription ) && $subscription->is_active() );
11627 }
11628
11629 /**
11630 * Check if trial already utilized.
11631 *
11632 * @since 1.0.9
11633 *
11634 * @return bool
11635 */
11636 function is_trial_utilized() {
11637 $this->_logger->entrance();
11638
11639 if ( ! $this->is_registered() ) {
11640 return false;
11641 }
11642
11643 return $this->_site->is_trial_utilized();
11644 }
11645
11646 /**
11647 * Get trial plan information (if in trial).
11648 *
11649 * @author Vova Feldman (@svovaf)
11650 * @since 1.0.9
11651 *
11652 * @return bool|FS_Plugin_Plan
11653 */
11654 function get_trial_plan() {
11655 $this->_logger->entrance();
11656
11657 if ( ! $this->is_trial() ) {
11658 return false;
11659 }
11660
11661 // Try to load plan from local cache.
11662 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11663
11664 if ( ! is_object( $trial_plan ) ) {
11665 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11666
11667 /**
11668 * If managed to fetch the plan, add it to the plans collection.
11669 */
11670 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11671 if ( ! is_array( $this->_plans ) ) {
11672 $this->_plans = array();
11673 }
11674
11675 $this->_plans[] = $trial_plan;
11676 $this->_store_plans();
11677 }
11678 }
11679
11680 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11681 return $trial_plan;
11682 }
11683
11684 /**
11685 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11686 */
11687 $trial_plan = new FS_Plugin_Plan();
11688 $trial_plan->id = $this->_site->trial_plan_id;
11689 $trial_plan->name = 'pro';
11690 $trial_plan->title = 'Pro';
11691
11692 return $trial_plan;
11693 }
11694
11695 /**
11696 * Check if the user has an activate, non-expired license on current plugin's install.
11697 *
11698 * @since 1.0.9
11699 *
11700 * @return bool
11701 */
11702 function is_paying() {
11703 $this->_logger->entrance();
11704
11705 if ( ! $this->is_registered( true ) ) {
11706 return false;
11707 }
11708
11709 if ( ! $this->has_paid_plan() ) {
11710 return false;
11711 }
11712
11713 return (
11714 ! $this->is_trial() &&
11715 'free' !== $this->get_plan_name() &&
11716 $this->has_active_valid_license()
11717 );
11718 }
11719
11720 /**
11721 * @author Vova Feldman (@svovaf)
11722 * @since 1.0.4
11723 *
11724 * @return bool
11725 */
11726 function is_free_plan() {
11727 if ( ! $this->is_registered() ) {
11728 return true;
11729 }
11730
11731 if ( ! $this->has_paid_plan() ) {
11732 return true;
11733 }
11734
11735 return (
11736 'free' === $this->get_plan_name() ||
11737 ! $this->has_features_enabled_license()
11738 );
11739 }
11740
11741 /**
11742 * @author Vova Feldman (@svovaf)
11743 * @since 1.0.5
11744 *
11745 * @return bool
11746 */
11747 function _has_premium_license() {
11748 $this->_logger->entrance();
11749
11750 $premium_license = $this->_get_available_premium_license();
11751
11752 return ( false !== $premium_license );
11753 }
11754
11755 /**
11756 * Check if user has any licenses associated with the plugin (including expired or blocking).
11757 *
11758 * @author Vova Feldman (@svovaf)
11759 * @since 1.1.7.3
11760 *
11761 * @param bool $including_foreign
11762 *
11763 * @return bool
11764 */
11765 function has_any_license( $including_foreign = true ) {
11766 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11767 return false;
11768 }
11769
11770 if ( $including_foreign ) {
11771 return true;
11772 }
11773
11774 foreach ( $this->_licenses as $license ) {
11775 if ( $this->_user->id == $license->user_id ) {
11776 return true;
11777 }
11778 }
11779
11780 return false;
11781 }
11782
11783 /**
11784 * @author Vova Feldman (@svovaf)
11785 * @since 1.0.5
11786 *
11787 * @param bool|null $is_localhost
11788 *
11789 * @return FS_Plugin_License|false
11790 */
11791 function _get_available_premium_license( $is_localhost = null ) {
11792 $this->_logger->entrance();
11793
11794 $licenses = $this->get_available_premium_licenses( $is_localhost );
11795 if ( ! empty( $licenses ) ) {
11796 return $licenses[0];
11797 }
11798
11799 return false;
11800 }
11801
11802 /**
11803 * @author Vova Feldman (@svovaf)
11804 * @since 1.0.5
11805 *
11806 * @param bool|null $is_localhost
11807 *
11808 * @return FS_Plugin_License[]
11809 */
11810 function get_available_premium_licenses( $is_localhost = null ) {
11811 $this->_logger->entrance();
11812
11813 $licenses = array();
11814 if ( ! $this->has_paid_plan() ) {
11815 return $licenses;
11816 }
11817
11818 if ( is_array( $this->_licenses ) ) {
11819 foreach ( $this->_licenses as $license ) {
11820 if ( ! $license->can_activate( $is_localhost ) ) {
11821 continue;
11822 }
11823
11824 $licenses[] = $license;
11825 }
11826 }
11827
11828 return $licenses;
11829 }
11830
11831 /**
11832 * Sync local plugin plans with remote server.
11833 *
11834 * IMPORTANT: If for some reason a site is associated with deleted plan, we'll preserve the plan's information and append it as the last plan. This means that if plan is deleted, the is_plan() method will ALWAYS return true for any given argument (it becomes the most inclusive plan).
11835 *
11836 * @author Vova Feldman (@svovaf)
11837 * @since 1.0.5
11838 *
11839 * @return FS_Plugin_Plan[]|object
11840 */
11841 function _sync_plans() {
11842 $plans = $this->_fetch_plugin_plans();
11843
11844 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11845 $plans_map = array();
11846 foreach ( $plans as $plan ) {
11847 $plans_map[ $plan->id ] = true;
11848 }
11849
11850 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11851
11852 foreach ( $plans_ids_to_keep as $plan_id ) {
11853 if ( isset( $plans_map[ $plan_id ] ) ) {
11854 continue;
11855 }
11856
11857 $missing_plan = self::_get_plan_by_id( $plan_id );
11858
11859 if ( is_object( $missing_plan ) ) {
11860 $plans[] = $missing_plan;
11861 }
11862 }
11863
11864 $this->_plans = $plans;
11865 $this->_store_plans();
11866 }
11867
11868 $this->do_action( 'after_plans_sync', $plans );
11869
11870 return $this->_plans;
11871 }
11872
11873 /**
11874 * Check if specified plan exists locally. If not, fetch it and store it.
11875 *
11876 * @author Vova Feldman (@svovaf)
11877 * @since 2.0.0
11878 *
11879 * @param number $plan_id
11880 *
11881 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11882 */
11883 private function sync_plan_if_not_exist( $plan_id ) {
11884 $plan = self::_get_plan_by_id( $plan_id );
11885
11886 if ( is_object( $plan ) ) {
11887 // Plan already exists.
11888 return $plan;
11889 }
11890
11891 $plan = $this->fetch_plan_by_id( $plan_id );
11892
11893 if ( $plan instanceof FS_Plugin_Plan ) {
11894 $this->_plans[] = $plan;
11895 $this->_store_plans();
11896
11897 return $plan;
11898 }
11899
11900 return $plan;
11901 }
11902
11903 /**
11904 * Check if specified license exists locally. If not, fetch it and store it.
11905 *
11906 * @author Vova Feldman (@svovaf)
11907 * @since 2.0.0
11908 *
11909 * @param number $license_id
11910 * @param string $license_key
11911 *
11912 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11913 */
11914 private function sync_license_if_not_exist( $license_id, $license_key ) {
11915 $license = $this->_get_license_by_id( $license_id );
11916
11917 if ( is_object( $license ) ) {
11918 // License already exists.
11919 return $license;
11920 }
11921
11922 $license = $this->fetch_license_by_key( $license_id, $license_key );
11923
11924 if ( $license instanceof FS_Plugin_License ) {
11925 $this->_licenses[] = $license;
11926
11927 $this->set_license( $license );
11928
11929 $this->_store_licenses();
11930
11931 return $license;
11932 }
11933
11934 return $license;
11935 }
11936
11937 /**
11938 * Get a collection of unique plan IDs that are associated with any installs in the network.
11939 *
11940 * @author Leo Fajardo (@leorw)
11941 * @since 2.0.0
11942 *
11943 * @return number[]
11944 */
11945 private function get_plans_ids_associated_with_installs() {
11946 if ( ! is_multisite() ) {
11947 if ( ! is_object( $this->_site ) ||
11948 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11949 ) {
11950 return array();
11951 }
11952
11953 return array( $this->_site->plan_id );
11954 }
11955
11956 $plan_ids = array();
11957 $sites = self::get_sites();
11958 foreach ( $sites as $site ) {
11959 $blog_id = self::get_site_blog_id( $site );
11960 $install = $this->get_install_by_blog_id( $blog_id );
11961
11962 if ( ! is_object( $install ) ||
11963 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11964 ) {
11965 continue;
11966 }
11967
11968 $plan_ids[ $install->plan_id ] = true;
11969 }
11970
11971 return array_keys( $plan_ids );
11972 }
11973
11974 /**
11975 * Get a collection of unique license IDs that are associated with any installs in the network.
11976 *
11977 * @author Leo Fajardo (@leorw)
11978 * @since 2.0.0
11979 *
11980 * @return number[]
11981 */
11982 private function get_license_ids_associated_with_installs() {
11983 if ( ! $this->_is_network_active ) {
11984 if ( ! is_object( $this->_site ) ||
11985 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11986 ) {
11987 return array();
11988 }
11989
11990 return array( $this->_site->license_id );
11991 }
11992
11993 $license_ids = array();
11994 $sites = self::get_sites();
11995 foreach ( $sites as $site ) {
11996 $blog_id = self::get_site_blog_id( $site );
11997 $install = $this->get_install_by_blog_id( $blog_id );
11998
11999 if ( ! is_object( $install ) ||
12000 ! FS_Plugin_License::is_valid_id( $install->license_id )
12001 ) {
12002 continue;
12003 }
12004
12005 $license_ids[ $install->license_id ] = true;
12006 }
12007
12008 return array_keys( $license_ids );
12009 }
12010
12011 /**
12012 * @author Vova Feldman (@svovaf)
12013 * @since 1.0.5
12014 *
12015 * @param number $id
12016 *
12017 * @return FS_Plugin_Plan|false
12018 */
12019 function _get_plan_by_id( $id ) {
12020 $this->_logger->entrance();
12021
12022 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
12023 $this->_sync_plans();
12024 }
12025
12026 foreach ( $this->_plans as $plan ) {
12027 if ( $id == $plan->id ) {
12028 return $plan;
12029 }
12030 }
12031
12032 return false;
12033 }
12034
12035 /**
12036 * @author Vova Feldman (@svovaf)
12037 * @since 1.1.8.1
12038 *
12039 * @param string $name
12040 *
12041 * @return FS_Plugin_Plan|false
12042 */
12043 private function get_plan_by_name( $name ) {
12044 $this->_logger->entrance();
12045
12046 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
12047 $this->_sync_plans();
12048 }
12049
12050 foreach ( $this->_plans as $plan ) {
12051 if ( $name == $plan->name ) {
12052 return $plan;
12053 }
12054 }
12055
12056 return false;
12057 }
12058
12059 /**
12060 * Sync local licenses with remote server.
12061 *
12062 * @author Vova Feldman (@svovaf)
12063 * @since 1.0.6
12064 *
12065 * @param number|bool $site_license_id
12066 * @param number|null $blog_id
12067 *
12068 * @return FS_Plugin_License[]|object
12069 */
12070 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
12071 $this->_logger->entrance();
12072
12073 $is_network_admin = fs_is_network_admin();
12074
12075 if ( $is_network_admin && is_null( $blog_id ) ) {
12076 $all_licenses = self::get_all_licenses( $this->_module_id );
12077 } else {
12078 $all_licenses = $this->get_user_licenses( $this->_user->id );
12079 }
12080
12081 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
12082
12083 $all_licenses_map = array();
12084 foreach ( $all_licenses as $license ) {
12085 $all_licenses_map[ $license->id ] = true;
12086 }
12087
12088 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
12089
12090 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
12091 $licenses_map = array();
12092 foreach ( $licenses as $license ) {
12093 $licenses_map[ $license->id ] = true;
12094 }
12095
12096 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
12097 // foreach ( $license_ids_to_keep as $license_id ) {
12098 // if ( isset( $licenses_map[ $license_id ] ) ) {
12099 // continue;
12100 // }
12101 //
12102 // $missing_license = self::_get_license_by_id( $license_id, false );
12103 // if ( is_object( $missing_license ) ) {
12104 // $licenses[] = $missing_license;
12105 // $licenses_map[ $missing_license->id ] = true;
12106 // }
12107 // }
12108
12109 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
12110
12111 foreach ( $user_license_ids as $key => $license_id ) {
12112 if ( ! isset( $licenses_map[ $license_id ] ) ) {
12113 // Remove access to licenses that no longer exist.
12114 unset( $user_license_ids[ $key ] );
12115 }
12116 }
12117
12118 if ( ! empty( $user_license_ids ) ) {
12119 foreach ( $licenses_map as $license_id => $value ) {
12120 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
12121 // Associate new licenses with the user who triggered the license syncing.
12122 $user_license_ids[] = $license_id;
12123 }
12124 }
12125
12126 $user_license_ids = array_unique( $user_license_ids );
12127 } else {
12128 $user_license_ids = array_keys( $licenses_map );
12129 }
12130
12131 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
12132 $user_licenses = array();
12133 foreach ( $licenses as $license ) {
12134 if ( ! in_array( $license->id, $user_license_ids ) ) {
12135 continue;
12136 }
12137
12138 $user_licenses[] = $license;
12139 }
12140
12141 $this->_licenses = $user_licenses;
12142 } else {
12143 $this->_licenses = $licenses;
12144 }
12145
12146 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
12147
12148 $this->_store_licenses( true, $this->_module_id, $licenses );
12149 }
12150
12151 // Update current license.
12152 if ( is_object( $this->_license ) ) {
12153 $license = $this->_get_license_by_id( $this->_license->id );
12154
12155 if ( is_object( $license ) ) {
12156 /**
12157 * `$license` can be `false` in case a user change action has just been completed and this method
12158 * has synced the `$this->_licenses` collection for the new user. In this case, the
12159 * `$this->_licenses` collection may have only the newly activated license that is associated with
12160 * the new user. `set_license` will eventually be called in the same request by the logic that
12161 * follows outside this method which will detect that the install's license has been updated, and
12162 * then `_update_site_license` will be called which in turn will call `set_license`.
12163 *
12164 * @author Leo Fajardo (@leorw)
12165 * @since 2.3.2
12166 */
12167 $this->set_license( $license );
12168 }
12169 }
12170
12171 return $this->_licenses;
12172 }
12173
12174 /**
12175 * @author Vova Feldman (@svovaf)
12176 * @since 1.0.5
12177 *
12178 * @param number $id
12179 * @param bool $sync_licenses
12180 *
12181 * @return FS_Plugin_License|false
12182 */
12183 function _get_license_by_id( $id, $sync_licenses = true ) {
12184 $this->_logger->entrance();
12185
12186 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
12187 return false;
12188 }
12189
12190 /**
12191 * When running from the network level admin and opted-in from the network,
12192 * check if the license exists in the network user licenses collection.
12193 *
12194 * @author Vova Feldman (@svovaf)
12195 * @since 2.0.0
12196 */
12197 if ( fs_is_network_admin() &&
12198 $this->is_network_registered() &&
12199 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
12200 ) {
12201 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
12202
12203 foreach ( $licenses as $license ) {
12204 if ( $id == $license->id ) {
12205 return $license;
12206 }
12207 }
12208 }
12209
12210 if ( ! $this->has_any_license() && $sync_licenses ) {
12211 $this->_sync_licenses( $id );
12212 }
12213
12214 if ( is_array( $this->_licenses ) ) {
12215 foreach ( $this->_licenses as $license ) {
12216 if ( $id == $license->id ) {
12217 return $license;
12218 }
12219 }
12220 }
12221
12222 return false;
12223 }
12224
12225 /**
12226 * Get license by ID. Unlike _get_license_by_id(), this method only checks the local storage and return any license, whether it's associated with the current context user/install or not.
12227 *
12228 * @author Vova Feldman (@svovaf)
12229 * @since 2.0.0
12230 *
12231 * @param number $id
12232 *
12233 * @return FS_Plugin_License
12234 */
12235 private function get_license_by_id( $id ) {
12236 $licenses = self::get_all_licenses( $this->_module_id );
12237
12238 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
12239 foreach ( $licenses as $license ) {
12240 if ( $id == $license->id ) {
12241 return $license;
12242 }
12243 }
12244 }
12245
12246 return null;
12247 }
12248
12249 /**
12250 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
12251 *
12252 * @author Vova Feldman (@svovaf)
12253 * @since 2.0.0
12254 *
12255 * @return \FS_Plugin_License|mixed
12256 */
12257 private function sync_site_license() {
12258 $api = $this->get_api_user_scope();
12259
12260 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
12261
12262 if ( ! $this->is_api_result_entity( $result ) ) {
12263 return $result;
12264 }
12265
12266 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
12267 $this->_store_licenses();
12268
12269 return $license;
12270 }
12271
12272 /**
12273 * Get all user's available licenses for the current module.
12274 *
12275 * @author Vova Feldman (@svovaf)
12276 * @since 2.0.0
12277 *
12278 * @param number $user_id
12279 *
12280 * @return FS_Plugin_License[]
12281 */
12282 private function get_user_licenses( $user_id ) {
12283 $all_licenses = self::get_all_licenses( $this->_module_id );
12284 if ( empty( $all_licenses ) ) {
12285 return array();
12286 }
12287
12288 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
12289 if ( empty( $user_license_ids ) ) {
12290 return array();
12291 }
12292
12293 $licenses = array();
12294 foreach ( $all_licenses as $license ) {
12295 if ( in_array( $license->id, $user_license_ids ) ) {
12296 $licenses[] = $license;
12297 }
12298 }
12299
12300 return $licenses;
12301 }
12302
12303 /**
12304 * Checks if the context license is network activated except on the given blog ID.
12305 *
12306 * @author Vova Feldman (@svovaf)
12307 * @since 2.0.0
12308 *
12309 * @param int $except_blog_id
12310 *
12311 * @return bool
12312 */
12313 private function is_license_network_active( $except_blog_id = 0 ) {
12314 $this->_logger->entrance();
12315
12316 if ( ! is_object( $this->_license ) ) {
12317 return false;
12318 }
12319
12320 $sites = self::get_sites();
12321
12322 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
12323 // There are more sites than the number of activations, so license cannot be network activated.
12324 return false;
12325 }
12326
12327 foreach ( $sites as $site ) {
12328 $blog_id = self::get_site_blog_id( $site );
12329
12330 if ( $except_blog_id == $blog_id ) {
12331 // Skip excluded blog.
12332 continue;
12333 }
12334
12335 $install = $this->get_install_by_blog_id( $blog_id );
12336
12337 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
12338 return false;
12339 }
12340 }
12341
12342 return true;
12343 }
12344
12345 /**
12346 * Checks if license can be activated on all the network sites (opted-in or skipped) that are not yet associated with a license. If possible, try to make the activation, if not return false.
12347 *
12348 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
12349 *
12350 * @author Vova Feldman (@svovaf)
12351 * @since 2.0.0
12352 *
12353 * @param \FS_User $user
12354 * @param \FS_Plugin_License $license
12355 *
12356 * @return bool
12357 */
12358 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
12359 $this->_logger->entrance();
12360
12361 $result = $this->can_activate_license_on_network( $license );
12362
12363 if ( false === $result ) {
12364 return false;
12365 }
12366
12367 $installs_without_license = $result['installs'];
12368 if ( ! empty( $installs_without_license ) ) {
12369 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
12370 }
12371
12372 $disconnected_site_ids = $result['sites'];
12373 if ( ! empty( $disconnected_site_ids ) ) {
12374 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
12375 }
12376
12377 $this->link_license_2_user( $license->id, $user->id );
12378
12379 // Sync license after activations.
12380 $license->activated += $result['production_count'];
12381 $license->activated_local += $result['localhost_count'];
12382
12383 // $this->_store_licenses()
12384
12385 return true;
12386 }
12387
12388 /**
12389 * Checks if the given license can be activated on the whole network.
12390 *
12391 * @author Vova Feldman (@svovaf)
12392 * @since 2.0.0
12393 *
12394 * @param \FS_Plugin_License $license
12395 *
12396 * @return false|array {
12397 * @type array[int]FS_Site $installs Blog ID to install map.
12398 * @type int[] $sites Non-connected blog IDs.
12399 * @type int $production_count Production sites count.
12400 * @type int $localhost_count Production sites count.
12401 * }
12402 */
12403 private function can_activate_license_on_network( FS_Plugin_License $license ) {
12404 $sites = self::get_sites();
12405
12406 $production_count = 0;
12407 $localhost_count = 0;
12408
12409 $installs_without_license = array();
12410 $disconnected_site_ids = array();
12411
12412 foreach ( $sites as $site ) {
12413 $blog_id = self::get_site_blog_id( $site );
12414 $install = $this->get_install_by_blog_id( $blog_id );
12415
12416 if ( is_object( $install ) ) {
12417 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
12418 // License already activated on the install.
12419 continue;
12420 }
12421
12422 $url = $install->url;
12423
12424 $installs_without_license[ $blog_id ] = $install;
12425 } else {
12426 $url = is_object( $site ) ?
12427 $site->siteurl :
12428 self::get_unfiltered_site_url( $blog_id );
12429
12430 $disconnected_site_ids[] = $blog_id;
12431 }
12432
12433 if ( FS_Site::is_localhost_by_address( $url ) ) {
12434 $localhost_count ++;
12435 } else {
12436 $production_count ++;
12437 }
12438 }
12439
12440 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12441 return false;
12442 }
12443
12444 return array(
12445 'installs' => $installs_without_license,
12446 'sites' => $disconnected_site_ids,
12447 'production_count' => $production_count,
12448 'localhost_count' => $localhost_count,
12449 );
12450 }
12451
12452 /**
12453 * Activate a given license on a collection of installs.
12454 *
12455 * @author Vova Feldman (@svovaf)
12456 * @since 2.0.0
12457 *
12458 * @param \FS_User $user
12459 * @param string $license_key
12460 * @param array $blog_2_install_map {
12461 * @key int Blog ID.
12462 * @value FS_Site Blog's associated install.
12463 * }
12464 *
12465 * @return mixed|true
12466 */
12467 private function activate_license_on_many_installs(
12468 FS_User $user,
12469 $license_key,
12470 array $blog_2_install_map
12471 ) {
12472 $params = array(
12473 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12474 );
12475
12476 $install_2_blog_map = array();
12477 foreach ( $blog_2_install_map as $blog_id => $install ) {
12478 $params[] = array( 'id' => $install->id, 'url' => $install->url );
12479
12480 $install_2_blog_map[ $install->id ] = $blog_id;
12481 }
12482
12483 $result = $this->get_api_user_scope_by_user( $user )->call(
12484 "plugins/{$this->_plugin->id}/installs.json",
12485 'PUT',
12486 $params
12487 );
12488
12489 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12490 return $result;
12491 }
12492
12493 foreach ( $result->installs as $r_install ) {
12494 $install = new FS_Site( $r_install );
12495 $install->is_disconnected = false;
12496
12497 // Update install.
12498 $this->_store_site(
12499 true,
12500 $install_2_blog_map[ $r_install->id ],
12501 $install
12502 );
12503 }
12504
12505 return true;
12506 }
12507
12508 /**
12509 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12510 *
12511 * @author Vova Feldman (@svovaf)
12512 * @since 2.3.1
12513 *
12514 * @param \FS_User $user
12515 * @param string $license_key
12516 *
12517 * @return true|mixed True if successful, otherwise, the API result.
12518 */
12519 private function activate_license_on_site( FS_User $user, $license_key ) {
12520 return $this->activate_license_on_many_sites( $user, $license_key );
12521 }
12522
12523 /**
12524 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12525 *
12526 * @author Vova Feldman (@svovaf)
12527 * @since 2.0.0
12528 *
12529 * @param \FS_User $user
12530 * @param string $license_key
12531 * @param int[] $site_ids
12532 *
12533 * @return true|mixed True if successful, otherwise, the API result.
12534 */
12535 private function activate_license_on_many_sites(
12536 FS_User $user,
12537 $license_key,
12538 array $site_ids = array()
12539 ) {
12540 $sites = array();
12541 foreach ( $site_ids as $site_id ) {
12542 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12543 }
12544
12545 // Install the plugin.
12546 $result = $this->create_installs_with_user(
12547 $user,
12548 $license_key,
12549 false,
12550 $sites,
12551 false,
12552 true
12553 );
12554
12555 if ( ! $this->is_api_result_entity( $result ) &&
12556 ! $this->is_api_result_object( $result, 'installs' )
12557 ) {
12558 return $result;
12559 }
12560
12561 $installs = array();
12562
12563 if ( $this->is_api_result_entity( $result ) ) {
12564 $install = new FS_Site( $result );
12565
12566 $this->_user = $user;
12567
12568 $this->_store_site( true, null, $install );
12569
12570 $this->_site = $install;
12571
12572 $this->reset_anonymous_mode();
12573 } else {
12574 foreach ( $result->installs as $install ) {
12575 $installs[] = new FS_Site( $install );
12576 }
12577
12578 // Map site addresses to their blog IDs.
12579 $address_to_blog_map = $this->get_address_to_blog_map();
12580
12581 $first_blog_id = null;
12582
12583 foreach ( $installs as $install ) {
12584 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12585 $blog_id = $address_to_blog_map[ $address ];
12586
12587 $this->_store_site( true, $blog_id, $install );
12588
12589 $this->reset_anonymous_mode( $blog_id );
12590
12591 if ( is_null( $first_blog_id ) ) {
12592 $first_blog_id = $blog_id;
12593 }
12594 }
12595
12596 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12597 $this->_storage->network_install_blog_id = $first_blog_id;
12598 }
12599 }
12600
12601 return true;
12602 }
12603
12604 /**
12605 * Sync site's license with user licenses.
12606 *
12607 * @author Vova Feldman (@svovaf)
12608 * @since 1.0.6
12609 *
12610 * @param FS_Plugin_License|null $new_license
12611 *
12612 * @return FS_Plugin_License|null
12613 */
12614 function _update_site_license( $new_license ) {
12615 $this->_logger->entrance();
12616
12617 /**
12618 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12619 * accordingly so that it will also handle the case when an ownership change is done via license
12620 * activation.
12621 *
12622 * @author Leo Fajardo (@leorw)
12623 * @since 2.3.2
12624 */
12625 $this->set_license( $new_license );
12626
12627 if ( ! is_object( $new_license ) ) {
12628 $this->_site->license_id = null;
12629 $this->_sync_site_subscription( null );
12630
12631 return $this->_license;
12632 }
12633
12634 $this->_site->license_id = $this->_license->id;
12635
12636 if ( ! is_array( $this->_licenses ) ) {
12637 $this->_licenses = array();
12638 }
12639
12640 $is_license_found = false;
12641 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12642 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12643 $this->_licenses[ $i ] = $new_license;
12644
12645 $is_license_found = true;
12646 break;
12647 }
12648 }
12649
12650 // If new license just append.
12651 if ( ! $is_license_found ) {
12652 $this->_licenses[] = $new_license;
12653 }
12654
12655 $this->_sync_site_subscription( $new_license );
12656
12657 return $this->_license;
12658 }
12659
12660 /**
12661 * @author Vova Feldman (@svovaf)
12662 * @since 2.3.1
12663 *
12664 * @param \FS_Plugin_License $license
12665 */
12666 private function set_license( FS_Plugin_License $license = null ) {
12667 $this->_license = $license;
12668
12669 $this->maybe_update_whitelabel_flag( $license );
12670 }
12671
12672 /**
12673 * @author Leo Fajardo (@leorw)
12674 * @since 2.3.1
12675 *
12676 * @param FS_Plugin_License $license
12677 */
12678 private function maybe_update_whitelabel_flag( $license ) {
12679 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12680 $this->_storage->is_whitelabeled :
12681 false;
12682
12683 if ( is_object( $license ) ) {
12684 $license_user = self::_get_user_by_id( $license->user_id );
12685
12686 if ( ! is_object( $license_user ) ) {
12687 // If foreign license, do not update the `is_whitelabeled` flag.
12688 return;
12689 }
12690
12691 if ( $this->is_addon() ) {
12692 /**
12693 * Store the last license data to the parent's storage since it's needed only when showing the
12694 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12695 * iterate over the add-ons just to get the last license data.
12696 */
12697 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12698 } else {
12699 $this->store_last_activated_license_data( $license );
12700 }
12701
12702 if ( $license->is_whitelabeled ) {
12703 // Activated a developer license, data should be hidden.
12704 $is_whitelabeled = true;
12705 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12706 // The account owner activated a regular license key, no need to hide the data.
12707 $is_whitelabeled = false;
12708 }
12709 }
12710
12711 $this->_storage->is_whitelabeled = $is_whitelabeled;
12712
12713 // Reset the whitelabeled status after update.
12714 $this->is_whitelabeled = null;
12715 if ( $this->is_addon() ) {
12716 $parent_fs = $this->get_parent_instance();
12717
12718 if ( is_object( $parent_fs ) ) {
12719 $parent_fs->is_whitelabeled = null;
12720 }
12721 }
12722 }
12723
12724 /**
12725 * @author Leo Fajardo (@leorw)
12726 * @since 2.3.1
12727 *
12728 * @param FS_Plugin_License $license
12729 * @param FS_User $license_user
12730 */
12731 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12732 if ( ! is_object( $license_user ) ) {
12733 $this->_storage->last_license_key = md5( $license->secret_key );
12734 $this->_storage->last_license_user_id = null;
12735 } else {
12736 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12737 $this->_storage->last_license_user_id = $license_user->id;
12738 }
12739 }
12740
12741 /**
12742 * @author Leo Fajardo (@leorw)
12743 * @since 2.3.1
12744 *
12745 * @param bool $ignore_data_debug_mode
12746 *
12747 * @return bool
12748 */
12749 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12750 if ( true !== $this->_storage->is_whitelabeled ) {
12751 return false;
12752 } else if ( $ignore_data_debug_mode ) {
12753 return true;
12754 }
12755
12756 $fs = $this->is_addon() ?
12757 $this->get_parent_instance() :
12758 $this;
12759
12760 return ! $fs->is_data_debug_mode();
12761 }
12762
12763 /**
12764 * @author Leo Fajardo (@leorw)
12765 * @since 2.3.1
12766 *
12767 * @return number
12768 */
12769 function get_last_license_user_id() {
12770 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12771 $this->_storage->last_license_user_id :
12772 null;
12773 }
12774
12775 /**
12776 * @author Leo Fajardo (@leorw)
12777 * @since 2.3.1
12778 *
12779 * @param int $blog_id
12780 * @param bool $ignore_data_debug_mode
12781 *
12782 * @return bool
12783 */
12784 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12785 if ( ! is_null( $blog_id ) ) {
12786 $this->switch_to_blog( $blog_id );
12787 }
12788
12789 if ( ! is_null( $this->is_whitelabeled ) ) {
12790 $is_whitelabeled = $this->is_whitelabeled;
12791 } else {
12792 $is_whitelabeled = false;
12793
12794 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12795
12796 if ( ! $this->has_addons() ) {
12797 $is_whitelabeled = $is_whitelabeled_flag;
12798 } else if ( $is_whitelabeled_flag ) {
12799 $is_whitelabeled = true;
12800 } else {
12801 if ( $this->is_registered() || $this->is_premium() ) {
12802 $addon_ids = $this->get_updated_account_addons();
12803 } else {
12804 $addons = self::get_all_addons();
12805
12806 $plugin_addons = isset( $addons[ $this->_plugin->id ] ) ?
12807 $addons[ $this->_plugin->id ] :
12808 array();
12809
12810 $addon_ids = array();
12811 foreach ( $plugin_addons as $addon ) {
12812 $addon_ids[] = $addon->id;
12813 }
12814 }
12815
12816 $installed_addons = $this->get_installed_addons();
12817 foreach ( $installed_addons as $fs_addon ) {
12818 $addon_ids[] = $fs_addon->get_id();
12819 }
12820
12821 if ( ! empty( $addon_ids ) ) {
12822 $addon_ids = array_unique( $addon_ids );
12823
12824 $is_network_level = (
12825 fs_is_network_admin() &&
12826 $this->is_network_active()
12827 );
12828
12829 foreach ( $addon_ids as $addon_id ) {
12830 $addon = $this->get_addon( $addon_id );
12831
12832 if ( ! is_object( $addon ) ) {
12833 continue;
12834 }
12835
12836 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12837 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12838 self::get_addon_instance( $addon_id ) :
12839 null;
12840
12841 $was_addon_network_activated = false;
12842
12843 if ( is_object( $fs_addon ) ) {
12844 $was_addon_network_activated = $fs_addon->is_network_active();
12845 } else if ( $is_network_level ) {
12846 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12847 }
12848
12849 $network_delegated_connection = (
12850 $was_addon_network_activated &&
12851 $addon_storage->get( 'is_delegated_connection', false, true )
12852 );
12853
12854 if (
12855 $is_network_level &&
12856 ( ! $was_addon_network_activated || $network_delegated_connection )
12857 ) {
12858 $sites = self::get_sites();
12859
12860 /**
12861 * If in network admin area and the add-on was not network-activated or network-activated
12862 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12863 */
12864 foreach ( $sites as $site ) {
12865 $site_info = $this->get_site_info( $site );
12866
12867 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12868 $is_whitelabeled = true;
12869 break;
12870 }
12871 }
12872
12873 if ( $is_whitelabeled ) {
12874 break;
12875 }
12876 } else {
12877 /**
12878 * This will be executed when any of the following is met:
12879 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12880 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12881 * 3. Add-on was not network-activated and in site admin area.
12882 */
12883 if ( true === $addon_storage->is_whitelabeled ) {
12884 $is_whitelabeled = true;
12885 break;
12886 }
12887 }
12888 }
12889 }
12890 }
12891
12892 $this->is_whitelabeled = $is_whitelabeled;
12893
12894 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12895 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12896 }
12897
12898 if ( ! is_null( $blog_id ) ) {
12899 $this->restore_current_blog();
12900 }
12901 }
12902
12903 return (
12904 $is_whitelabeled &&
12905 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12906 );
12907 }
12908
12909 /**
12910 * Sync site's subscription.
12911 *
12912 * @author Vova Feldman (@svovaf)
12913 * @since 1.0.9
12914 *
12915 * @param FS_Plugin_License|null $license
12916 *
12917 * @return bool|\FS_Subscription
12918 */
12919 private function _sync_site_subscription( $license ) {
12920 if ( ! is_object( $license ) ) {
12921 $this->delete_unused_subscriptions();
12922
12923 return false;
12924 }
12925
12926 // Load subscription details if not lifetime.
12927 $subscription = $license->is_lifetime() ?
12928 false :
12929 $this->_fetch_site_license_subscription();
12930
12931 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12932 $this->store_subscription( $subscription );
12933 } else {
12934 $this->delete_unused_subscriptions();
12935 }
12936
12937 return $subscription;
12938 }
12939
12940 /**
12941 * @author Vova Feldman (@svovaf)
12942 * @since 1.0.6
12943 *
12944 * @return bool|\FS_Plugin_License
12945 */
12946 function _get_license() {
12947 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12948 return $this->_license;
12949 }
12950
12951 return $this->_get_available_premium_license();
12952 }
12953
12954 /**
12955 * @param number $license_id
12956 *
12957 * @return null|\FS_Subscription
12958 */
12959 function _get_subscription( $license_id ) {
12960 if ( ! isset( $this->_storage->subscriptions ) ||
12961 empty( $this->_storage->subscriptions )
12962 ) {
12963 return null;
12964 }
12965
12966 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12967 if ( $subscription->license_id == $license_id ) {
12968 return $subscription;
12969 }
12970 }
12971
12972 return null;
12973 }
12974
12975 /**
12976 * @author Leo Fajardo (@leorw)
12977 * @since 2.0.0
12978 *
12979 * @param FS_Subscription $subscription
12980 */
12981 function store_subscription( FS_Subscription $subscription ) {
12982 if ( ! isset( $this->_storage->subscriptions ) ) {
12983 $this->_storage->subscriptions = array();
12984 }
12985
12986 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12987 $this->_storage->subscriptions = array( $subscription );
12988
12989 return;
12990 }
12991
12992 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
12993
12994 $updated_subscription = false;
12995 foreach ( $subscriptions as $key => $existing_subscription ) {
12996 if ( $existing_subscription->id == $subscription->id ) {
12997 $subscriptions[ $key ] = $subscription;
12998 $updated_subscription = true;
12999 break;
13000 }
13001 }
13002
13003 if ( ! $updated_subscription ) {
13004 $subscriptions[] = $subscription;
13005 }
13006
13007 $this->_storage->subscriptions = $subscriptions;
13008 }
13009
13010 /**
13011 * @author Leo Fajardo (@leorw)
13012 * @since 2.0.0
13013 */
13014 function delete_unused_subscriptions() {
13015 if ( ! isset( $this->_storage->subscriptions ) ||
13016 empty( $this->_storage->subscriptions ) ||
13017 // Clean up only if there are already at least 3 subscriptions.
13018 ( count( $this->_storage->subscriptions ) < 3 )
13019 ) {
13020 return;
13021 }
13022
13023 if ( ! is_multisite() ) {
13024 // If not multisite, there should only be 1 subscription, so just clear the array.
13025 $this->_storage->subscriptions = array();
13026
13027 return;
13028 }
13029
13030 $subscriptions_to_keep_by_license_id_map = array();
13031 $sites = self::get_sites();
13032 foreach ( $sites as $site ) {
13033 $blog_id = self::get_site_blog_id( $site );
13034 $install = $this->get_install_by_blog_id( $blog_id );
13035
13036 if ( ! is_object( $install ) ||
13037 ! FS_Plugin_License::is_valid_id( $install->license_id )
13038 ) {
13039 continue;
13040 }
13041
13042 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
13043 }
13044
13045 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
13046 $this->_storage->subscriptions = array();
13047
13048 return;
13049 }
13050
13051 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
13052 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
13053 unset( $this->_storage->subscriptions[ $key ] );
13054 }
13055 }
13056 }
13057
13058 /**
13059 * @author Vova Feldman (@svovaf)
13060 * @since 1.0.2
13061 *
13062 * @param string $plan Plan name
13063 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
13064 *
13065 * @return bool
13066 */
13067 function is_plan( $plan, $exact = false ) {
13068 $this->_logger->entrance();
13069
13070 if ( ! $this->is_registered() ) {
13071 return false;
13072 }
13073
13074 $plan = strtolower( $plan );
13075
13076 $current_plan_name = $this->get_plan_name();
13077
13078 if ( $current_plan_name === $plan ) {
13079 // Exact plan.
13080 return true;
13081 } else if ( $exact ) {
13082 // Required exact, but plans are different.
13083 return false;
13084 }
13085
13086 $current_plan_order = - 1;
13087 $required_plan_order = PHP_INT_MAX;
13088 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
13089 if ( $plan === $this->_plans[ $i ]->name ) {
13090 $required_plan_order = $i;
13091 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
13092 $current_plan_order = $i;
13093 }
13094 }
13095
13096 return ( $current_plan_order > $required_plan_order );
13097 }
13098
13099 /**
13100 * Check if module has only one plan.
13101 *
13102 * @author Vova Feldman (@svovaf)
13103 * @since 1.2.1.7
13104 *
13105 * @param bool $double_check In some cases developers prefer to release their paid offering as premium-only, even though there is a free version. For those cases, looking at the 'is_premium_only' value isn't enough because the result will return false even when the product has only signle paid plan.
13106 *
13107 * @return bool
13108 */
13109 function is_single_plan( $double_check = false ) {
13110 $this->_logger->entrance();
13111
13112 if ( ! $this->is_registered() ||
13113 ! is_array( $this->_plans ) ||
13114 0 === count( $this->_plans )
13115 ) {
13116 return true;
13117 }
13118
13119 $has_free_plan = $this->has_free_plan();
13120
13121 if ( ! $has_free_plan && $double_check ) {
13122 foreach ( $this->_plans as $plan ) {
13123 if ( $plan->is_free() ) {
13124 $has_free_plan = true;
13125 break;
13126 }
13127 }
13128 }
13129
13130 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
13131 }
13132
13133 /**
13134 * Check if plan based on trial. If not in trial mode, should return false.
13135 *
13136 * @since 1.0.9
13137 *
13138 * @param string $plan Plan name
13139 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
13140 *
13141 * @return bool
13142 */
13143 function is_trial_plan( $plan, $exact = false ) {
13144 $this->_logger->entrance();
13145
13146 if ( ! $this->is_registered() ) {
13147 return false;
13148 }
13149
13150 if ( ! $this->is_trial() ) {
13151 return false;
13152 }
13153
13154 $trial_plan = $this->get_trial_plan();
13155
13156 if ( $trial_plan->name === $plan ) {
13157 // Exact plan.
13158 return true;
13159 } else if ( $exact ) {
13160 // Required exact, but plans are different.
13161 return false;
13162 }
13163
13164 $current_plan_order = - 1;
13165 $required_plan_order = - 1;
13166 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
13167 if ( $plan === $this->_plans[ $i ]->name ) {
13168 $required_plan_order = $i;
13169 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
13170 $current_plan_order = $i;
13171 }
13172 }
13173
13174 return ( $current_plan_order > $required_plan_order );
13175 }
13176
13177 /**
13178 * Check if plugin has any paid plans.
13179 *
13180 * @author Vova Feldman (@svovaf)
13181 * @since 1.0.7
13182 *
13183 * @return bool
13184 */
13185 function has_paid_plan() {
13186 return $this->_has_paid_plans ||
13187 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
13188 }
13189
13190 /**
13191 * Check if plugin has any plan with a trail.
13192 *
13193 * @author Vova Feldman (@svovaf)
13194 * @since 1.0.9
13195 *
13196 * @return bool
13197 */
13198 function has_trial_plan() {
13199 /**
13200 * @author Vova Feldman(@svovaf)
13201 * @since 1.2.1.5
13202 *
13203 * Allow setting a trial from the SDK without calling the API.
13204 * But, if the user did opt-in, continue using the real data from the API.
13205 */
13206 if ( $this->_trial_days >= 0 ) {
13207 return true;
13208 }
13209
13210 return $this->_storage->get( 'has_trial_plan', false );
13211 }
13212
13213 /**
13214 * Check if plugin has any free plan, or is it premium only.
13215 *
13216 * Note: If no plans configured, assume plugin is free.
13217 *
13218 * @author Vova Feldman (@svovaf)
13219 * @since 1.0.7
13220 *
13221 * @return bool
13222 */
13223 function has_free_plan() {
13224 return ! $this->is_only_premium();
13225 }
13226
13227 /**
13228 * Displays a license activation dialog box when the user clicks on the "Activate License"
13229 * or "Change License" link on the plugins
13230 * page.
13231 *
13232 * @author Leo Fajardo (@leorw)
13233 * @since 1.1.9
13234 */
13235 function _add_license_activation_dialog_box() {
13236 $vars = array(
13237 'id' => $this->_module_id,
13238 );
13239
13240 fs_require_template( 'forms/license-activation.php', $vars );
13241 fs_require_template( 'forms/resend-key.php', $vars );
13242 }
13243
13244 /**
13245 * Displays an email address update dialog box when the user clicks on the email address "Edit" button on the "Account" page.
13246 *
13247 * @author Leo Fajardo (@leorw)
13248 * @since 2.5.0
13249 */
13250 function _add_email_address_update_dialog_box() {
13251 $vars = array( 'id' => $this->_module_id );
13252
13253 fs_require_template( 'forms/email-address-update.php', $vars );
13254 }
13255
13256 /**
13257 * @author Leo Fajardo (@leorw)
13258 * @since 2.5.0
13259 */
13260 function _add_email_address_update_option() {
13261 if ( ! $this->should_handle_user_change() ) {
13262 return;
13263 }
13264
13265 // Add email address update AJAX handler.
13266 $this->add_ajax_action( 'update_email_address', array( &$this, '_email_address_update_ajax_handler' ) );
13267 }
13268
13269 /**
13270 * @author Leo Fajardo (@leorw)
13271 * @since 2.5.0
13272 */
13273 function _email_address_update_ajax_handler() {
13274 $this->check_ajax_referer( 'update_email_address' );
13275
13276 $new_email_address = fs_request_get( 'email_address' );
13277 $transfer_type = fs_request_get( 'transfer_type' );
13278
13279 $result = $this->update_email( $new_email_address );
13280
13281 if ( ! FS_Api::is_api_error( $result ) ) {
13282 self::shoot_ajax_success();
13283 }
13284
13285 $error = '';
13286
13287 if ( FS_Api::is_api_error_object( $result ) ) {
13288 switch ( $result->error->code ) {
13289 case 'user_exist':
13290 case 'account_verification_required':
13291 $error = array(
13292 'code' => 'change_ownership',
13293 'url' => $this->get_account_url( 'change_owner', array(
13294 'state' => 'init',
13295 'candidate_email' => $new_email_address,
13296 'transfer_type' => $transfer_type,
13297 ) ),
13298 );
13299
13300 break;
13301 }
13302 }
13303
13304 if ( empty( $error ) ) {
13305 $error = is_object( $result ) ?
13306 var_export( $result->error, true ) :
13307 $result;
13308 }
13309
13310 self::shoot_ajax_failure( $error );
13311 }
13312
13313 /**
13314 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
13315 *
13316 * @author Leo Fajardo (@leorw)
13317 * @since 2.3.2
13318 *
13319 * @return number[]
13320 */
13321 function get_installs_ids_with_foreign_licenses() {
13322 $installs = array();
13323
13324 if (
13325 is_object( $this->_license ) &&
13326 $this->_site->user_id != $this->_license->user_id
13327 ) {
13328 $installs[] = $this->_site->id;
13329 }
13330
13331 /**
13332 * Also try to get foreign licenses for the context product's add-ons.
13333 */
13334 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
13335
13336 foreach ( $installs_by_slug_map as $slug => $install_info ) {
13337 if ( $slug == $this->get_slug() ) {
13338 continue;
13339 }
13340
13341 $install = $install_info['install'];
13342 $license = $install_info['license'];
13343
13344 if (
13345 is_object( $license ) &&
13346 $install->user_id != $license->user_id
13347 ) {
13348 $installs[] = $install->id;
13349 }
13350 }
13351
13352 return $installs;
13353 }
13354
13355 /**
13356 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
13357 *
13358 * @author Leo Fajardo (@leorw)
13359 * @since 2.3.2
13360 *
13361 * @param number[] $install_ids
13362 */
13363 function _add_user_change_dialog_box( $install_ids ) {
13364 $vars = array(
13365 'id' => $this->_module_id,
13366 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
13367 );
13368
13369 fs_require_template( 'forms/user-change.php', $vars );
13370 }
13371
13372 /**
13373 * @author Leo Fajardo (@leorw)
13374 * @since 2.3.1
13375 */
13376 function _add_data_debug_mode_dialog_box() {
13377 $vars = array(
13378 'id' => $this->_module_id,
13379 );
13380
13381 fs_require_template( 'forms/data-debug-mode.php', $vars );
13382 }
13383
13384 /**
13385 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
13386 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
13387 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
13388 * is only activated on a single production site.
13389 *
13390 * @author Leo Fajardo (@leorw)
13391 * @since 2.2.1
13392 *
13393 * @param bool $is_license_deactivation
13394 *
13395 * @return array
13396 */
13397 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
13398 if ( fs_is_network_admin() ) {
13399 // Subscription cancellation dialog box is currently not supported for multisite networks.
13400 return array();
13401 }
13402
13403 if ( $this->is_whitelabeled() ) {
13404 return array();
13405 }
13406
13407 $license = $this->_get_license();
13408
13409 /**
13410 * If the installation is associated with a non-lifetime license, which is either a single-site or only activated on a single production site (or zero), and connected to an active subscription, suggest the customer to cancel the subscription upon deactivation.
13411 *
13412 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
13413 * @since 2.2.1
13414 */
13415 if ( ! is_object( $license ) ||
13416 $license->is_lifetime() ||
13417 ( ! $license->is_single_site() && $license->activated > 1 )
13418 ) {
13419 return array();
13420 }
13421
13422 /**
13423 * @var FS_Subscription $subscription
13424 */
13425 $subscription = $this->_get_subscription( $license->id );
13426 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
13427 return array();
13428 }
13429
13430 return array(
13431 'id' => $this->_module_id,
13432 'license' => $license,
13433 'has_trial' => $this->is_paid_trial(),
13434 'is_license_deactivation' => $is_license_deactivation,
13435 );
13436 }
13437
13438 /**
13439 * @author Leo Fajardo (@leorw)
13440 * @since 2.0.2
13441 */
13442 function _add_premium_version_upgrade_selection_dialog_box() {
13443 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
13444 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
13445 return;
13446 }
13447
13448 $vars = array(
13449 'id' => $this->_module_id,
13450 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
13451 $modules_update->response[ $this->_plugin_basename ]->new_version :
13452 $modules_update->response[ $this->_plugin_basename ]['new_version']
13453 );
13454
13455 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13456 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13457 }
13458
13459 /**
13460 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13461 * page.
13462 *
13463 * @author Leo Fajardo (@leorw)
13464 * @since 1.2.1.5
13465 */
13466 function _add_optout_dialog() {
13467 if ( $this->is_theme() ) {
13468 $vars = null;
13469 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13470 }
13471
13472 $vars = array( 'id' => $this->_module_id );
13473 fs_require_template( 'forms/optout.php', $vars );
13474 }
13475
13476 /**
13477 * Prepare page to include all required UI and logic for the license activation dialog.
13478 *
13479 * @author Vova Feldman (@svovaf)
13480 * @since 1.2.0
13481 */
13482 function _add_license_activation() {
13483 if ( $this->is_migration() ) {
13484 return;
13485 }
13486
13487 if ( ! $this->is_user_admin() ) {
13488 // Only admins can activate a license.
13489 return;
13490 }
13491
13492 if ( ! $this->has_paid_plan() ) {
13493 // Module doesn't have any paid plans.
13494 return;
13495 }
13496
13497 if (
13498 $this->has_premium_version() &&
13499 ! $this->is_premium() &&
13500 /**
13501 * Also handle the case when an upgrade was made using the free version.
13502 *
13503 * @author Leo Fajardo (@leorw)
13504 * @since 2.3.2
13505 */
13506 ! is_object( $this->_get_license() )
13507 ) {
13508 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13509 return;
13510 }
13511
13512 // Add license activation link and AJAX request handler.
13513 if ( self::is_plugins_page() ) {
13514 $is_network_admin = fs_is_network_admin();
13515
13516 if (
13517 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13518 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13519 ) {
13520 if (
13521 $this->is_premium() ||
13522 ( $this->has_paid_plan() && ! $this->has_premium_version() )
13523 ) {
13524 /**
13525 * @since 1.2.0 Add license action link only on plugins page.
13526 */
13527 $this->_add_license_action_link();
13528 }
13529 }
13530 }
13531
13532 // Add license activation AJAX callback.
13533 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13534
13535 // Add resend license AJAX callback.
13536 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13537 }
13538
13539 /**
13540 * Prepares page to include all required UI and logic for the "Change User" dialog.
13541 *
13542 * @author Leo Fajardo (@leorw)
13543 * @since 2.3.2
13544 */
13545 function _add_user_change_option() {
13546 if ( ! $this->should_handle_user_change() ) {
13547 return;
13548 }
13549
13550 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13551
13552 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13553 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13554 return;
13555 }
13556
13557 // Add user change AJAX handler.
13558 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13559 }
13560
13561 /**
13562 * @author Leo Fajardo (@leorw)
13563 * @since 2.3.2
13564 */
13565 function should_handle_user_change() {
13566 if ( ! $this->is_user_admin() ) {
13567 // Only admins can change user.
13568 return false;
13569 }
13570
13571 if ( $this->is_addon() ) {
13572 return false;
13573 }
13574
13575 if ( ! $this->is_registered() ) {
13576 return false;
13577 }
13578
13579 if (
13580 $this->is_network_active() &&
13581 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13582 ) {
13583 // Handle only on site-level "Account" section for now.
13584 return false;
13585 }
13586
13587 return true;
13588 }
13589
13590 /**
13591 * @author Leo Fajardo (@leorw)
13592 * @since 2.0.2
13593 */
13594 function _add_premium_version_upgrade_selection() {
13595 if ( ! $this->is_user_admin() ) {
13596 return;
13597 }
13598
13599 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13600 // This is relevant only to the free versions and premium versions without an active license.
13601 return;
13602 }
13603
13604 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13605 $this->_add_premium_version_upgrade_selection_action();
13606 }
13607 }
13608
13609 /**
13610 * @author Edgar Melkonyan
13611 * @since 2.4.1
13612 *
13613 * @throws Freemius_Exception
13614 */
13615 function _toggle_whitelabel_mode_ajax_handler() {
13616 $this->_logger->entrance();
13617
13618 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13619
13620 if ( ! $this->is_user_admin() ) {
13621 // Only for admins.
13622 self::shoot_ajax_failure();
13623 }
13624
13625 $license = $this->get_api_user_scope()->call(
13626 "/licenses/{$this->_site->license_id}.json",
13627 'put',
13628 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13629 );
13630
13631 if ( ! $this->is_api_result_entity( $license ) ) {
13632 self::shoot_ajax_failure(
13633 FS_Api::is_api_error_object( $license ) ?
13634 $license->error->message :
13635 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13636 );
13637 }
13638
13639 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13640 $this->_store_licenses();
13641
13642 $this->_sync_license();
13643
13644 if ( ! $license->is_whitelabeled ) {
13645 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13646 } else {
13647 $this->_admin_notices->add_sticky(
13648 sprintf(
13649 $this->get_text_inline(
13650 'Your %s license was flagged as white-labeled to hide sensitive information from the WP Admin (e.g. your email, license key, prices, billing address & invoices). If you ever wish to revert it back, you can easily do it through your %s. If this was a mistake you can also %s.',
13651 'license_whitelabeled'
13652 ),
13653 "<strong>{$this->get_plugin_title()}</strong>",
13654 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13655 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13656 ),
13657 'license_whitelabeled'
13658 );
13659 }
13660
13661 self::shoot_ajax_response( array( 'success' => true ) );
13662 }
13663
13664 /**
13665 * @author Leo Fajardo (@leorw)
13666 * @since 2.3.0
13667 */
13668 function _add_beta_mode_update_handler() {
13669 if ( ! $this->is_user_admin() ) {
13670 return;
13671 }
13672
13673 if ( ! $this->is_premium() ) {
13674 return;
13675 }
13676
13677 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13678 }
13679
13680 /**
13681 * @author Leo Fajardo (@leorw)
13682 * @since 2.3.0
13683 */
13684 function _set_beta_mode_ajax_handler() {
13685 $this->_logger->entrance();
13686
13687 $this->check_ajax_referer( 'set_beta_mode' );
13688
13689 if ( ! $this->is_user_admin() ) {
13690 // Only for admins.
13691 self::shoot_ajax_failure();
13692 }
13693
13694 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13695
13696 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13697 self::shoot_ajax_failure();
13698 }
13699
13700 $site = $this->api_site_call(
13701 '',
13702 'put',
13703 array(
13704 'is_beta' => ( 'true' == $is_beta ),
13705 'fields' => 'is_beta'
13706 )
13707 );
13708
13709 if ( ! $this->is_api_result_entity( $site ) ) {
13710 self::shoot_ajax_failure(
13711 FS_Api::is_api_error_object( $site ) ?
13712 $site->error->message :
13713 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13714 );
13715 }
13716
13717 $this->_site->is_beta = $site->is_beta;
13718 $this->_store_site();
13719
13720 self::shoot_ajax_response( array( 'success' => true ) );
13721 }
13722
13723 /**
13724 * License activation WP AJAX handler.
13725 *
13726 * @author Leo Fajardo (@leorw)
13727 * @since 1.1.9
13728 *
13729 * @uses Freemius::activate_license()
13730 */
13731 function _activate_license_ajax_action() {
13732 $this->_logger->entrance();
13733
13734 $this->check_ajax_referer( 'activate_license' );
13735
13736 $license_key = trim( fs_request_get_raw( 'license_key' ) );
13737
13738 if ( empty( $license_key ) ) {
13739 exit;
13740 }
13741
13742 $sites = fs_is_network_admin() ?
13743 fs_request_get( 'sites', array(), 'post' ) :
13744 array();
13745
13746 $result = $this->activate_license(
13747 $license_key,
13748 $sites,
13749 fs_request_get_bool( 'is_marketing_allowed', null ),
13750 fs_request_get( 'blog_id', null ),
13751 fs_request_get( 'module_id', null, 'post' ),
13752 fs_request_get( 'user_id', null ),
13753 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
13754 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null )
13755 );
13756
13757 if (
13758 $result['success'] &&
13759 $this->is_bundle_license_auto_activation_enabled()
13760 ) {
13761 $license = new FS_Plugin_License();
13762 $license->secret_key = $license_key;
13763
13764 $this->maybe_activate_bundle_license( $license, $sites );
13765 }
13766
13767 echo json_encode( $result );
13768
13769 exit;
13770 }
13771
13772 /**
13773 * User change WP AJAX handler.
13774 *
13775 * @author Leo Fajardo (@leorw)
13776 * @since 2.3.2
13777 */
13778 function _user_change_ajax_action() {
13779 $this->_logger->entrance();
13780
13781 $this->check_ajax_referer( 'change_user' );
13782
13783 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13784 $new_user_id = fs_request_get( 'user_id' );
13785
13786 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13787 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13788 }
13789
13790 $params = array();
13791
13792 if ( ! empty( $new_email_address ) ) {
13793 $params['user_email'] = $new_email_address;
13794 } else {
13795 $params['user_id'] = $new_user_id;
13796 }
13797
13798 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13799 $install_ids = array();
13800
13801 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13802 $install_ids[ $slug ] = $install_info['install']->id;
13803 }
13804
13805 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13806
13807 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13808
13809 if ( FS_Api::is_api_error( $install ) ) {
13810 $error = '';
13811
13812 if ( is_object( $install ) ) {
13813 switch ( $install->error->code ) {
13814 case 'user_exist':
13815 $error = (
13816 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13817 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
13818 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>' ) .
13819 sprintf(
13820 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13821 $this->get_account_url( 'change_owner', array(
13822 'state' => 'init',
13823 'candidate_email' => $new_email_address
13824 ) ),
13825 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
13826 )
13827 );
13828 break;
13829 }
13830 }
13831
13832 if ( empty( $error ) ) {
13833 $error = FS_Api::is_api_error_object( $install ) ?
13834 $install->error->message :
13835 var_export( $install->error, true );
13836 }
13837
13838 self::shoot_ajax_failure( $error );
13839 } else {
13840 if (
13841 // If successful ownership change.
13842 $this->get_user()->id != $install->user_id ||
13843 ! empty( $new_email_address )
13844 ) {
13845 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
13846 }
13847 }
13848
13849 self::shoot_ajax_success();
13850 }
13851
13852 /**
13853 * @author Leo Fajardo (@leorw)
13854 * @since 2.3.2.14
13855 */
13856 function starting_migration() {
13857 if ( ! empty( $this->_storage->license_migration ) ) {
13858 // Do not overwrite the data if already set.
13859 return;
13860 }
13861
13862 $this->_storage->license_migration = array(
13863 'is_migrating' => true,
13864 'start_timestamp' => time()
13865 );
13866 }
13867
13868 /**
13869 * @author Leo Fajardo (@leorw)
13870 * @since 2.3.2.14
13871 */
13872 function is_migration() {
13873 if ( $this->is_addon() ) {
13874 return $this->get_parent_instance()->is_migration();
13875 }
13876
13877 if ( empty( $this->_storage->license_migration ) ) {
13878 return false;
13879 }
13880
13881 if ( ! $this->_storage->license_migration['is_migrating'] ) {
13882 return false;
13883 }
13884
13885 return (
13886 // Return `true` if the migration is within 5 minutes from the starting time.
13887 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
13888 );
13889 }
13890
13891 /**
13892 *
13893 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
13894 *
13895 * @author Vova Feldman (@svovaf)
13896 * @since 2.3.0
13897 *
13898 * @param string $license_key
13899 * @param null|bool $is_marketing_allowed
13900 * @param null|number $plugin_id
13901 * @param array $sites
13902 * @param int $blog_id
13903 *
13904 * @return array {
13905 * @var bool $success
13906 * @var string $error
13907 * @var string $next_page
13908 * }
13909 *
13910 * @uses Freemius::activate_license()
13911 */
13912 function activate_migrated_license(
13913 $license_key,
13914 $is_marketing_allowed = null,
13915 $plugin_id = null,
13916 $sites = array(),
13917 $blog_id = null
13918 ) {
13919 $this->_logger->entrance();
13920
13921 $result = $this->activate_license(
13922 $license_key,
13923 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
13924 $this->get_sites_for_network_level_optin() :
13925 $sites,
13926 $is_marketing_allowed,
13927 $blog_id,
13928 $plugin_id
13929 );
13930
13931 // No need to show the sticky after license activation notice after migrating a license.
13932 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13933
13934 return $result;
13935 }
13936
13937 /**
13938 * @author Leo Fajardo (@leorw)
13939 * @since 2.3.1
13940 *
13941 * @return string
13942 */
13943 function get_pricing_js_path() {
13944 if ( ! isset( $this->_pricing_js_path ) ) {
13945 $pricing_js_path = $this->apply_filters( 'freemius_pricing_js_path', '' );
13946
13947 if ( empty( $pricing_js_path ) ) {
13948 global $fs_active_plugins;
13949
13950 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
13951 if ( $data->plugin_path == $this->get_plugin_basename() ) {
13952 $plugin_or_theme_root_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
13953
13954 $pricing_js_path = $plugin_or_theme_root_dir
13955 . '/'
13956 // The basename will be `plugins`, `themes`, or the basename of a custom plugins or themes directory.
13957 . str_replace( '../' . basename( $plugin_or_theme_root_dir ) . '/', '', $sdk_path )
13958 . '/includes/freemius-pricing/freemius-pricing.js';
13959
13960 break;
13961 }
13962 }
13963 }
13964
13965 $this->_pricing_js_path = $pricing_js_path;
13966 }
13967
13968 return $this->_pricing_js_path;
13969 }
13970
13971 /**
13972 * @author Leo Fajardo (@leorw)
13973 * @since 2.3.1
13974 *
13975 * @return bool
13976 */
13977 function should_use_external_pricing() {
13978 if ( is_null( $this->_use_external_pricing ) ) {
13979 $pricing_js_path = $this->get_pricing_js_path();
13980
13981 $this->_use_external_pricing = ( empty( $pricing_js_path ) || ! file_exists( $pricing_js_path ) );
13982 }
13983
13984 return $this->_use_external_pricing;
13985 }
13986
13987 /**
13988 * The implementation of this method was previously in `_activate_license_ajax_action()`.
13989 *
13990 * @author Vova Feldman (@svovaf)
13991 * @since 2.2.4
13992 * @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).
13993 * @param string $license_key
13994 * @param array $sites
13995 * @param null|bool $is_marketing_allowed
13996 * @param null|int $blog_id
13997 * @param null|number $plugin_id
13998 * @param null|number $license_owner_id
13999 * @param bool|null $is_extensions_tracking_allowed
14000 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint.
14001 *
14002 *
14003 * @return array {
14004 * @var bool $success
14005 * @var string $error
14006 * @var string $next_page
14007 * }
14008 */
14009 private function activate_license(
14010 $license_key,
14011 $sites = array(),
14012 $is_marketing_allowed = null,
14013 $blog_id = null,
14014 $plugin_id = null,
14015 $license_owner_id = null,
14016 $is_extensions_tracking_allowed = null,
14017 $is_diagnostic_tracking_allowed = null
14018 ) {
14019 $this->_logger->entrance();
14020
14021 $license_key = trim( $license_key );
14022
14023 $is_network_activation_or_migration = (
14024 fs_is_network_admin() ||
14025 ( ! empty( $sites ) && $this->is_migration() )
14026 );
14027
14028 if ( ! $is_network_activation_or_migration ) {
14029 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
14030 $sites = array();
14031 }
14032
14033 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
14034 $this :
14035 $this->get_addon_instance( $plugin_id );
14036
14037 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
14038 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
14039 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
14040 ) );
14041
14042 $error = false;
14043 $next_page = false;
14044
14045 $has_valid_blog_id = is_numeric( $blog_id );
14046
14047 $user = null;
14048
14049 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
14050 /**
14051 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
14052 *
14053 * @author Vova Feldman (@svovaf)
14054 */
14055 $user = $fs->get_parent_instance()->get_current_or_network_user();
14056 } else if ( $fs->is_registered() ) {
14057 $user = $fs->get_current_or_network_user();
14058 }
14059
14060 if ( $has_valid_blog_id ) {
14061 /**
14062 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
14063 *
14064 * @author Leo Fajardo (@leorw)
14065 */
14066 $fs->switch_to_blog( $blog_id );
14067 }
14068
14069 if ( is_object( $user ) ) {
14070 $result = true;
14071
14072 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
14073 // If no specific blog ID was provided, activate the license for all sites in the network.
14074 $blog_2_install_map = array();
14075 $site_ids = array();
14076
14077 foreach ( $sites as $site ) {
14078 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
14079 continue;
14080 }
14081
14082 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
14083
14084 if ( is_object( $install ) ) {
14085 $blog_2_install_map[ $site['blog_id'] ] = $install;
14086 } else {
14087 $site_ids[] = $site['blog_id'];
14088 }
14089 }
14090
14091 if ( ! empty( $blog_2_install_map ) ) {
14092 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
14093 }
14094
14095 if ( true === $result && ! empty( $site_ids ) ) {
14096 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
14097 }
14098 } else {
14099 if ( $fs->is_registered() ) {
14100 $params = array(
14101 'license_key' => $fs->apply_filters( 'license_key', $license_key )
14102 );
14103
14104 $install_ids = array();
14105
14106 $change_owner = FS_User::is_valid_id( $license_owner_id );
14107
14108 if ( $change_owner ) {
14109 $params['user_id'] = $license_owner_id;
14110
14111 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
14112
14113 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
14114 $install_ids[ $slug ] = $install_info['install']->id;
14115 }
14116
14117 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
14118 }
14119
14120 $api = $fs->get_api_site_scope();
14121
14122 $result = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
14123
14124 if ( ! FS_Api::is_api_error( $result ) ) {
14125 $install = $result;
14126
14127 $fs->reconnect_locally( $has_valid_blog_id );
14128
14129 if (
14130 $change_owner &&
14131 // If successful ownership change.
14132 $fs->get_user()->id != $install->user_id
14133 ) {
14134 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
14135 }
14136 }
14137 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
14138 $result = $fs->activate_license_on_site( $user, $license_key );
14139 }
14140 }
14141
14142 if ( true !== $result && ! FS_Api::is_api_result_entity( $result ) ) {
14143 if ( FS_Api::is_blocked( $result ) ) {
14144 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
14145 }
14146
14147 $error = FS_Api::is_api_error_object( $result ) ?
14148 $result->error->message :
14149 var_export( $result, true );
14150 } else {
14151 $fs->network_upgrade_mode_completed();
14152
14153 $fs->_user = $user;
14154
14155 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
14156 $fs->_site = $fs->get_network_install();
14157 }
14158
14159 $fs->_sync_license( true, $has_valid_blog_id );
14160
14161 $this->maybe_sync_install_user();
14162
14163 $next_page = $fs->is_addon() ?
14164 $fs->get_parent_instance()->get_account_url() :
14165 $fs->get_after_activation_url( 'after_connect_url' );
14166 }
14167 } else {
14168 $next_page = $fs->opt_in(
14169 false,
14170 false,
14171 false,
14172 $license_key,
14173 false,
14174 false,
14175 false,
14176 $is_marketing_allowed,
14177 $sites
14178 );
14179
14180 if ( isset( $next_page->error ) ) {
14181 $error = $next_page->error;
14182 } else {
14183 if ( $is_network_activation_or_migration ) {
14184 /**
14185 * Get the list of sites that were just opted-in (and license activated).
14186 * This is an optimization for the next part below saving some DB queries.
14187 */
14188 $connected_sites = array();
14189 foreach ( $sites as $site ) {
14190 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
14191 $connected_sites[ $site['blog_id'] ] = true;
14192 }
14193 }
14194
14195 $all_sites = self::get_sites();
14196 $pending_blog_ids = array();
14197
14198 /**
14199 * 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.
14200 *
14201 * @author Vova Feldman (@svovaf)
14202 */
14203 foreach ( $all_sites as $site ) {
14204 $blog_id = self::get_site_blog_id( $site );
14205
14206 if ( isset( $connected_sites[ $blog_id ] ) ) {
14207 // Site was just connected.
14208 continue;
14209 }
14210
14211 if ( $fs->is_installed_on_site( $blog_id ) ) {
14212 // Site was already connected before.
14213 continue;
14214 }
14215
14216 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
14217 // Site's connection was delegated.
14218 continue;
14219 }
14220
14221 if ( $fs->is_anonymous_site( $blog_id ) ) {
14222 // Site connection was already skipped.
14223 continue;
14224 }
14225
14226 $pending_blog_ids[] = $blog_id;
14227 }
14228
14229 if ( ! empty( $pending_blog_ids ) ) {
14230 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
14231 $fs->skip_connection( $pending_blog_ids );
14232 } else {
14233 $fs->delegate_connection( $pending_blog_ids );
14234 }
14235 }
14236 }
14237 }
14238 }
14239
14240 if ( false === $error && true === $fs->_storage->require_license_activation ) {
14241 $fs->_storage->require_license_activation = false;
14242 }
14243
14244 $result = array(
14245 'success' => ( false === $error )
14246 );
14247
14248 if ( false !== $error ) {
14249 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
14250 } else {
14251 if ( $fs->is_addon() || $fs->has_addons() ) {
14252 /**
14253 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
14254 * an updated valid user licenses collection will be fetched from the server which is used to also
14255 * update the account add-ons (add-ons the user has licenses for).
14256 *
14257 * @author Leo Fajardo (@leorw)
14258 * @since 2.2.4
14259 */
14260 $fs->purge_valid_user_licenses_cache();
14261 }
14262
14263 $result['next_page'] = $next_page;
14264 }
14265
14266 return $result;
14267 }
14268
14269 /**
14270 * @author Leo Fajardo (@leorw)
14271 * @since 2.3.2
14272 *
14273 * @return array {
14274 * @key string Product slug.
14275 * @value array {
14276 * @property FS_Site $site
14277 * @property FS_Plugin_License $license
14278 * }
14279 * }
14280 */
14281 private function get_parent_and_addons_installs_info() {
14282 $fs = $this->is_addon() ?
14283 $this->get_parent_instance() :
14284 $this;
14285
14286 $installed_addons_ids = array();
14287
14288 $installed_addons_instances = $fs->get_installed_addons();
14289 foreach ( $installed_addons_instances as $instance ) {
14290 $installed_addons_ids[] = $instance->get_id();
14291 }
14292
14293 $addons_ids = array_unique( array_merge(
14294 $installed_addons_ids,
14295 $fs->get_updated_account_addons()
14296 ) );
14297
14298 // Add parent product info.
14299 $installs_info_by_slug_map = array(
14300 $fs->get_slug() => array(
14301 'install' => $fs->get_site(),
14302 'license' => $fs->_get_license()
14303 )
14304 );
14305
14306 foreach ( $addons_ids as $addon_id ) {
14307 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
14308
14309 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
14310
14311 if ( ! isset( $addon_info['is_connected'] ) || ! $addon_info['is_connected'] ) {
14312 // Add-on is not associated with an install entity.
14313 continue;
14314 }
14315
14316 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
14317 'install' => $addon_info['site'],
14318 'license' => isset( $addon_info['license'] ) ?
14319 $addon_info['license'] :
14320 null
14321 );
14322 }
14323
14324 return $installs_info_by_slug_map;
14325 }
14326
14327 /**
14328 * @author Leo Fajardo (@leorw)
14329 * @since 1.2.3.1
14330 */
14331 function _network_activate_ajax_action() {
14332 $this->_logger->entrance();
14333
14334 $this->check_ajax_referer( 'network_activate' );
14335
14336 $plugin_id = fs_request_get( 'module_id', '', 'post' );
14337 $fs = ( $plugin_id == $this->_module_id ) ?
14338 $this :
14339 $this->get_addon_instance( $plugin_id );
14340
14341 $error = false;
14342
14343 $sites = fs_request_get( 'sites', array(), 'post' );
14344 if ( is_array( $sites ) && ! empty( $sites ) ) {
14345 $sites_by_action = array(
14346 'allow' => array(),
14347 'delegate' => array(),
14348 'skip' => array()
14349 );
14350
14351 foreach ( $sites as $site ) {
14352 $sites_by_action[ $site['action'] ][] = $site;
14353 }
14354
14355 $total_sites = count( $sites );
14356 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
14357
14358 $next_page = '';
14359
14360 $has_any_install = fs_request_get_bool( 'has_any_install' );
14361
14362 if ( $total_sites === $total_sites_to_delegate &&
14363 ! $this->is_network_upgrade_mode() &&
14364 ! $has_any_install
14365 ) {
14366 $this->delegate_connection();
14367 } else {
14368 if ( ! empty( $sites_by_action['delegate'] ) ) {
14369 $this->delegate_connection( self::get_sites_blog_ids( $sites_by_action['delegate'] ) );
14370 }
14371
14372 if ( ! empty( $sites_by_action['skip'] ) ) {
14373 $this->skip_connection( self::get_sites_blog_ids( $sites_by_action['skip'] ) );
14374 }
14375
14376 if ( empty( $sites_by_action['allow'] ) ) {
14377 if ( $has_any_install ) {
14378 $first_install = $fs->find_first_install();
14379
14380 if ( ! is_null( $first_install ) ) {
14381 $fs->_site = $first_install['install'];
14382 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
14383
14384 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
14385 $fs->_storage->network_user_id = $fs->_user->id;
14386 }
14387 }
14388 } else {
14389 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
14390 $next_page = $fs->opt_in(
14391 false,
14392 false,
14393 false,
14394 false,
14395 false,
14396 false,
14397 false,
14398 fs_request_get_bool( 'is_marketing_allowed', null ),
14399 $sites_by_action['allow']
14400 );
14401 } else {
14402 $next_page = $fs->install_with_user(
14403 $this->get_network_user(),
14404 false,
14405 false,
14406 false,
14407 true,
14408 $sites_by_action['allow']
14409 );
14410 }
14411
14412 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
14413 $error = $next_page->error;
14414 }
14415 }
14416 }
14417
14418 if ( empty( $next_page ) ) {
14419 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
14420 }
14421 } else {
14422 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
14423 }
14424
14425 $result = array(
14426 'success' => ( false === $error )
14427 );
14428
14429 if ( false !== $error ) {
14430 $result['error'] = $error;
14431 } else {
14432 $result['next_page'] = $next_page;
14433 }
14434
14435 echo json_encode( $result );
14436
14437 exit;
14438 }
14439
14440 /**
14441 * Billing update AJAX callback.
14442 *
14443 * @author Vova Feldman (@svovaf)
14444 * @since 1.2.1.5
14445 */
14446 function _update_billing_ajax_action() {
14447 $this->_logger->entrance();
14448
14449 $this->check_ajax_referer( 'update_billing' );
14450
14451 if ( ! $this->is_user_admin() ) {
14452 // Only for admins.
14453 self::shoot_ajax_failure();
14454 }
14455
14456 $billing = fs_request_get( 'billing' );
14457
14458 $api = $this->get_api_user_scope();
14459 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
14460 'plugin_id' => $this->get_parent_id(),
14461 ) ) );
14462
14463 if ( ! $this->is_api_result_entity( $result ) ) {
14464 self::shoot_ajax_failure();
14465 }
14466
14467 // Purge cached billing.
14468 $this->get_api_user_scope()->purge_cache( 'billing.json' );
14469
14470 self::shoot_ajax_success();
14471 }
14472
14473 /**
14474 * Trial start for anonymous users (AJAX callback).
14475 *
14476 * @author Vova Feldman (@svovaf)
14477 * @since 1.2.1.5
14478 */
14479 function _start_trial_ajax_action() {
14480 $this->_logger->entrance();
14481
14482 $this->check_ajax_referer( 'start_trial' );
14483
14484 if ( ! $this->is_user_admin() ) {
14485 // Only for admins.
14486 self::shoot_ajax_failure();
14487 }
14488
14489 $trial_data = fs_request_get( 'trial' );
14490
14491 $next_page = $this->opt_in(
14492 false,
14493 false,
14494 false,
14495 false,
14496 false,
14497 $trial_data['plan_id']
14498 );
14499
14500 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14501 self::shoot_ajax_failure(
14502 isset( $next_page->error ) ?
14503 $next_page->error->message :
14504 var_export( $next_page, true )
14505 );
14506 }
14507
14508 $this->shoot_ajax_success( array(
14509 'next_page' => $next_page,
14510 ) );
14511 }
14512
14513 /**
14514 * @author Leo Fajardo (@leorw)
14515 * @since 1.2.0
14516 */
14517 function _resend_license_key_ajax_action() {
14518 $this->_logger->entrance();
14519
14520 $this->check_ajax_referer( 'resend_license_key' );
14521
14522 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14523
14524 if ( empty( $email_address ) ) {
14525 exit;
14526 }
14527
14528 $error = false;
14529
14530 $api = $this->get_api_plugin_scope();
14531 $result = $api->call( '/licenses/resend.json', 'post',
14532 array(
14533 'email' => $email_address,
14534 'url' => home_url(),
14535 )
14536 );
14537
14538 if ( is_object( $result ) && isset( $result->error ) ) {
14539 $error = $result->error;
14540
14541 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14542 $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' );
14543 } else if ( 'no_license' === $error->code ) {
14544 $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' );
14545 } else {
14546 $error = $error->message;
14547 }
14548 }
14549
14550 $licenses = array(
14551 'success' => ( false === $error )
14552 );
14553
14554 if ( false !== $error ) {
14555 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14556 }
14557
14558 echo json_encode( $licenses );
14559
14560 exit;
14561 }
14562
14563 /**
14564 * @author Vova Feldman (@svovaf)
14565 * @since 1.2.1.8
14566 *
14567 * @var string
14568 */
14569 private static $_pagenow;
14570
14571 /**
14572 * Get current page or the referer if executing a WP AJAX request.
14573 *
14574 * @author Vova Feldman (@svovaf)
14575 * @since 1.2.1.8
14576 *
14577 * @return string
14578 */
14579 static function get_current_page() {
14580 if ( ! isset( self::$_pagenow ) ) {
14581 global $pagenow;
14582 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14583 /**
14584 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14585 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14586 *
14587 * @author Leo Fajardo (@leorw)
14588 * @since 2.2.3
14589 */
14590 if ( is_network_admin() ) {
14591 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14592 } else if ( is_user_admin() ) {
14593 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14594 } else {
14595 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14596 }
14597
14598 $pagenow = $self_matches[1];
14599 $pagenow = trim( $pagenow, '/' );
14600 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14601 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14602 $pagenow = 'index.php';
14603 } else {
14604 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14605 $pagenow = strtolower( $self_matches[1] );
14606 if ( '.php' !== substr($pagenow, -4, 4) )
14607 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14608 }
14609 }
14610
14611 self::$_pagenow = $pagenow;
14612
14613 if ( self::is_ajax() &&
14614 'admin-ajax.php' === $pagenow
14615 ) {
14616 $referer = fs_get_raw_referer();
14617
14618 if ( is_string( $referer ) ) {
14619 $parts = explode( '?', $referer );
14620
14621 self::$_pagenow = basename( $parts[0] );
14622 }
14623 }
14624 }
14625
14626 return self::$_pagenow;
14627 }
14628
14629 /**
14630 * Helper method to check if user in the plugins page.
14631 *
14632 * @author Vova Feldman (@svovaf)
14633 * @since 1.2.1.5
14634 *
14635 * @return bool
14636 */
14637 static function is_plugins_page() {
14638 return ( 'plugins.php' === self::get_current_page() );
14639 }
14640
14641 /**
14642 * @author Leo Fajardo (@leorw)
14643 * @since 2.2.3
14644 *
14645 * @return bool
14646 */
14647 static function is_plugin_install_page() {
14648 return ( 'plugin-install.php' === self::get_current_page() );
14649 }
14650
14651 /**
14652 * @author Leo Fajardo (@leorw)
14653 * @since 2.0.2
14654 *
14655 * @return bool
14656 */
14657 static function is_updates_page() {
14658 return ( 'update-core.php' === self::get_current_page() );
14659 }
14660
14661 /**
14662 * Helper method to check if user in the themes page.
14663 *
14664 * @author Vova Feldman (@svovaf)
14665 * @since 1.2.2.6
14666 *
14667 * @return bool
14668 */
14669 static function is_themes_page() {
14670 return ( 'themes.php' === self::get_current_page() );
14671 }
14672
14673 #----------------------------------------------------------------------------------
14674 #region Affiliation
14675 #----------------------------------------------------------------------------------
14676
14677 /**
14678 * @author Leo Fajardo (@leorw)
14679 * @since 1.2.3
14680 *
14681 * @return bool
14682 */
14683 function has_affiliate_program() {
14684 if ( ! is_object( $this->_plugin ) ) {
14685 return false;
14686 }
14687
14688 return $this->_plugin->has_affiliate_program();
14689 }
14690
14691 /**
14692 * Get Plugin ID under which we will track affiliate application.
14693 *
14694 * This could either be the Bundle ID or the main plugin ID.
14695 *
14696 * @return number Bundle ID if developer has provided one, else the main plugin ID.
14697 */
14698 private function get_plugin_id_for_affiliate_terms() {
14699 return $this->has_bundle_context() ?
14700 $this->get_bundle_id() :
14701 $this->_plugin->id;
14702 }
14703
14704 /**
14705 * @author Leo Fajardo (@leorw)
14706 * @since 1.2.4
14707 */
14708 private function fetch_affiliate_terms() {
14709 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14710 /**
14711 * In case we have a bundle set in SDK configuration, we would like to use that for affiliates, not the main plugin.
14712 */
14713 $plugins_api = $this->has_bundle_context() ?
14714 $this->get_api_bundle_scope() :
14715 $this->get_api_plugin_scope();
14716
14717 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14718
14719 /**
14720 * 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.
14721 */
14722 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14723 return;
14724 }
14725
14726 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14727 }
14728 }
14729
14730 /**
14731 * @author Leo Fajardo (@leorw)
14732 * @since 1.2.4
14733 */
14734 private function fetch_affiliate_and_custom_terms() {
14735 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14736 $application_data = $this->_storage->affiliate_application_data;
14737 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14738
14739 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14740
14741 $users_api = $this->get_api_user_scope();
14742 $result = $users_api->get( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14743 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14744 if ( ! empty( $result->affiliates ) ) {
14745 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14746
14747 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14748 $application_data['status'] = $affiliate->status;
14749 $this->_storage->affiliate_application_data = $application_data;
14750 }
14751
14752 if ( $affiliate->is_using_custom_terms ) {
14753 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14754 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14755 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14756 }
14757 }
14758
14759 $this->affiliate = $affiliate;
14760 }
14761 }
14762 }
14763 }
14764
14765 /**
14766 * @author Leo Fajardo (@leorw)
14767 * @since 1.2.3
14768 */
14769 private function fetch_affiliate_and_terms() {
14770 $this->_logger->entrance();
14771
14772 $this->fetch_affiliate_terms();
14773 $this->fetch_affiliate_and_custom_terms();
14774 }
14775
14776 /**
14777 * @author Leo Fajardo (@leorw)
14778 * @since 1.2.3
14779 *
14780 * @return FS_Affiliate
14781 */
14782 function get_affiliate() {
14783 return $this->affiliate;
14784 }
14785
14786
14787 /**
14788 * @author Leo Fajardo (@leorw)
14789 * @since 1.2.3
14790 *
14791 * @return FS_AffiliateTerms
14792 */
14793 function get_affiliate_terms() {
14794 return is_object( $this->custom_affiliate_terms ) ?
14795 $this->custom_affiliate_terms :
14796 $this->plugin_affiliate_terms;
14797 }
14798
14799 /**
14800 * @author Leo Fajardo (@leorw)
14801 * @since 1.2.3
14802 */
14803 function _submit_affiliate_application() {
14804 $this->_logger->entrance();
14805
14806 $this->check_ajax_referer( 'submit_affiliate_application' );
14807
14808 if ( ! $this->is_user_admin() ) {
14809 // Only for admins.
14810 self::shoot_ajax_failure();
14811 }
14812
14813 $affiliate = fs_request_get( 'affiliate' );
14814
14815 if ( empty( $affiliate['promotion_methods'] ) ) {
14816 unset( $affiliate['promotion_methods'] );
14817 }
14818
14819 if ( ! empty( $affiliate['additional_domains'] ) ) {
14820 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
14821 }
14822
14823 if ( ! $this->is_registered() ) {
14824 $email_address = isset( $affiliate['email'] ) ? $affiliate['email'] : '';
14825
14826 if ( ! is_email( $email_address ) ) {
14827 self::shoot_ajax_failure('Invalid email address.');
14828 }
14829
14830 // Opt in but don't track usage.
14831 $next_page = $this->opt_in(
14832 $email_address,
14833 false,
14834 false,
14835 false,
14836 false,
14837 false,
14838 true
14839 );
14840
14841 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14842 self::shoot_ajax_failure(
14843 isset( $next_page->error ) ?
14844 $next_page->error->message :
14845 var_export( $next_page, true )
14846 );
14847 } else if ( $this->is_pending_activation() ) {
14848 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' ) );
14849 }
14850 }
14851
14852 $this->fetch_affiliate_terms();
14853
14854 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14855
14856 $api = $this->get_api_user_scope();
14857 $result = $api->call(
14858 ( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
14859 'post',
14860 $affiliate
14861 );
14862
14863 if ( $this->is_api_error( $result ) ) {
14864 self::shoot_ajax_failure(
14865 isset( $result->error ) ?
14866 $result->error->message :
14867 var_export( $result, true )
14868 );
14869 } else {
14870 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
14871 $this->_admin_notices->remove_sticky( 'affiliate_program' );
14872 }
14873
14874 $affiliate_application_data = array(
14875 'status' => 'pending',
14876 'stats_description' => $affiliate['stats_description'],
14877 'promotion_method_description' => $affiliate['promotion_method_description'],
14878 );
14879
14880 if ( ! empty( $affiliate['promotion_methods'] ) ) {
14881 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
14882 }
14883
14884 if ( ! empty( $affiliate['domain'] ) ) {
14885 $affiliate_application_data['domain'] = $affiliate['domain'];
14886 }
14887
14888 if ( ! empty( $affiliate['additional_domains'] ) ) {
14889 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
14890 }
14891
14892 $this->_storage->affiliate_application_data = $affiliate_application_data;
14893 }
14894
14895 // Purge cached affiliate.
14896 $api->purge_cache( 'affiliate.json' );
14897
14898 self::shoot_ajax_success( $result );
14899 }
14900
14901 /**
14902 * @author Leo Fajardo (@leorw)
14903 * @since 1.2.3
14904 *
14905 * @return array|null
14906 */
14907 function get_affiliate_application_data() {
14908 if ( empty( $this->_storage->affiliate_application_data ) ) {
14909 return null;
14910 }
14911
14912 return $this->_storage->affiliate_application_data;
14913 }
14914
14915 #endregion Affiliation ------------------------------------------------------------
14916
14917 #----------------------------------------------------------------------------------
14918 #region URL Generators
14919 #----------------------------------------------------------------------------------
14920
14921 /**
14922 * Alias to pricing_url().
14923 *
14924 * @author Vova Feldman (@svovaf)
14925 * @since 1.0.2
14926 *
14927 * @uses pricing_url()
14928 *
14929 * @param string $period Billing cycle
14930 * @param bool $is_trial
14931 *
14932 * @return string
14933 */
14934 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14935 return $this->pricing_url( $period, $is_trial );
14936 }
14937
14938 /**
14939 * @author Vova Feldman (@svovaf)
14940 * @since 1.0.9
14941 *
14942 * @uses get_upgrade_url()
14943 *
14944 * @return string
14945 */
14946 function get_trial_url() {
14947 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
14948 }
14949
14950 /**
14951 * @author Leo Fajardo (@leorw)
14952 * @since 2.1.4
14953 *
14954 * @param string $new_version
14955 *
14956 * @return string
14957 */
14958 function version_upgrade_checkout_link( $new_version ) {
14959 if ( ! is_object( $this->_license ) ) {
14960 $url = $this->pricing_url();
14961
14962 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
14963 } else {
14964 $subscription = $this->_get_subscription( $this->_license->id );
14965
14966 $url = $this->checkout_url(
14967 is_object( $subscription ) ?
14968 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
14969 WP_FS__PERIOD_LIFETIME,
14970 false,
14971 array( 'licenses' => $this->_license->quota )
14972 );
14973
14974 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
14975 }
14976
14977 return sprintf(
14978 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
14979 sprintf(
14980 '<a href="%s">%s</a>',
14981 $this->apply_filters( 'update_notice_checkout_url', $url ),
14982 $purchase_license_text
14983 ),
14984 $new_version
14985 );
14986 }
14987
14988 /**
14989 * Plugin's pricing URL.
14990 *
14991 * @author Vova Feldman (@svovaf)
14992 * @since 1.0.4
14993 *
14994 * @param string $billing_cycle Billing cycle
14995 *
14996 * @param bool $is_trial
14997 *
14998 * @return string
14999 */
15000 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
15001 $this->_logger->entrance();
15002
15003 $params = array(
15004 'billing_cycle' => $billing_cycle
15005 );
15006
15007 if ( $is_trial ) {
15008 $params['trial'] = 'true';
15009 }
15010
15011 $url = $this->is_addon() ?
15012 $this->_parent->addon_url( $this->_slug ) :
15013 $this->_get_admin_page_url( 'pricing', $params );
15014
15015 return $this->apply_filters( 'pricing_url', $url );
15016 }
15017
15018 /**
15019 * Checkout page URL.
15020 *
15021 * @author Vova Feldman (@svovaf)
15022 * @since 1.0.6
15023 *
15024 * @param string $billing_cycle Billing cycle
15025 * @param bool $is_trial
15026 * @param array $extra (optional) Extra parameters, override other query params.
15027 * @param bool|null $network
15028 *
15029 * @return string
15030 */
15031 function checkout_url(
15032 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
15033 $is_trial = false,
15034 $extra = array(),
15035 $network = null
15036 ) {
15037 $this->_logger->entrance();
15038
15039 $params = array(
15040 'checkout' => 'true',
15041 'billing_cycle' => $billing_cycle,
15042 );
15043
15044 if ( $is_trial ) {
15045 $params['trial'] = 'true';
15046 }
15047
15048 /**
15049 * Params in extra override other params.
15050 */
15051 $params = array_merge( $params, $extra );
15052
15053 return $this->apply_filters( 'checkout_url', $this->_get_admin_page_url( 'pricing', $params, $network ) );
15054 }
15055
15056 /**
15057 * Add-on checkout URL.
15058 *
15059 * @author Vova Feldman (@svovaf)
15060 * @since 1.1.7
15061 *
15062 * @param number $addon_id
15063 * @param number $pricing_id
15064 * @param string $billing_cycle
15065 * @param bool $is_trial
15066 * @param bool|null $network
15067 *
15068 * @return string
15069 */
15070 function addon_checkout_url(
15071 $addon_id,
15072 $pricing_id,
15073 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
15074 $is_trial = false,
15075 $network = null
15076 ) {
15077 return $this->checkout_url( $billing_cycle, $is_trial, array(
15078 'plugin_id' => $addon_id,
15079 'pricing_id' => $pricing_id,
15080 ), $network );
15081 }
15082
15083 #endregion
15084
15085 #endregion ------------------------------------------------------------------
15086
15087 /**
15088 * Check if plugin has any add-ons.
15089 *
15090 * @author Vova Feldman (@svovaf)
15091 * @since 1.0.5
15092 *
15093 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
15094 *
15095 * @return bool
15096 */
15097 function has_addons() {
15098 $this->_logger->entrance();
15099
15100 return $this->_has_addons;
15101 }
15102
15103 /**
15104 * Check if plugin can work in anonymous mode.
15105 *
15106 * @author Vova Feldman (@svovaf)
15107 * @since 1.0.9
15108 *
15109 * @return bool
15110 *
15111 * @deprecated Please use is_enable_anonymous() instead.
15112 */
15113 function enable_anonymous() {
15114 return $this->_enable_anonymous;
15115 }
15116
15117 /**
15118 * Check if plugin can work in anonymous mode.
15119 *
15120 * @author Vova Feldman (@svovaf)
15121 * @since 1.1.9
15122 *
15123 * @return bool
15124 */
15125 function is_enable_anonymous() {
15126 return $this->_enable_anonymous;
15127 }
15128
15129 /**
15130 * Check if plugin is premium only (no free plans).
15131 *
15132 * @author Vova Feldman (@svovaf)
15133 * @since 1.1.9
15134 *
15135 * @return bool
15136 */
15137 function is_only_premium() {
15138 return $this->_is_premium_only;
15139 }
15140
15141 /**
15142 * Checks if the plugin's type is "plugin". The other type is "theme".
15143 *
15144 * @author Leo Fajardo (@leorw)
15145 * @since 1.2.2
15146 *
15147 * @return bool
15148 */
15149 function is_plugin() {
15150 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
15151 }
15152
15153 /**
15154 * @author Leo Fajardo (@leorw)
15155 * @since 1.2.2
15156 *
15157 * @return string
15158 */
15159 function get_module_type() {
15160 if ( ! isset( $this->_module_type ) ) {
15161 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
15162 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
15163 }
15164
15165 return $this->_module_type;
15166 }
15167
15168 /**
15169 * @author Leo Fajardo (@leorw)
15170 * @since 1.2.2
15171 *
15172 * @return string
15173 */
15174 function get_plugin_main_file_path() {
15175 return $this->_plugin_main_file_path;
15176 }
15177
15178 /**
15179 * Check if module has a premium code version.
15180 *
15181 * Serviceware module might be freemium without any
15182 * premium code version, where the paid features
15183 * are all part of the service.
15184 *
15185 * @author Vova Feldman (@svovaf)
15186 * @since 1.2.1.6
15187 *
15188 * @return bool
15189 */
15190 function has_premium_version() {
15191 return $this->_has_premium_version;
15192 }
15193
15194 /**
15195 * Check if feature supported with current site's plan.
15196 *
15197 * @author Vova Feldman (@svovaf)
15198 * @since 1.0.1
15199 *
15200 * @todo IMPLEMENT
15201 *
15202 * @param number $feature_id
15203 *
15204 * @throws Exception
15205 */
15206 function is_feature_supported( $feature_id ) {
15207 throw new Exception( 'not implemented' );
15208 }
15209
15210 /**
15211 * @author Vova Feldman (@svovaf)
15212 * @since 1.0.1
15213 *
15214 * @return bool Is running in SSL/HTTPS
15215 */
15216 function is_ssl() {
15217 return WP_FS__IS_HTTPS;
15218 }
15219
15220 /**
15221 * @author Vova Feldman (@svovaf)
15222 * @since 1.0.9
15223 *
15224 * @return bool Is running in AJAX call.
15225 *
15226 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
15227 */
15228 static function is_ajax() {
15229 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
15230 }
15231
15232 /**
15233 * Check if it's an AJAX call targeted for the current module.
15234 *
15235 * @author Vova Feldman (@svovaf)
15236 * @since 1.2.0
15237 *
15238 * @param array|string $actions Collection of AJAX actions.
15239 *
15240 * @return bool
15241 */
15242 function is_ajax_action( $actions ) {
15243 // Verify it's an ajax call.
15244 if ( ! self::is_ajax() ) {
15245 return false;
15246 }
15247
15248 // Verify the call is relevant for the plugin.
15249 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
15250 return false;
15251 }
15252
15253 // Verify it's one of the specified actions.
15254 if ( is_string( $actions ) ) {
15255 $actions = explode( ',', $actions );
15256 }
15257
15258 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15259 $ajax_action = fs_request_get( 'action' );
15260
15261 foreach ( $actions as $action ) {
15262 if ( $ajax_action === $this->get_action_tag( $action ) ) {
15263 return true;
15264 }
15265 }
15266 }
15267
15268 return false;
15269 }
15270
15271 /**
15272 * Check if it's an AJAX call targeted for current request.
15273 *
15274 * @author Vova Feldman (@svovaf)
15275 * @since 1.2.0
15276 *
15277 * @param array|string $actions Collection of AJAX actions.
15278 * @param number|null $module_id
15279 *
15280 * @return bool
15281 */
15282 static function is_ajax_action_static( $actions, $module_id = null ) {
15283 // Verify it's an ajax call.
15284 if ( ! self::is_ajax() ) {
15285 return false;
15286 }
15287
15288
15289 if ( ! empty( $module_id ) ) {
15290 // Verify the call is relevant for the plugin.
15291 if ( $module_id != fs_request_get( 'module_id' ) ) {
15292 return false;
15293 }
15294 }
15295
15296 // Verify it's one of the specified actions.
15297 if ( is_string( $actions ) ) {
15298 $actions = explode( ',', $actions );
15299 }
15300
15301 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15302 $ajax_action = fs_request_get( 'action' );
15303
15304 foreach ( $actions as $action ) {
15305 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
15306 return true;
15307 }
15308 }
15309 }
15310
15311 return false;
15312 }
15313
15314 /**
15315 * @author Vova Feldman (@svovaf)
15316 * @since 1.1.7
15317 *
15318 * @return bool
15319 */
15320 static function is_cron() {
15321 return ( defined( 'DOING_CRON' ) && DOING_CRON );
15322 }
15323
15324 /**
15325 * @author Leo Fajardo (@leorw)
15326 * @since 2.5.0
15327 *
15328 * @return bool
15329 */
15330 static function is_admin_post() {
15331 return ( 'admin-post.php' === self::get_current_page() );
15332 }
15333
15334 /**
15335 * Check if a real user is visiting the admin dashboard.
15336 *
15337 * @author Vova Feldman (@svovaf)
15338 * @since 1.1.7
15339 *
15340 * @return bool
15341 */
15342 function is_user_in_admin() {
15343 return (
15344 is_admin() &&
15345 ! self::is_ajax() &&
15346 ! self::is_cron() &&
15347 ! self::is_admin_post()
15348 );
15349 }
15350
15351 /**
15352 * Check if a real user is in the customizer view.
15353 *
15354 * @author Vova Feldman (@svovaf)
15355 * @since 1.2.2.7
15356 *
15357 * @return bool
15358 */
15359 static function is_customizer() {
15360 return is_customize_preview();
15361 }
15362
15363 /**
15364 * Check if running in HTTPS and if site's plan matching the specified plan.
15365 *
15366 * @param string $plan
15367 * @param bool $exact
15368 *
15369 * @return bool
15370 */
15371 function is_ssl_and_plan( $plan, $exact = false ) {
15372 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
15373 }
15374
15375 /**
15376 * Construct plugin's settings page URL.
15377 *
15378 * @author Vova Feldman (@svovaf)
15379 * @since 1.0.4
15380 *
15381 * @param string $page
15382 * @param array $params
15383 * @param bool|null $network
15384 *
15385 * @return string
15386 */
15387 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
15388 if ( is_null( $network ) ) {
15389 $network = (
15390 $this->_is_network_active &&
15391 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
15392 );
15393 }
15394
15395 if ( 0 < count( $params ) ) {
15396 foreach ( $params as $k => $v ) {
15397 $params[ $k ] = urlencode( $v );
15398 }
15399 }
15400
15401 $page_param = $this->_menu->get_slug( $page );
15402
15403 if ( empty( $page ) &&
15404 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
15405 $this->show_opt_in_on_themes_page()
15406 ) {
15407 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
15408
15409 return add_query_arg(
15410 $params,
15411 $this->admin_url( 'themes.php', 'admin', $network )
15412 );
15413 }
15414
15415 if ( ! $this->has_settings_menu() ) {
15416 if ( ! empty( $page ) ) {
15417 // Module doesn't have a setting page, but since the request is for
15418 // a specific Freemius page, use the admin.php path.
15419 return add_query_arg( array_merge( $params, array(
15420 'page' => $page_param,
15421 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15422 } else {
15423 if ( $this->is_activation_mode() ) {
15424 /**
15425 * @author Vova Feldman
15426 * @since 1.2.1.6
15427 *
15428 * If plugin doesn't have a settings page, create one for the opt-in screen.
15429 */
15430 return add_query_arg( array_merge( $params, array(
15431 'page' => $this->_slug,
15432 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15433 } else {
15434 // Plugin without a settings page.
15435 return add_query_arg(
15436 $params,
15437 $this->admin_url( 'plugins.php', 'admin', $network )
15438 );
15439 }
15440 }
15441 }
15442
15443 // Module has a submenu settings page.
15444 if ( ! $this->_menu->is_top_level() ) {
15445 $parent_slug = $this->_menu->get_parent_slug();
15446 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
15447 $parent_slug :
15448 'admin.php';
15449
15450 return add_query_arg( array_merge( $params, array(
15451 'page' => $page_param,
15452 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
15453 }
15454
15455 // Module has a top level CPT settings page.
15456 if ( $this->_menu->is_cpt() ) {
15457 if ( empty( $page ) && $this->is_activation_mode() ) {
15458 return add_query_arg( array_merge( $params, array(
15459 'page' => $page_param
15460 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15461 } else {
15462 if ( ! empty( $page ) ) {
15463 $params['page'] = $page_param;
15464 }
15465
15466 return add_query_arg(
15467 $params,
15468 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
15469 );
15470 }
15471 }
15472
15473 // Module has a custom top level settings page.
15474 return add_query_arg( array_merge( $params, array(
15475 'page' => $page_param,
15476 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15477 }
15478
15479 #--------------------------------------------------------------------------------
15480 #region Multisite
15481 #--------------------------------------------------------------------------------
15482
15483 /**
15484 * @author Leo Fajardo (@leorw)
15485 * @since 2.0.0
15486 *
15487 * @return bool
15488 */
15489 function is_network_active() {
15490 return $this->_is_network_active;
15491 }
15492
15493 /**
15494 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
15495 *
15496 * @author Leo Fajardo (@leorw)
15497 * @since 2.0.0
15498 *
15499 * @param bool|int[] $all_or_blog_ids
15500 */
15501 private function delegate_connection( $all_or_blog_ids = true ) {
15502 $this->_logger->entrance();
15503
15504 $this->_admin_notices->remove_sticky( 'connect_account' );
15505
15506 if ( true === $all_or_blog_ids ) {
15507 // All sites delegation.
15508 $this->_storage->store( 'is_delegated_connection', true, true );
15509 } else {
15510 // Specified sites delegation.
15511 foreach ( $all_or_blog_ids as $blog_id ) {
15512 $this->delegate_site_connection( $blog_id );
15513 }
15514 }
15515
15516 $this->network_upgrade_mode_completed();
15517 }
15518
15519 /**
15520 * Delegate specific network site conncetion to the site admin.
15521 *
15522 * @author Vova Feldman (@svovaf)
15523 * @since 2.0.0
15524 *
15525 * @param int $blog_id
15526 */
15527 private function delegate_site_connection( $blog_id ) {
15528 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
15529 }
15530
15531 /**
15532 * Check if super-admin delegated the connection of ALL sites to the site admins.
15533 *
15534 * @author Vova Feldman (@svovaf)
15535 * @since 2.0.0
15536 *
15537 * @return bool
15538 */
15539 function is_network_delegated_connection() {
15540 if ( ! $this->_is_network_active ) {
15541 return false;
15542 }
15543
15544 return $this->_storage->get( 'is_delegated_connection', false, true );
15545 }
15546
15547 /**
15548 * @author Leo Fajardo (@leorw)
15549 * @since 2.0.0
15550 *
15551 * @param int $blog_id
15552 *
15553 * @return bool
15554 */
15555 function is_site_delegated_connection( $blog_id = 0 ) {
15556 if ( ! $this->_is_network_active ) {
15557 return false;
15558 }
15559
15560 if ( 0 == $blog_id ) {
15561 $blog_id = get_current_blog_id();
15562 }
15563
15564 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15565 }
15566
15567 /**
15568 * Check if delegated the connection. When running within the network admin,
15569 * and haven't specified the blog ID, checks if network level delegated. If running
15570 * within a site admin or specified a blog ID, check if delegated the connection for
15571 * the current context site.
15572 *
15573 * If executed outside the the admin, check if delegated the connection
15574 * for the current context site OR the whole network.
15575 *
15576 * @author Vova Feldman (@svovaf)
15577 * @since 2.0.0
15578 *
15579 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15580 *
15581 * @return bool
15582 */
15583 function is_delegated_connection( $blog_id = 0 ) {
15584 if ( ! $this->_is_network_active ) {
15585 return false;
15586 }
15587
15588 if ( fs_is_network_admin() && 0 == $blog_id ) {
15589 return $this->is_network_delegated_connection();
15590 }
15591
15592 return (
15593 $this->is_network_delegated_connection() ||
15594 $this->is_site_delegated_connection( $blog_id )
15595 );
15596 }
15597
15598 /**
15599 * Check if the current module is active for the site.
15600 *
15601 * @author Vova Feldman (@svovaf)
15602 * @since 2.0.0
15603 *
15604 * @param int $blog_id
15605 *
15606 * @return bool
15607 */
15608 function is_active_for_site( $blog_id ) {
15609 if ( ! is_multisite() ) {
15610 // Not a multisite and this code is executed, means that the plugin is active.
15611 return true;
15612 }
15613
15614 if ( $this->is_theme() ) {
15615 // All themes are site level activated.
15616 return true;
15617 }
15618
15619 if ( $this->_is_network_active ) {
15620 // Plugin was network activated so it's active.
15621 return true;
15622 }
15623
15624 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15625 }
15626
15627 /**
15628 * @todo Implement pagination when accessing the subsites collection.
15629 *
15630 * @author Leo Fajardo (@leorw)
15631 * @since 2.0.0
15632 *
15633 * @param int $limit Default to 1,000
15634 * @param int $offset Default to 0
15635 *
15636 * @return array Active & public sites collection.
15637 */
15638 static function get_sites( $limit = 1000, $offset = 0 ) {
15639 if ( ! is_multisite() ) {
15640 return array();
15641 }
15642
15643 /**
15644 * For consistency with get_blog_list() which only return active public sites.
15645 *
15646 * @author Vova Feldman (@svovaf)
15647 */
15648 $args = array(
15649 /**
15650 * Commented out in order to handle the migration of site options whether the site is public or not.
15651 *
15652 * @author Leo Fajardo (@leorw)
15653 * @since 2.2.1
15654 */
15655 // 'public' => 1,
15656 'archived' => 0,
15657 'mature' => 0,
15658 'spam' => 0,
15659 'deleted' => 0,
15660 'number' => $limit,
15661 'offset' => $offset,
15662 );
15663
15664 return get_sites( $args );
15665 }
15666
15667 /**
15668 * Checks if a given blog is active.
15669 *
15670 * @author Vova Feldman (@svovaf)
15671 * @since 2.0.0
15672 *
15673 * @param $blog_id
15674 *
15675 * @return bool
15676 */
15677 private static function is_site_active( $blog_id ) {
15678 global $wpdb;
15679
15680 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15681
15682 if ( ! is_object( $blog_info ) ) {
15683 return false;
15684 }
15685
15686 return (
15687 true == $blog_info->public &&
15688 false == $blog_info->archived &&
15689 false == $blog_info->mature &&
15690 false == $blog_info->spam &&
15691 false == $blog_info->deleted
15692 );
15693 }
15694
15695 /**
15696 * Get a mapping between the site addresses to their blog IDs.
15697 *
15698 * @author Vova Feldman (@svovaf)
15699 * @since 2.0.0
15700 *
15701 * @return array {
15702 * @key string Site address without protocol with a trailing slash.
15703 * @value int Site's blog ID.
15704 * }
15705 */
15706 private function get_address_to_blog_map() {
15707 $sites = self::get_sites();
15708
15709 // Map site addresses to their blog IDs.
15710 $address_to_blog_map = array();
15711 foreach ( $sites as $site ) {
15712 $blog_id = self::get_site_blog_id( $site );
15713 $address = self::get_unfiltered_site_url( $blog_id, true, true );
15714 $address_to_blog_map[ $address ] = $blog_id;
15715 }
15716
15717 return $address_to_blog_map;
15718 }
15719
15720 /**
15721 * Get a mapping between the site addresses to their blog IDs.
15722 *
15723 * @author Vova Feldman (@svovaf)
15724 * @since 2.0.0
15725 *
15726 * @return array {
15727 * @key int Site's blog ID.
15728 * @value FS_Site Associated install.
15729 * }
15730 */
15731 function get_blog_install_map() {
15732 $sites = self::get_sites();
15733
15734 // Map site blog ID to its install.
15735 $install_map = array();
15736
15737 foreach ( $sites as $site ) {
15738 $blog_id = self::get_site_blog_id( $site );
15739 $install = $this->get_install_by_blog_id( $blog_id );
15740
15741 if ( is_object( $install ) ) {
15742 $install_map[ $blog_id ] = $install;
15743 }
15744 }
15745
15746 return $install_map;
15747 }
15748
15749 /**
15750 * @author Vova Feldman (@svovaf)
15751 * @since 2.5.1
15752 *
15753 * @param bool|null $is_delegated When `true`, returns only connection delegated blog IDs. When `false`, only non-delegated blog IDs.
15754 *
15755 * @return int[]
15756 */
15757 private function get_blog_ids( $is_delegated = null ) {
15758 $blog_ids = array();
15759
15760 $sites = self::get_sites();
15761 foreach ( $sites as $site ) {
15762 $blog_id = self::get_site_blog_id( $site );
15763
15764 if (
15765 is_null( $is_delegated ) ||
15766 $is_delegated === $this->is_site_delegated_connection( $blog_id )
15767 ) {
15768 $blog_ids[] = $blog_id;
15769 }
15770 }
15771
15772 return $blog_ids;
15773 }
15774
15775 /**
15776 * @author Vova Feldman (@svovaf)
15777 * @since 2.5.1
15778 *
15779 * @return int[]
15780 */
15781 private function get_non_delegated_blog_ids() {
15782 return $this->get_blog_ids( false );
15783 }
15784
15785 /**
15786 * Gets a map of module IDs that the given user has opted-in to.
15787 *
15788 * @author Leo Fajardo (@leorw)
15789 * @since 2.1.0
15790 *
15791 * @param number $fs_user_id
15792 *
15793 * @return array {
15794 * @key number $plugin_id
15795 * @value bool Always true.
15796 * }
15797 */
15798 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15799 self::$_static_logger->entrance();
15800
15801 if ( ! is_multisite() ) {
15802 $installs = array_merge(
15803 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15804 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15805 );
15806 } else {
15807 $sites = self::get_sites();
15808
15809 $installs = array();
15810 foreach ( $sites as $site ) {
15811 $blog_id = self::get_site_blog_id( $site );
15812
15813 $installs = array_merge(
15814 $installs,
15815 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15816 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15817 );
15818 }
15819 }
15820
15821 $module_ids_map = array();
15822 foreach ( $installs as $install ) {
15823 if ( is_object( $install ) &&
15824 FS_Site::is_valid_id( $install->id ) &&
15825 FS_User::is_valid_id( $install->user_id ) &&
15826 ( $install->user_id == $fs_user_id )
15827 ) {
15828 $module_ids_map[ $install->plugin_id ] = true;
15829 }
15830 }
15831
15832 return $module_ids_map;
15833 }
15834
15835 /**
15836 * @author Leo Fajardo (@leorw)
15837 *
15838 * @return null|array {
15839 * 'install' => FS_Site Module's install,
15840 * 'blog_id' => string The associated blog ID.
15841 * }
15842 */
15843 function find_first_install() {
15844 $sites = self::get_sites();
15845
15846 foreach ( $sites as $site ) {
15847 $blog_id = self::get_site_blog_id( $site );
15848 $install = $this->get_install_by_blog_id( $blog_id );
15849
15850 if ( is_object( $install ) ) {
15851 return array(
15852 'install' => $install,
15853 'blog_id' => $blog_id
15854 );
15855 }
15856 }
15857
15858 return null;
15859 }
15860
15861 /**
15862 * Switches the Freemius site level context to a specified blog.
15863 *
15864 * @author Vova Feldman (@svovaf)
15865 * @since 2.0.0
15866 *
15867 * @param int $blog_id
15868 * @param FS_Site $install
15869 * @param bool $flush
15870 *
15871 * @return bool Since 2.3.1 returns if a switch was made.
15872 */
15873 function switch_to_blog( $blog_id, FS_Site $install = null, $flush = false ) {
15874 if ( ! is_numeric( $blog_id ) ) {
15875 return false;
15876 }
15877
15878 if ( ! $flush && $blog_id == $this->_context_is_network_or_blog_id ) {
15879 return false;
15880 }
15881
15882 switch_to_blog( $blog_id );
15883 $this->_context_is_network_or_blog_id = $blog_id;
15884
15885 self::$_accounts->set_site_blog_context( $blog_id );
15886 $this->_storage->set_site_blog_context( $blog_id );
15887 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
15888
15889 $this->_site = is_object( $install ) ?
15890 $install :
15891 $this->get_install_by_blog_id( $blog_id );
15892
15893 $this->_user = false;
15894 $this->_licenses = false;
15895 $this->_license = null;
15896 $this->is_whitelabeled = null;
15897
15898 if ( is_object( $this->_site ) ) {
15899 // Try to fetch user from install.
15900 $this->_user = self::_get_user_by_id( $this->_site->user_id );
15901
15902 if ( ! is_object( $this->_user ) &&
15903 FS_User::is_valid_id( $this->_storage->prev_user_id )
15904 ) {
15905 // Try to fetch previously saved user.
15906 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
15907
15908 if ( ! is_object( $this->_user ) ) {
15909 // Fallback to network's user.
15910 $this->_user = $this->get_network_user();
15911 }
15912 }
15913
15914 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
15915
15916 if ( ! empty( $all_plugin_licenses ) ) {
15917 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
15918 $this->_license = null;
15919 } else {
15920 $license_found = false;
15921 foreach ( $all_plugin_licenses as $license ) {
15922 if ( $license->id == $this->_site->license_id ) {
15923 // License found.
15924 $this->_license = $license;
15925 $license_found = true;
15926 break;
15927 }
15928 }
15929
15930 if ( $license_found ) {
15931 $this->link_license_2_user( $this->_license->id, $this->_user->id );
15932 }
15933 }
15934
15935 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15936 }
15937 }
15938
15939 unset( $this->_site_api );
15940 unset( $this->_user_api );
15941
15942 return true;
15943 }
15944
15945 /**
15946 * Restore the blog context to the blog that originally loaded the module.
15947 *
15948 * @author Vova Feldman (@svovaf)
15949 * @since 2.0.0
15950 */
15951 function restore_current_blog() {
15952 $this->switch_to_blog( $this->_blog_id );
15953 }
15954
15955 /**
15956 * @author Vova Feldman (@svovaf)
15957 * @since 2.0.0
15958 *
15959 * @param array|WP_Site $site
15960 *
15961 * @return int
15962 */
15963 static function get_site_blog_id( &$site ) {
15964 return ( $site instanceof WP_Site ) ?
15965 $site->blog_id :
15966 ( is_object( $site ) && isset( $site->userblog_id ) ?
15967 $site->userblog_id :
15968 $site['blog_id'] );
15969 }
15970
15971 /**
15972 * @author Vova Feldman (@svovaf)
15973 * @since 2.5.1
15974 *
15975 * @param WP_Site[]|array[] $sites
15976 *
15977 * @return int[]
15978 */
15979 static function get_sites_blog_ids( $sites ) {
15980 $blog_ids = array();
15981 foreach ( $sites as $site ) {
15982 $blog_ids[] = self::get_site_blog_id( $site );
15983 }
15984
15985 return $blog_ids;
15986 }
15987
15988 /**
15989 * @author Leo Fajardo (@leorw)
15990 * @since 2.0.0
15991 *
15992 * @param array|WP_Site|null $site
15993 * @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.
15994 *
15995 * @return array
15996 */
15997 function get_site_info( $site = null, $load_registration = false ) {
15998 $this->_logger->entrance();
15999
16000 $switched = false;
16001
16002 $registration_date = null;
16003
16004 if ( is_null( $site ) ) {
16005 $url = self::get_unfiltered_site_url();
16006 $name = get_bloginfo( 'name' );
16007 $blog_id = null;
16008 } else {
16009 $blog_id = self::get_site_blog_id( $site );
16010
16011 if ( get_current_blog_id() != $blog_id ) {
16012 switch_to_blog( $blog_id );
16013 $switched = true;
16014 }
16015
16016 if ( $site instanceof WP_Site ) {
16017 $url = $site->siteurl;
16018 $name = $site->blogname;
16019 $registration_date = $site->registered;
16020 } else {
16021 $url = self::get_unfiltered_site_url( $blog_id );
16022 $name = get_bloginfo( 'name' );
16023 }
16024 }
16025
16026 if ( empty( $registration_date ) && $load_registration ) {
16027 $blog_details = get_blog_details( $blog_id, false );
16028
16029 if ( is_object( $blog_details ) && isset( $blog_details->registered ) ) {
16030 $registration_date = $blog_details->registered;
16031 }
16032 }
16033
16034 $info = array(
16035 'uid' => $this->get_anonymous_id( $blog_id ),
16036 'url' => $url,
16037 );
16038
16039 // Add these diagnostic information only if user allowed to track.
16040 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
16041 $info = array_merge( $info, array(
16042 'title' => $name,
16043 'language' => self::get_sanitized_language(),
16044 ) );
16045 }
16046
16047 if ( is_numeric( $blog_id ) ) {
16048 $info['blog_id'] = $blog_id;
16049 }
16050
16051 if ( ! empty( $registration_date ) ) {
16052 $info[ 'registration_date' ] = $registration_date;
16053 }
16054
16055 if ( $switched ) {
16056 restore_current_blog();
16057 }
16058
16059 return $info;
16060 }
16061
16062 /**
16063 * Load the module's install based on the blog ID.
16064 *
16065 * @author Vova Feldman (@svovaf)
16066 * @since 2.0.0
16067 *
16068 * @param int|null $blog_id
16069 *
16070 * @return FS_Site
16071 */
16072 function get_install_by_blog_id( $blog_id = null ) {
16073 $installs = self::get_all_sites( $this->_module_type, $blog_id );
16074 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
16075
16076 if ( is_object( $install ) &&
16077 is_numeric( $install->id ) &&
16078 is_numeric( $install->user_id ) &&
16079 FS_Plugin_Plan::is_valid_id( $install->plan_id )
16080 ) {
16081 // Load site.
16082 $install = clone $install;
16083 }
16084
16085 return $install;
16086 }
16087
16088 /**
16089 * Check if module is installed on a specified site.
16090 *
16091 * @author Vova Feldman (@svovaf)
16092 * @since 2.0.0
16093 *
16094 * @param int|null $blog_id
16095 *
16096 * @return bool
16097 */
16098 function is_installed_on_site( $blog_id = null ) {
16099 $installs = self::get_all_sites( $this->_module_type, $blog_id );
16100 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
16101
16102 return (
16103 is_object( $install ) &&
16104 is_numeric( $install->id ) &&
16105 is_numeric( $install->user_id ) &&
16106 FS_Plugin_Plan::is_valid_id( $install->plan_id )
16107 );
16108 }
16109
16110 /**
16111 * Check if super-admin connected at least one site via the network opt-in.
16112 *
16113 * @author Vova Feldman (@svovaf)
16114 * @since 2.0.0
16115 *
16116 * @return bool
16117 */
16118 function is_network_registered() {
16119 if ( ! $this->_is_network_active ) {
16120 return false;
16121 }
16122
16123 return FS_User::is_valid_id( $this->_storage->network_user_id );
16124 }
16125
16126 /**
16127 * Returns the main user associated with the network.
16128 *
16129 * @author Vova Feldman (@svovaf)
16130 * @since 2.0.0
16131 *
16132 * @return FS_User
16133 */
16134 function get_network_user() {
16135 if ( ! $this->_is_network_active ) {
16136 return null;
16137 }
16138
16139 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
16140 self::_get_user_by_id( $this->_storage->network_user_id ) :
16141 null;
16142 }
16143
16144 /**
16145 * Returns the current context user or the network's main user.
16146 *
16147 * @author Vova Feldman (@svovaf)
16148 * @since 2.0.0
16149 *
16150 * @return FS_User
16151 */
16152 function get_current_or_network_user() {
16153 return ( $this->_user instanceof FS_User ) ?
16154 $this->_user :
16155 $this->get_network_user();
16156 }
16157
16158 /**
16159 * Returns the main install associated with the network.
16160 *
16161 * @author Vova Feldman (@svovaf)
16162 * @since 2.0.0
16163 *
16164 * @return FS_Site
16165 */
16166 function get_network_install() {
16167 if ( ! $this->_is_network_active ) {
16168 return null;
16169 }
16170
16171 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
16172 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
16173 null;
16174 }
16175
16176 /**
16177 * Returns the blog ID that is associated with the main install.
16178 *
16179 * @author Leo Fajardo (@leorw)
16180 * @since 2.0.0
16181 *
16182 * @return int|null
16183 */
16184 function get_network_install_blog_id() {
16185 if ( ! $this->_is_network_active ) {
16186 return null;
16187 }
16188
16189 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
16190 $this->_storage->network_install_blog_id :
16191 null;
16192 }
16193
16194 /**
16195 * Returns the current context install or the network's main install.
16196 *
16197 * @author Vova Feldman (@svovaf)
16198 * @since 2.0.0
16199 *
16200 * @return FS_Site
16201 */
16202 function get_current_or_network_install() {
16203 return ( $this->_site instanceof FS_Site ) ?
16204 $this->_site :
16205 $this->get_network_install();
16206 }
16207
16208 /**
16209 * Check if executing a site level action from the network level admin.
16210 *
16211 * @author Vova Feldman (@svovaf)
16212 * @since 2.0.0
16213 *
16214 * @return false|int If yes, return the requested blog ID.
16215 */
16216 private function is_network_level_site_specific_action() {
16217 if ( ! $this->_is_network_active ) {
16218 return false;
16219 }
16220
16221 if ( ! fs_is_network_admin() ) {
16222 return false;
16223 }
16224
16225 $blog_id = fs_request_get( 'blog_id', '' );
16226
16227 return is_numeric( $blog_id ) ? $blog_id : false;
16228 }
16229
16230 /**
16231 * Check if executing an action from the network level admin.
16232 *
16233 * @author Vova Feldman (@svovaf)
16234 * @since 2.0.0
16235 *
16236 * @return bool
16237 */
16238 private function is_network_level_action() {
16239 return ( $this->_is_network_active && fs_is_network_admin() );
16240 }
16241
16242 /**
16243 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
16244 * 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.
16245 *
16246 * @author Vova Feldman (@svovaf)
16247 * @since 2.0.0
16248 *
16249 * @param int $context_blog_id
16250 */
16251 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
16252 $this->_logger->entrance();
16253
16254 if ( $this->_is_network_active ) {
16255 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
16256 $installs_map = $this->get_blog_install_map();
16257
16258 foreach ( $installs_map as $blog_id => $install ) {
16259 /**
16260 * @var FS_Site $install
16261 */
16262 if ( $context_blog_id == $blog_id ) {
16263 continue;
16264 }
16265
16266 if ( $install->user_id != $this->_storage->network_user_id ) {
16267 continue;
16268 }
16269
16270 // Switch reference to a blog that is opted-in and belong to the same super-admin.
16271 $this->_storage->network_install_blog_id = $blog_id;
16272 break;
16273 }
16274 }
16275 }
16276
16277 if ( ! $this->is_registered() ) {
16278 return;
16279 }
16280
16281 if ( $this->is_sync_cron_scheduled() &&
16282 $context_blog_id == $this->get_sync_cron_blog_id()
16283 ) {
16284 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
16285 }
16286
16287 if ( $this->is_install_sync_scheduled() &&
16288 $context_blog_id == $this->get_install_sync_cron_blog_id()
16289 ) {
16290 $this->schedule_install_sync( $context_blog_id );
16291 }
16292 }
16293
16294 /**
16295 * Executed after site deactivation, archive, or flag as spam.
16296 *
16297 * @author Vova Feldman (@svovaf)
16298 * @since 2.0.0
16299 *
16300 * @param int $context_blog_id
16301 */
16302 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
16303 $this->_logger->entrance();
16304
16305 $install = $this->get_install_by_blog_id( $context_blog_id );
16306
16307 if ( ! is_object( $install ) ) {
16308 // Site not connected.
16309 return;
16310 }
16311
16312 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16313
16314 if ( ! $this->is_registered() ) {
16315 return;
16316 }
16317
16318 $current_blog_id = get_current_blog_id();
16319
16320 $this->switch_to_blog( $context_blog_id );
16321
16322 // Send deactivation event.
16323 $this->sync_install( array(
16324 'is_active' => false,
16325 ) );
16326
16327 $this->switch_to_blog( $current_blog_id );
16328 }
16329
16330 /**
16331 * Executed after site deletion.
16332 *
16333 * @author Vova Feldman (@svovaf)
16334 * @since 2.0.0
16335 *
16336 * @param int $context_blog_id
16337 * @param bool $drop True if site's database tables should be dropped. Default is false.
16338 */
16339 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
16340 $this->_logger->entrance();
16341
16342 $install = $this->get_install_by_blog_id( $context_blog_id );
16343
16344 if ( ! is_object( $install ) ) {
16345 // Site not connected.
16346 return;
16347 }
16348
16349 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16350
16351 if ( ! $this->is_registered() ) {
16352 return;
16353 }
16354
16355 $current_blog_id = get_current_blog_id();
16356
16357 $this->switch_to_blog( $context_blog_id );
16358
16359 if ( $drop ) {
16360 // Delete install if dropping site DB.
16361 $this->delete_account_event();
16362 } else {
16363 // Send deactivation event.
16364 $this->sync_install( array(
16365 'is_active' => false,
16366 ) );
16367 }
16368
16369 $this->switch_to_blog( $current_blog_id );
16370 }
16371
16372 /**
16373 * Executed after site deletion, called from wp_delete_site
16374 *
16375 * @author Dario Curvino (@dudo)
16376 * @since 2.5.0
16377 *
16378 * @param WP_Site $old_site
16379 */
16380 public function _after_wpsite_deleted_callback( WP_Site $old_site ) {
16381 $this->_logger->entrance();
16382
16383 $this->_after_site_deleted_callback( $old_site->blog_id, true );
16384 }
16385
16386 /**
16387 * Executed after site re-activation.
16388 *
16389 * @author Vova Feldman (@svovaf)
16390 * @since 2.0.0
16391 *
16392 * @param int $context_blog_id
16393 */
16394 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
16395 $this->_logger->entrance();
16396
16397 $install = $this->get_install_by_blog_id( $context_blog_id );
16398
16399 if ( ! is_object( $install ) ) {
16400 // Site not connected.
16401 return;
16402 }
16403
16404 if ( ! self::is_site_active( $context_blog_id ) ) {
16405 // Site not yet active (can be in spam mode, archived, deleted...).
16406 return;
16407 }
16408
16409 $current_blog_id = get_current_blog_id();
16410
16411 $this->switch_to_blog( $context_blog_id );
16412
16413 // Send re-activation event.
16414 $this->sync_install( array(
16415 'is_active' => true,
16416 ) );
16417
16418 $this->switch_to_blog( $current_blog_id );
16419 }
16420
16421 #endregion Multisite
16422
16423 /**
16424 * @author Leo Fajardo (@leorw)
16425 *
16426 * @param string $path
16427 * @param string $scheme
16428 * @param bool $network
16429 *
16430 * @return string
16431 */
16432 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
16433 return ( $this->_is_network_active && $network ) ?
16434 network_admin_url( $path, $scheme ) :
16435 admin_url( $path, $scheme );
16436 }
16437
16438 /**
16439 * Check if currently in a specified admin page.
16440 *
16441 * @author Vova Feldman (@svovaf)
16442 * @since 1.2.2.7
16443 *
16444 * @param string $page
16445 *
16446 * @return bool
16447 */
16448 function is_admin_page( $page ) {
16449 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
16450 }
16451
16452 /**
16453 * Check if currently in the product's main admin page.
16454 *
16455 * @author Vova Feldman (@svovaf)
16456 * @since 2.3.1
16457 *
16458 * @return bool
16459 */
16460 function is_main_admin_page() {
16461 return $this->is_admin_page( '' );
16462 }
16463
16464 /**
16465 * Get module's main admin setting page URL.
16466 *
16467 * @author Vova Feldman (@svovaf)
16468 * @since 1.2.2.7
16469 *
16470 * @return string
16471 */
16472 function main_menu_url() {
16473 return $this->_menu->main_menu_url();
16474 }
16475
16476 /**
16477 * Check if currently on the theme's setting page or
16478 * on any of the Freemius added pages (via tabs).
16479 *
16480 * @author Vova Feldman (@svovaf)
16481 * @since 1.2.2.7
16482 *
16483 * @return bool
16484 *
16485 * @deprecated Please use is_product_settings_page() instead;
16486 */
16487 function is_theme_settings_page() {
16488 return $this->is_product_settings_page();
16489 }
16490
16491 /**
16492 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
16493 *
16494 * @author Vova Feldman (@svovaf)
16495 * @since 1.2.2.7
16496 *
16497 * @return bool
16498 */
16499 function is_product_settings_page() {
16500 $page = fs_request_get( 'page', '', 'get' );
16501 $menu_slug = $this->_menu->get_slug();
16502
16503 if ( $page === $menu_slug ) {
16504 return true;
16505 }
16506
16507 return fs_starts_with(
16508 // e.g., {$menu_slug}-account, {$menu_slug}-affiliation, etc.
16509 $page,
16510 ( $menu_slug . '-' )
16511 );
16512 }
16513
16514 /**
16515 * Plugin's account page + sync license URL.
16516 *
16517 * @author Vova Feldman (@svovaf)
16518 * @since 1.1.9.1
16519 *
16520 * @param bool|number $plugin_id
16521 * @param bool $add_action_nonce
16522 * @param array $params
16523 *
16524 * @return string
16525 */
16526 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
16527 if ( is_numeric( $plugin_id ) ) {
16528 $params['plugin_id'] = $plugin_id;
16529 }
16530
16531 return $this->get_account_url(
16532 $this->get_unique_affix() . '_sync_license',
16533 $params,
16534 $add_action_nonce
16535 );
16536 }
16537
16538 /**
16539 * Plugin's account URL.
16540 *
16541 * @author Vova Feldman (@svovaf)
16542 * @since 1.0.4
16543 *
16544 * @param bool|string $action
16545 * @param array $params
16546 *
16547 * @param bool $add_action_nonce
16548 *
16549 * @return string
16550 */
16551 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
16552 if ( is_string( $action ) ) {
16553 $params['fs_action'] = $action;
16554 }
16555
16556 self::require_pluggable_essentials();
16557
16558 return ( $add_action_nonce && is_string( $action ) ) ?
16559 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
16560 $this->_get_admin_page_url( 'account', $params );
16561 }
16562
16563 /**
16564 * @author Vova Feldman (@svovaf)
16565 * @since 1.2.0
16566 *
16567 * @param string $tab
16568 * @param bool $action
16569 * @param array $params
16570 * @param bool $add_action_nonce
16571 *
16572 * @return string
16573 *
16574 * @uses get_account_url()
16575 */
16576 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
16577 $params['tab'] = $tab;
16578
16579 return $this->get_account_url( $action, $params, $add_action_nonce );
16580 }
16581
16582 /**
16583 * Plugin's account URL.
16584 *
16585 * @author Vova Feldman (@svovaf)
16586 * @since 1.0.4
16587 *
16588 * @param bool|string $topic
16589 * @param bool|string $message
16590 * @param bool|string $summary Since 2.5.1.
16591 *
16592 * @return string
16593 */
16594 function contact_url( $topic = false, $message = false, $summary = false ) {
16595 $params = array();
16596 if ( is_string( $topic ) ) {
16597 $params['topic'] = $topic;
16598 }
16599 if ( is_string( $message ) ) {
16600 $params['message'] = $message;
16601 }
16602
16603 if ( is_string( $summary ) ) {
16604 $params['summary'] = $summary;
16605 }
16606
16607 if ( $this->is_addon() ) {
16608 $params['addon_id'] = $this->get_id();
16609
16610 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
16611 } else {
16612 return $this->_get_admin_page_url( 'contact', $params );
16613 }
16614 }
16615
16616 /**
16617 * Add-on direct info URL.
16618 *
16619 * @author Vova Feldman (@svovaf)
16620 * @since 1.1.0
16621 *
16622 * @param string $slug
16623 *
16624 * @return string
16625 */
16626 function addon_url( $slug ) {
16627 return $this->_get_admin_page_url( 'addons', array(
16628 'slug' => $slug
16629 ) );
16630 }
16631
16632 /**
16633 * Add-ons URL.
16634 *
16635 * @author Vova Feldman (@svovaf)
16636 * @since 2.4.5
16637 *
16638 * @return string
16639 */
16640 function get_addons_url() {
16641 return $this->_get_admin_page_url( 'addons' );
16642 }
16643
16644 /* Logger
16645 ------------------------------------------------------------------------------------------------------------------*/
16646 /**
16647 * @param string $id
16648 * @param bool $prefix_slug
16649 *
16650 * @return FS_Logger
16651 */
16652 function get_logger( $id = '', $prefix_slug = true ) {
16653 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16654 }
16655
16656 /**
16657 * Note: This method is used externally so don't delete it.
16658 *
16659 * @param $id
16660 * @param bool $load_options
16661 * @param bool $prefix_slug
16662 *
16663 * @return FS_Option_Manager
16664 */
16665 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16666 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16667 }
16668
16669 /* Security
16670 ------------------------------------------------------------------------------------------------------------------*/
16671 private static function _encrypt( $str ) {
16672 if ( is_null( $str ) ) {
16673 return null;
16674 }
16675
16676 /**
16677 * The encrypt/decrypt functions are used to protect
16678 * the user from messing up with some of the sensitive
16679 * data stored for the module as a JSON in the database.
16680 *
16681 * I used the same suggested hack by the theme review team.
16682 * For more details, look at the function `Base64UrlDecode()`
16683 * in `./sdk/FreemiusBase.php`.
16684 *
16685 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16686 *
16687 * @author Vova Feldman (@svovaf)
16688 * @since 1.2.2
16689 */
16690 $fn = 'base64' . '_encode';
16691
16692 return $fn( $str );
16693 }
16694
16695 static function _decrypt( $str ) {
16696 if ( is_null( $str ) ) {
16697 return null;
16698 }
16699
16700 /**
16701 * The encrypt/decrypt functions are used to protect
16702 * the user from messing up with some of the sensitive
16703 * data stored for the module as a JSON in the database.
16704 *
16705 * I used the same suggested hack by the theme review team.
16706 * For more details, look at the function `Base64UrlDecode()`
16707 * in `./sdk/FreemiusBase.php`.
16708 *
16709 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16710 *
16711 * @author Vova Feldman (@svovaf)
16712 * @since 1.2.2
16713 */
16714 $fn = 'base64' . '_decode';
16715
16716 return $fn( $str );
16717 }
16718
16719 /**
16720 * @author Vova Feldman (@svovaf)
16721 * @since 1.0.5
16722 *
16723 * @param FS_Entity $entity
16724 *
16725 * @return FS_Entity Return an encrypted clone entity.
16726 */
16727 private static function _encrypt_entity( FS_Entity $entity ) {
16728 $clone = clone $entity;
16729 $props = get_object_vars( $entity );
16730
16731 foreach ( $props as $key => $val ) {
16732 $clone->{$key} = self::_encrypt( $val );
16733 }
16734
16735 return $clone;
16736 }
16737
16738 /**
16739 * @author Vova Feldman (@svovaf)
16740 * @since 1.0.5
16741 *
16742 * @param FS_Entity $entity
16743 *
16744 * @return FS_Entity Return an decrypted clone entity.
16745 */
16746 private static function decrypt_entity( FS_Entity $entity ) {
16747 $clone = clone $entity;
16748 $props = get_object_vars( $entity );
16749
16750 foreach ( $props as $key => $val ) {
16751 $clone->{$key} = self::_decrypt( $val );
16752 }
16753
16754 return $clone;
16755 }
16756
16757 /**
16758 * @author Vova Feldman (@svovaf)
16759 * @since 1.0.7
16760 *
16761 * @param string $email
16762 *
16763 * @return FS_User|false
16764 */
16765 public static function _get_user_by_email( $email ) {
16766 self::$_static_logger->entrance();
16767
16768 $email = trim( strtolower( $email ) );
16769
16770 $users = self::get_all_users();
16771
16772 if ( is_array( $users ) ) {
16773 foreach ( $users as $user ) {
16774 if ( $email === trim( strtolower( $user->email ) ) ) {
16775 return $user;
16776 }
16777 }
16778 }
16779
16780 return false;
16781 }
16782
16783 #----------------------------------------------------------------------------------
16784 #region Account (Loading, Updates & Activation)
16785 #----------------------------------------------------------------------------------
16786
16787 /***
16788 * Load account information (user + site).
16789 *
16790 * @author Vova Feldman (@svovaf)
16791 * @since 1.0.1
16792 */
16793 private function _load_account() {
16794 $this->_logger->entrance();
16795
16796 $this->do_action( 'before_account_load' );
16797
16798 $users = self::get_all_users();
16799 $plans = self::get_all_plans( $this->_module_type );
16800
16801 if ( $this->_logger->is_on() && is_admin() ) {
16802 $this->_logger->log( 'users = ' . var_export( $users, true ) );
16803 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
16804 }
16805
16806 $site = fs_is_network_admin() ?
16807 $this->get_network_install() :
16808 $this->get_install_by_blog_id();
16809
16810 if ( fs_is_network_admin() &&
16811 $this->is_network_active() &&
16812 ! is_object( $site ) &&
16813 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
16814 ) {
16815 $first_install = $this->find_first_install();
16816
16817 if ( is_null( $first_install ) ) {
16818 unset( $this->_storage->network_install_blog_id );
16819 } else {
16820 $site = $first_install['install'];
16821 $this->_storage->network_install_blog_id = $first_install['blog_id'];
16822 }
16823 }
16824
16825 if ( is_object( $site ) &&
16826 is_numeric( $site->id ) &&
16827 is_numeric( $site->user_id ) &&
16828 FS_Plugin_Plan::is_valid_id( $site->plan_id )
16829 ) {
16830 // Load site.
16831 $this->_site = $site;
16832 }
16833
16834 $user = null;
16835 if ( fs_is_network_admin() && $this->_is_network_active ) {
16836 $user = $this->get_network_user();
16837 }
16838
16839 if ( is_object( $user ) ) {
16840 $this->_user = clone $user;
16841 } else if ( $this->_site ) {
16842 $user = self::_get_user_by_id( $this->_site->user_id );
16843
16844 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
16845 /**
16846 * Try to load the previous owner. This recovery is used for the following use-case:
16847 * 1. Opt-in
16848 * 2. Cloning site1 to site2
16849 * 3. Ownership switch in site1 (same applies for site2)
16850 * 4. Install data sync on site2
16851 * 5. Now site2's install is associated with the new owner which does not exists locally.
16852 */
16853 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
16854 }
16855
16856 if ( ! is_object( $user ) ) {
16857 /**
16858 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
16859 */
16860 if (
16861 ! isset( $this->_storage->user_recovery_from_install_last_attempt_timestamp ) ||
16862 time() > ( $this->_storage->user_recovery_from_install_last_attempt_timestamp + FS_Clone_Manager::CLONE_RESOLUTION_MAX_EXECUTION_TIME )
16863 ) {
16864 $user = $this->sync_user_by_current_install();
16865 } else {
16866 return;
16867 }
16868
16869 if ( is_object( $user ) ) {
16870 $this->_storage->user_was_recovered_from_install = true;
16871 } else {
16872 $this->_storage->user_recovery_from_install_attempts = isset( $this->_storage->user_recovery_from_install_attempts ) ?
16873 ( $this->_storage->user_recovery_from_install_attempts + 1 ) :
16874 1;
16875
16876 if ( $this->_storage->user_recovery_from_install_attempts >= 3 ) {
16877 $this->delete_current_install( false );
16878 } else {
16879 $this->_storage->user_recovery_from_install_last_attempt_timestamp = time();
16880
16881 return;
16882 }
16883 }
16884 }
16885
16886 $this->_user = ( $user instanceof FS_User ) ?
16887 clone $user :
16888 null;
16889 }
16890
16891 if ( is_object( $this->_user ) ) {
16892 // Load licenses.
16893 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16894 }
16895
16896 if ( is_object( $this->_site ) ) {
16897 // Load plans.
16898 $this->_plans = isset( $plans[ $this->_slug ] ) ?
16899 $plans[ $this->_slug ] :
16900 array();
16901
16902 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
16903 $this->_sync_plans();
16904 } else {
16905 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16906 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
16907 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
16908 } else {
16909 unset( $this->_plans[ $i ] );
16910 }
16911 }
16912 }
16913
16914 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
16915
16916 if ( $this->_site->version != $this->get_plugin_version() ) {
16917 // If stored install version is different than current installed plugin version,
16918 // then update plugin version event.
16919 $this->update_plugin_version_event();
16920 }
16921 }
16922
16923 if ( true === $this->_storage->require_license_activation &&
16924 ! fs_request_get_bool( 'require_license', true )
16925 ) {
16926 $this->_storage->require_license_activation = false;
16927 }
16928
16929 if ( $this->is_theme() ) {
16930 $this->_register_account_hooks();
16931 }
16932
16933 if ( $this->is_user_in_admin() && $this->is_clone() ) {
16934 if ( empty( FS_Clone_Manager::instance()->get_clone_identification_timestamp() ) ) {
16935 FS_Clone_Manager::instance()->store_clone_identification_timestamp();
16936 }
16937
16938 FS_Clone_Manager::instance()->maybe_update_clone_resolution_support_flag( $this->_storage->sdk_last_version );
16939 $this->send_pending_clone_update_once();
16940 }
16941 }
16942
16943 /**
16944 * Special user recovery mechanism.
16945 *
16946 * @author Vova Feldman (@svovaf)
16947 * @since 2.0.0
16948 *
16949 * @param number|null $site_user_id
16950 *
16951 * @return \FS_User|mixed
16952 */
16953 private function sync_user_by_current_install( $site_user_id = null ) {
16954 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
16955 $site_user_id :
16956 $this->_site->user_id;
16957
16958 $api = $this->get_api_site_scope();
16959
16960 $uid = $this->get_anonymous_id();
16961 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
16962
16963 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
16964
16965 if ( $this->is_api_result_entity( $result ) ) {
16966 $user = new FS_User( $result );
16967 $this->_user = $user;
16968 $this->_store_user();
16969
16970 return $user;
16971 }
16972
16973 $error_code = FS_Api::get_error_code( $result );
16974
16975 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
16976 /**
16977 * Those API errors will continue coming and are not recoverable with the
16978 * current site's data. Therefore, extend the API call's cached result to 7 days.
16979 */
16980 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
16981 }
16982
16983 return $result;
16984 }
16985
16986 /**
16987 * @author Vova Feldman (@svovaf)
16988 * @since 1.0.1
16989 *
16990 * @param FS_User $user
16991 * @param FS_Site $site
16992 * @param bool|array $plans
16993 */
16994 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
16995 $site->user_id = $user->id;
16996
16997 $this->_site = $site;
16998 $this->_user = $user;
16999 if ( false !== $plans ) {
17000 $this->_plans = $plans;
17001 }
17002
17003 $this->send_install_update();
17004
17005 $this->_store_account();
17006
17007 }
17008
17009 /**
17010 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
17011 * Each version is trimmed after the 16th char.
17012 *
17013 * @author Vova Feldman (@svovaf)
17014 * @since 2.2.1
17015 *
17016 * @return array
17017 */
17018 private function get_versions() {
17019 $versions = array();
17020 $versions['sdk_version'] = $this->version;
17021
17022 // Collect these diagnostic information only if it's allowed.
17023 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17024 $versions['platform_version'] = get_bloginfo( 'version' );
17025 $versions['programming_language_version'] = phpversion();
17026 }
17027
17028 foreach ( $versions as $k => $version ) {
17029 $versions[ $k ] = self::get_api_sanitized_property( $k, $version );
17030 }
17031
17032 return $versions;
17033 }
17034
17035 /**
17036 * Get sanitized site language.
17037 *
17038 * @param string $language
17039 * @param int $max_len
17040 *
17041 * @since 2.5.1
17042 * @author Vova Feldman (@svovaf)
17043 *
17044 * @return string
17045 */
17046 private static function get_sanitized_language( $language = '', $max_len = self::LANGUAGE_MAX_CHARS ) {
17047 if ( empty( $language ) ) {
17048 $language = get_bloginfo( 'language' );
17049 }
17050
17051 return substr( $language, 0, $max_len );
17052 }
17053
17054 /**
17055 * Get core version stripped from pre-release and build.
17056 *
17057 * @since 2.5.1
17058 * @author Vova Feldman (@svovaf)
17059 *
17060 * @param string $version
17061 * @param int $parts
17062 * @param int $max_len
17063 * @param bool $include_pre_release
17064 *
17065 * @return string
17066 */
17067 private static function get_core_version(
17068 $version,
17069 $parts = 3,
17070 $max_len = self::VERSION_MAX_CHARS,
17071 $include_pre_release = false
17072 ) {
17073 if ( empty( $version ) ) {
17074 // Version is empty.
17075 return '';
17076 }
17077
17078 if ( is_numeric( $version ) ) {
17079 $is_float_version = is_float( $version );
17080
17081 $version = (string) $version;
17082
17083 /**
17084 * 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.
17085 */
17086 if ( $is_float_version && false === strpos( $version, '.' ) ) {
17087 $version .= '.0';
17088 }
17089 }
17090
17091 if ( ! is_string( $version ) ) {
17092 return '';
17093 }
17094
17095 if ( $parts < 1 ) {
17096 return '';
17097 }
17098
17099 $pre_release_regex = $include_pre_release ?
17100 '(\-(alpha|beta|RC)([0-9]+)?)?' :
17101 '';
17102
17103 if ( 0 === preg_match( '/^([0-9]+(\.[0-9]+){0,' . ( $parts - 1 ) . '}' . $pre_release_regex . ')/i', $version, $matches ) ) {
17104 // Version is not starting with a digit.
17105 return '';
17106 }
17107
17108 return substr( $matches[1], 0, $max_len );
17109 }
17110
17111 /**
17112 * @param string $prop
17113 * @param mixed $val
17114 *
17115 * @return mixed
17116 *@author Vova Feldman (@svovaf)
17117 *
17118 * @since 2.5.1
17119 */
17120 private static function get_api_sanitized_property( $prop, $val ) {
17121 if ( ! is_string( $val ) || empty( $val ) ) {
17122 return $val;
17123 }
17124
17125 switch ( $prop ) {
17126 case 'programming_language_version':
17127 // Get core PHP version, which can have up to 3 parts (ignore pre-releases).
17128 return self::get_core_version( $val );
17129 case 'platform_version':
17130 // Get the exact WordPress version, which can have up to 3 parts (including pre-releases).
17131 return self::get_core_version( $val, 3, self::VERSION_MAX_CHARS, true );
17132 case 'sdk_version':
17133 // Get the exact SDK version, which can have up to 4 parts.
17134 return self::get_core_version( $val, 4 );
17135 case 'version':
17136 // Get the entire version but just limited in length.
17137 return substr( $val, 0, self::VERSION_MAX_CHARS );
17138 case 'language':
17139 return self::get_sanitized_language( $val );
17140 default:
17141 return $val;
17142 }
17143 }
17144
17145 /**
17146 * @author Leo Fajardo (@leorw)
17147 * @since 2.3.0
17148 *
17149 * @return bool
17150 */
17151 function has_beta_update() {
17152 return (
17153 ! empty( $this->_storage->beta_data ) &&
17154 ( true === $this->_storage->beta_data['is_beta'] ) &&
17155 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
17156 );
17157 }
17158
17159 /**
17160 * @author Leo Fajardo (@leorw)
17161 * @since 2.3.0
17162 *
17163 * @return bool
17164 */
17165 function is_beta() {
17166 return (
17167 ! empty( $this->_storage->beta_data ) &&
17168 ( true === $this->_storage->beta_data['is_beta'] ) &&
17169 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
17170 );
17171 }
17172
17173 /**
17174 * @author Vova Feldman (@svovaf)
17175 * @since 1.1.7.4
17176 *
17177 * @param array $override_with
17178 * @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.
17179 *
17180 * @return array
17181 */
17182 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
17183 $this->_logger->entrance();
17184
17185 $current_user = self::_get_current_wp_user();
17186
17187 $activation_action = $this->get_unique_affix() . '_activate_new';
17188 $return_url = $this->is_anonymous() ?
17189 // If skipped already, then return to the account page.
17190 $this->get_account_url( $activation_action, array(), false ) :
17191 // Return to the module's main page.
17192 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
17193
17194 $versions = $this->get_versions();
17195
17196 $params = array_merge( $versions, array(
17197 'user_firstname' => $current_user->user_firstname,
17198 'user_lastname' => $current_user->user_lastname,
17199 'user_email' => $current_user->user_email,
17200 'plugin_slug' => $this->_slug,
17201 'plugin_id' => $this->get_id(),
17202 'plugin_public_key' => $this->get_public_key(),
17203 'plugin_version' => $this->get_plugin_version(),
17204 'return_url' => fs_nonce_url( $return_url, $activation_action ),
17205 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
17206 'account',
17207 array( 'fs_action' => 'sync_user' )
17208 ), 'sync_user' ),
17209 'is_premium' => $this->is_premium(),
17210 'is_active' => true,
17211 'is_uninstalled' => false,
17212 'is_localhost' => WP_FS__IS_LOCALHOST,
17213 ) );
17214
17215 if ( $this->is_addon() ) {
17216 $parent_fs = $this->get_parent_instance();
17217
17218 $params['parent_plugin_slug'] = $parent_fs->_slug;
17219 $params['parent_plugin_id'] = $parent_fs->get_id();
17220 }
17221
17222 if ( true === $network_level_or_blog_id ) {
17223 if ( ! isset( $override_with['sites'] ) ) {
17224 $params['sites'] = $this->get_sites_for_network_level_optin();
17225 }
17226 } else {
17227 $site = is_numeric( $network_level_or_blog_id ) ?
17228 array( 'blog_id' => $network_level_or_blog_id ) :
17229 null;
17230
17231 $site = $this->get_site_info( $site );
17232
17233 $diagnostic_info = array();
17234 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17235 $diagnostic_info = array(
17236 'site_name' => $site['title'],
17237 'language' => self::get_sanitized_language( $site['language'] ),
17238 );
17239 }
17240
17241 $params = array_merge( $params, $diagnostic_info, array(
17242 'site_uid' => $site['uid'],
17243 'site_url' => $site['url'],
17244 ) );
17245 }
17246
17247 if ( $this->is_pending_activation() &&
17248 ! empty( $this->_storage->pending_license_key )
17249 ) {
17250 $params['license_key'] = $this->_storage->pending_license_key;
17251 }
17252
17253 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
17254 // Even though rand() is known for its security issues,
17255 // the timestamp adds another layer of protection.
17256 // It would be very hard for an attacker to get the secret key form here.
17257 // Plus, this should never run in production since the secret should never
17258 // be included in the production version.
17259 $params['ts'] = WP_FS__SCRIPT_START_TIME;
17260 $params['salt'] = md5( uniqid( rand() ) );
17261 $params['secure'] = md5(
17262 $params['ts'] .
17263 $params['salt'] .
17264 $this->get_secret_key()
17265 );
17266 }
17267
17268 if ( is_multisite() && function_exists( 'get_network' ) ) {
17269 $params['network_uid'] = $this->get_anonymous_network_id();
17270 }
17271
17272 return array_merge( $params, $override_with );
17273 }
17274
17275 /**
17276 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
17277 * 2. If there was an API error, return the API result.
17278 *
17279 * @author Vova Feldman (@svovaf)
17280 * @since 1.1.7.4
17281 *
17282 * @param string|bool $email
17283 * @param string|bool $first
17284 * @param string|bool $last
17285 * @param string|bool $license_key
17286 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
17287 * In this case, the user and site info will be sent to the server but no
17288 * data will be saved to the WP installation's database.
17289 * @param number|bool $trial_plan_id
17290 * @param bool $is_disconnected Whether to opt in without tracking.
17291 * @param null|bool $is_marketing_allowed
17292 * @param array $sites If network-level opt-in, an array of containing details of sites.
17293 * @param bool $redirect
17294 *
17295 * @return string|object
17296 * @use WP_Error
17297 */
17298 function opt_in(
17299 $email = false,
17300 $first = false,
17301 $last = false,
17302 $license_key = false,
17303 $is_uninstall = false,
17304 $trial_plan_id = false,
17305 $is_disconnected = false,
17306 $is_marketing_allowed = null,
17307 $sites = array(),
17308 $redirect = true
17309 ) {
17310 $this->_logger->entrance();
17311
17312 if ( false === $email ) {
17313 $current_user = self::_get_current_wp_user();
17314 $email = $current_user->user_email;
17315 }
17316
17317 /**
17318 * @since 1.2.1 If activating with license key, ignore the context-user
17319 * since the user will be automatically loaded from the license.
17320 */
17321 if ( empty( $license_key ) ) {
17322 // Clean up pending license if opt-ing in again.
17323 $this->_storage->remove( 'pending_license_key' );
17324
17325 if ( ! $is_uninstall ) {
17326 $fs_user = Freemius::_get_user_by_email( $email );
17327 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
17328 return $this->install_with_user(
17329 $fs_user,
17330 false,
17331 $trial_plan_id,
17332 $redirect,
17333 true,
17334 $sites
17335 );
17336 }
17337 }
17338 }
17339
17340 $user_info = array();
17341 if ( ! empty( $email ) ) {
17342 $user_info['user_email'] = $email;
17343 }
17344 if ( ! empty( $first ) ) {
17345 $user_info['user_firstname'] = $first;
17346 }
17347 if ( ! empty( $last ) ) {
17348 $user_info['user_lastname'] = $last;
17349 }
17350
17351 if ( ! empty( $sites ) ) {
17352 $is_network = true;
17353
17354 $user_info['sites'] = $sites;
17355 } else {
17356 $is_network = false;
17357 }
17358
17359 $params = $this->get_opt_in_params( $user_info, $is_network );
17360
17361 $filtered_license_key = false;
17362 if ( is_string( $license_key ) ) {
17363 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
17364 $params['license_key'] = $filtered_license_key;
17365 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17366 $params['trial_plan_id'] = $trial_plan_id;
17367 }
17368
17369 if ( $is_uninstall ) {
17370 $params['uninstall_params'] = array(
17371 'reason_id' => $this->_storage->uninstall_reason->id,
17372 'reason_info' => $this->_storage->uninstall_reason->info
17373 );
17374 }
17375
17376 if ( isset( $params['license_key'] ) ) {
17377 $fs_user = Freemius::_get_user_by_email( $email );
17378
17379 if ( is_object( $fs_user ) ) {
17380 /**
17381 * If opting in with a context license and the context WP Admin user already opted in
17382 * before from the current site, add the user context security params to avoid the
17383 * unnecessary email activation when the context license is owned by the same context user.
17384 *
17385 * @author Leo Fajardo (@leorw)
17386 * @since 1.2.3
17387 */
17388 $params = array_merge( $params, FS_Security::instance()->get_context_params(
17389 $fs_user,
17390 false,
17391 'install_with_existing_user'
17392 ) );
17393 }
17394 }
17395
17396 if ( is_bool( $is_marketing_allowed ) ) {
17397 $params['is_marketing_allowed'] = $is_marketing_allowed;
17398 }
17399
17400 $params['is_disconnected'] = $is_disconnected;
17401 $params['format'] = 'json';
17402 $params['is_extensions_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed();
17403 $params['is_diagnostic_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed();
17404
17405 $request = array(
17406 'method' => 'POST',
17407 'body' => $params,
17408 'timeout' => 60,
17409 );
17410
17411 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
17412 $response = self::safe_remote_post( $url, $request );
17413
17414 if ( is_wp_error( $response ) ) {
17415 /**
17416 * @var WP_Error $response
17417 */
17418 $result = new stdClass();
17419
17420 $error_code = $response->get_error_code();
17421 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
17422
17423 $result->error = (object) array(
17424 'type' => $error_type,
17425 'message' => $response->get_error_message(),
17426 'code' => $error_code,
17427 'http' => 402
17428 );
17429
17430 $this->maybe_modify_api_curl_error_message( $result );
17431
17432 if ( FS_Api::is_blocked( $result ) ) {
17433 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
17434 }
17435
17436 $is_connected = null;
17437
17438 if ( empty( $license_key ) && $this->is_enable_anonymous() ) {
17439 $this->skip_connection( fs_is_network_admin() );
17440
17441 $is_connected = ( ! FS_Api::is_blocked( $result ) );
17442 }
17443
17444 $this->update_connectivity_info( $is_connected );
17445
17446 return $result;
17447 }
17448
17449 $this->update_connectivity_info( true );
17450
17451 // Module is being uninstalled, don't handle the returned data.
17452 if ( $is_uninstall ) {
17453 return true;
17454 }
17455
17456 /**
17457 * 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.
17458 *
17459 * @author Vova Feldman (@svovaf)
17460 * @since 1.2.3
17461 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
17462 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
17463 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
17464 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
17465 */
17466 $decoded = is_string( $response['body'] ) ?
17467 json_decode( $response['body'] ) :
17468 null;
17469
17470 if ( empty( $decoded ) ) {
17471 return false;
17472 }
17473
17474 if ( ! $this->is_api_result_object( $decoded ) ) {
17475 if ( ! empty( $params['license_key'] ) ) {
17476 // Pass the fully entered license key to the failure handler.
17477 $params['license_key'] = $license_key;
17478 }
17479
17480 return $is_uninstall ?
17481 $decoded :
17482 $this->apply_filters( 'after_install_failure', $decoded, $params );
17483 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
17484 if ( $is_network ) {
17485 $site_ids = array();
17486 foreach ( $sites as $site ) {
17487 $site_ids[] = $site['blog_id'];
17488 }
17489
17490 /**
17491 * Store the sites so that they can be installed once the user has clicked on the activation link
17492 * in the email.
17493 *
17494 * @author Leo Fajardo (@leorw)
17495 */
17496 $this->_storage->pending_sites_info = array(
17497 'blog_ids' => $site_ids,
17498 'license_key' => $license_key,
17499 'trial_plan_id' => $trial_plan_id
17500 );
17501 }
17502
17503 // Pending activation, add message.
17504 return $this->set_pending_confirmation(
17505 ( isset( $decoded->email ) ?
17506 $decoded->email :
17507 true ),
17508 false,
17509 $filtered_license_key,
17510 ! empty( $params['trial_plan_id'] ),
17511 isset( $decoded->is_suspicious_email ) && $decoded->is_suspicious_email
17512 );
17513 } else if ( isset( $decoded->install_secret_key ) ) {
17514 return $this->install_with_new_user(
17515 $decoded->user_id,
17516 $decoded->user_public_key,
17517 $decoded->user_secret_key,
17518 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17519 $decoded->is_marketing_allowed :
17520 null ),
17521 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17522 $decoded->is_extensions_tracking_allowed :
17523 null ),
17524 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17525 $decoded->is_diagnostic_tracking_allowed :
17526 null ),
17527 $decoded->install_id,
17528 $decoded->install_public_key,
17529 $decoded->install_secret_key,
17530 false
17531 );
17532 } else if ( is_array( $decoded->installs ) ) {
17533 return $this->install_many_with_new_user(
17534 $decoded->user_id,
17535 $decoded->user_public_key,
17536 $decoded->user_secret_key,
17537 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17538 $decoded->is_marketing_allowed :
17539 null ),
17540 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17541 $decoded->is_extensions_tracking_allowed :
17542 null ),
17543 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17544 $decoded->is_diagnostic_tracking_allowed :
17545 null ),
17546 $decoded->installs,
17547 false
17548 );
17549 }
17550
17551 return $decoded;
17552 }
17553
17554 /**
17555 * Set user and site identities.
17556 *
17557 * @author Vova Feldman (@svovaf)
17558 * @since 1.0.9
17559 *
17560 * @param FS_User $user
17561 * @param FS_Site $site
17562 * @param bool $redirect
17563 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
17564 * redirect (or return a URL) to the account page with a special parameter to
17565 * trigger the auto installation processes.
17566 *
17567 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17568 */
17569 function setup_account(
17570 FS_User $user,
17571 FS_Site $site,
17572 $redirect = true,
17573 $auto_install = false
17574 ) {
17575 return $this->setup_network_account(
17576 $user,
17577 array( $site ),
17578 $redirect,
17579 $auto_install,
17580 false
17581 );
17582 }
17583
17584 /**
17585 * Set user and site identities.
17586 *
17587 * @author Vova Feldman (@svovaf)
17588 * @since 2.0.0
17589 *
17590 * @param FS_User $user
17591 * @param FS_Site[] $installs
17592 * @param bool $redirect
17593 * @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.
17594 * @param bool $is_network_level_opt_in
17595 *
17596 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17597 */
17598 function setup_network_account(
17599 FS_User $user,
17600 array $installs,
17601 $redirect = true,
17602 $auto_install = false,
17603 $is_network_level_opt_in = true
17604 ) {
17605 $first_install = $installs[0];
17606
17607 $this->_user = $user;
17608 $this->_site = $first_install;
17609
17610 $this->_sync_plans();
17611
17612 if ( $this->_storage->handle_gdpr_admin_notice &&
17613 $this->should_handle_gdpr_admin_notice() &&
17614 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
17615 ) {
17616 /**
17617 * Clear user lock after an opt-in.
17618 */
17619 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
17620 FS_User_Lock::instance()->unlock();
17621 }
17622
17623 if ( 1 < count( $installs ) ) {
17624 // Only network level opt-in can have more than one install.
17625 $is_network_level_opt_in = true;
17626 }
17627
17628 $this->update_connectivity_info( true );
17629
17630 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
17631 // If Freemius was OFF before, turn it on.
17632 $this->turn_on();
17633
17634 $this->handle_account_connection(
17635 $installs,
17636 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
17637 );
17638
17639 if ( is_numeric( $first_install->license_id ) ) {
17640 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
17641 }
17642
17643 $this->_admin_notices->remove_sticky( 'connect_account' );
17644
17645 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
17646 $this->clear_pending_activation_mode();
17647
17648 if ( ! $this->is_paying_or_trial() ) {
17649 $this->_admin_notices->add_sticky(
17650 sprintf( $this->get_text_inline( '%s opt-in was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
17651 'activation_complete'
17652 );
17653 }
17654 }
17655
17656 if ( $this->is_paying_or_trial() ) {
17657 if ( ! $this->is_premium() ||
17658 ! $this->has_premium_version() ||
17659 ! $this->has_settings_menu()
17660 ) {
17661 if ( $this->is_paying() ) {
17662 $this->add_complete_upgrade_instructions_notice(
17663 sprintf(
17664 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
17665 $this->get_plan_title()
17666 ),
17667 'plan_upgraded'
17668 );
17669 } else {
17670 $trial_plan = $this->get_trial_plan();
17671
17672 $this->add_complete_upgrade_instructions_notice(
17673 sprintf(
17674 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
17675 '<i>' . $this->get_plugin_name() . '</i>'
17676 ),
17677 'trial_started',
17678 $trial_plan->title
17679 );
17680 }
17681 }
17682
17683 $this->_admin_notices->remove_sticky( array(
17684 'trial_promotion',
17685 ) );
17686 }
17687
17688 $plugin_id = fs_request_get( 'plugin_id', false );
17689
17690 // Store activation time ONLY for plugins & themes (not add-ons).
17691 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
17692 if ( empty( $this->_storage->activation_timestamp ) ) {
17693 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
17694 }
17695 }
17696
17697 $next_page = '';
17698
17699 $extra = array();
17700 if ( $auto_install ) {
17701 $extra['auto_install'] = 'true';
17702 }
17703
17704 if ( is_numeric( $plugin_id ) ) {
17705 /**
17706 * @author Leo Fajardo (@leorw)
17707 * @since 1.2.1.6
17708 *
17709 * Also sync the license after an anonymous user subscribes.
17710 */
17711 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
17712 // Add-on was installed - sync license right after install.
17713 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
17714 }
17715 } else {
17716 /**
17717 * @author Vova Feldman (@svovaf)
17718 * @since 1.1.9 If site installed with a valid license, sync license.
17719 */
17720 if ( $this->is_paying() ) {
17721 $this->_sync_plugin_license(
17722 true,
17723 // Installs data is already synced in the beginning of this method directly or via _set_account().
17724 false
17725 );
17726 }
17727
17728 // Reload the page with the keys.
17729 $next_page = $this->is_anonymous() ?
17730 // If user previously skipped, redirect to account page.
17731 $this->get_account_url( false, $extra ) :
17732 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
17733 }
17734
17735 if ( ! empty( $next_page ) && $redirect ) {
17736 fs_redirect( $next_page );
17737 }
17738
17739 return $next_page;
17740 }
17741
17742 /**
17743 * Install plugin with new user information after approval.
17744 *
17745 * @author Vova Feldman (@svovaf)
17746 * @since 1.0.7
17747 */
17748 function _install_with_new_user() {
17749 $this->_logger->entrance();
17750
17751 if ( $this->is_registered() ) {
17752 return;
17753 }
17754
17755 $has_pending_activation_confirmation_param = fs_request_has( 'pending_activation' );
17756
17757 $this->update_license_required_permissions_if_anonymous();
17758
17759 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17760 // @todo This logic should be improved because it's executed on every load of a theme.
17761 $this->is_theme()
17762 ) {
17763 // check_admin_referer( $this->_slug . '_activate_new' );
17764
17765 if ( fs_request_has( 'user_secret_key' ) ) {
17766 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17767 $pending_sites_info = $this->_storage->pending_sites_info;
17768
17769 $this->install_many_pending_with_user(
17770 fs_request_get( 'user_id' ),
17771 fs_request_get_raw( 'user_public_key' ),
17772 fs_request_get_raw( 'user_secret_key' ),
17773 fs_request_get_bool( 'is_marketing_allowed', null ),
17774 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17775 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17776 $pending_sites_info['blog_ids'],
17777 $pending_sites_info['license_key'],
17778 $pending_sites_info['trial_plan_id']
17779 );
17780 } else {
17781 $this->install_with_new_user(
17782 fs_request_get( 'user_id' ),
17783 fs_request_get_raw( 'user_public_key' ),
17784 fs_request_get_raw( 'user_secret_key' ),
17785 fs_request_get_bool( 'is_marketing_allowed', null ),
17786 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17787 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17788 fs_request_get( 'install_id' ),
17789 fs_request_get_raw( 'install_public_key' ),
17790 fs_request_get_raw( 'install_secret_key' ),
17791 true,
17792 fs_request_get_bool( 'auto_install' )
17793 );
17794 }
17795 } else if ( $has_pending_activation_confirmation_param ) {
17796 $this->set_pending_confirmation(
17797 fs_request_get( 'user_email' ),
17798 true,
17799 false,
17800 false,
17801 fs_request_get_bool( 'is_suspicious_email' ),
17802 fs_request_get_bool( 'has_upgrade_context' ),
17803 fs_request_get( 'support_email_address' )
17804 );
17805 }
17806 }
17807 }
17808
17809 /**
17810 * @author Vova Feldman (@svovaf)
17811 * @since 2.0.0
17812 *
17813 * @param number $id
17814 * @param string $public_key
17815 * @param string $secret_key
17816 *
17817 * @return \FS_User
17818 */
17819 private function setup_user( $id, $public_key, $secret_key ) {
17820 $user = self::_get_user_by_id( $id );
17821
17822 if ( is_object( $user ) ) {
17823 $this->_user = $user;
17824 } else {
17825 $user = new FS_User();
17826 $user->id = $id;
17827 $user->public_key = $public_key;
17828 $user->secret_key = $secret_key;
17829
17830 $this->_user = $user;
17831 $user_result = $this->get_api_user_scope()->get();
17832 $user = new FS_User( $user_result );
17833
17834 $this->_user = $user;
17835 $this->_store_user();
17836 }
17837
17838 return $user;
17839 }
17840
17841 /**
17842 * Install plugin with new user.
17843 *
17844 * @author Vova Feldman (@svovaf)
17845 * @since 1.1.7.4
17846 *
17847 * @param number $user_id
17848 * @param string $user_public_key
17849 * @param string $user_secret_key
17850 * @param bool|null $is_marketing_allowed
17851 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17852 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17853 * @param number $install_id
17854 * @param string $install_public_key
17855 * @param string $install_secret_key
17856 * @param bool $redirect
17857 * @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.
17858 *
17859 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17860 */
17861 private function install_with_new_user(
17862 $user_id,
17863 $user_public_key,
17864 $user_secret_key,
17865 $is_marketing_allowed,
17866 $is_extensions_tracking_allowed,
17867 $is_diagnostic_tracking_allowed,
17868 $install_id,
17869 $install_public_key,
17870 $install_secret_key,
17871 $redirect = true,
17872 $auto_install = false
17873 ) {
17874 /**
17875 * This method is also executed after opting in with a license key since the
17876 * license can be potentially associated with a different owner.
17877 *
17878 * @since 2.0.0
17879 */
17880 $user = self::_get_user_by_id( $user_id );
17881
17882 if ( ! is_object( $user ) ) {
17883 $user = new FS_User();
17884 $user->id = $user_id;
17885 $user->public_key = $user_public_key;
17886 $user->secret_key = $user_secret_key;
17887
17888 $this->_user = $user;
17889 $user_result = $this->get_api_user_scope()->get();
17890 $user = new FS_User( $user_result );
17891 }
17892
17893 $this->_user = $user;
17894
17895 $site = new FS_Site();
17896 $site->id = $install_id;
17897 $site->public_key = $install_public_key;
17898 $site->secret_key = $install_secret_key;
17899
17900 $this->_site = $site;
17901 $site_result = $this->get_api_site_scope( true )->get();
17902 $site = new FS_Site( $site_result );
17903 $this->_site = $site;
17904
17905 if ( ! is_null( $is_marketing_allowed ) ) {
17906 $this->disable_opt_in_notice_and_lock_user();
17907 }
17908
17909 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17910 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17911 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17912 ) );
17913
17914 return $this->setup_account(
17915 $this->_user,
17916 $this->_site,
17917 $redirect,
17918 $auto_install
17919 );
17920 }
17921
17922 /**
17923 * Install plugin with user.
17924 *
17925 * @author Leo Fajardo (@leorw)
17926 * @since 2.0.0
17927 *
17928 * @param number $user_id
17929 * @param string $user_public_key
17930 * @param string $user_secret_key
17931 * @param bool|null $is_marketing_allowed
17932 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17933 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17934 * @param array $site_ids
17935 * @param bool $license_key
17936 * @param bool $trial_plan_id
17937 * @param bool $redirect
17938 *
17939 * @return void
17940 */
17941 private function install_many_pending_with_user(
17942 $user_id,
17943 $user_public_key,
17944 $user_secret_key,
17945 $is_marketing_allowed,
17946 $is_extensions_tracking_allowed,
17947 $is_diagnostic_tracking_allowed,
17948 $site_ids,
17949 $license_key = false,
17950 $trial_plan_id = false,
17951 $redirect = true
17952 ) {
17953 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17954
17955 if ( ! is_null( $is_marketing_allowed ) ) {
17956 $this->disable_opt_in_notice_and_lock_user();
17957 }
17958
17959 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17960 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17961 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17962 ) );
17963
17964 $sites = array();
17965 foreach ( $site_ids as $site_id ) {
17966 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
17967 }
17968
17969 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17970 }
17971
17972 /**
17973 * Multi-site install with a new user.
17974 *
17975 * @author Vova Feldman (@svovaf)
17976 * @since 2.0.0
17977 *
17978 * @param number $user_id
17979 * @param string $user_public_key
17980 * @param string $user_secret_key
17981 * @param bool|null $is_marketing_allowed
17982 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17983 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17984 * @param object[] $installs
17985 * @param bool $redirect
17986 * @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.
17987 *
17988 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17989 */
17990 private function install_many_with_new_user(
17991 $user_id,
17992 $user_public_key,
17993 $user_secret_key,
17994 $is_marketing_allowed,
17995 $is_extensions_tracking_allowed,
17996 $is_diagnostic_tracking_allowed,
17997 array $installs,
17998 $redirect = true,
17999 $auto_install = false
18000 ) {
18001 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
18002
18003 if ( ! is_null( $is_marketing_allowed ) ) {
18004 $this->disable_opt_in_notice_and_lock_user();
18005 }
18006
18007 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
18008 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
18009 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
18010 ) );
18011
18012 $install_ids = array();
18013
18014 foreach ( $installs as $install ) {
18015 $install_ids[] = $install->id;
18016 }
18017
18018 $items_per_request = 25;
18019 $left = count( $install_ids );
18020 $offset = 0;
18021
18022 $installs = array();
18023 while ( $left > 0 ) {
18024 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, $items_per_request ) ) );
18025
18026 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18027 // @todo Handle API error.
18028 }
18029
18030 $installs = array_merge( $installs, $result->installs );
18031
18032 $left -= $items_per_request;
18033 $offset += $items_per_request;
18034 }
18035
18036 foreach ( $installs as &$install ) {
18037 $install = new FS_Site( $install );
18038 }
18039
18040 return $this->setup_network_account(
18041 $this->_user,
18042 $installs,
18043 $redirect,
18044 $auto_install
18045 );
18046 }
18047
18048 /**
18049 * @author Vova Feldman (@svovaf)
18050 * @since 1.1.7.4
18051 *
18052 * @param string|bool $email
18053 * @param bool $redirect
18054 * @param string|bool $license_key Since 1.2.1.5
18055 * @param bool $is_pending_trial Since 1.2.1.5
18056 * @param bool $is_suspicious_email Since 2.5.0
18057 * @param bool $has_upgrade_context Since 2.5.3
18058 * @param bool|string $support_email_address Since 2.5.3
18059 *
18060 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
18061 */
18062 private function set_pending_confirmation(
18063 $email = false,
18064 $redirect = true,
18065 $license_key = false,
18066 $is_pending_trial = false,
18067 $is_suspicious_email = false,
18068 $has_upgrade_context = false,
18069 $support_email_address = false
18070 ) {
18071 $is_network_admin = fs_is_network_admin();
18072
18073 if ( $this->_ignore_pending_mode && ! $has_upgrade_context ) {
18074 /**
18075 * If explicitly asked to ignore pending mode, set to anonymous mode
18076 * 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).
18077 *
18078 * @author Vova Feldman
18079 * @since 1.2.1.6
18080 */
18081 $this->skip_connection( $is_network_admin );
18082 } else {
18083 // Install must be activated via email since
18084 // user with the same email already exist.
18085 $this->_storage->is_pending_activation = true;
18086 $this->_add_pending_activation_notice(
18087 $email,
18088 $is_pending_trial,
18089 $is_suspicious_email,
18090 $has_upgrade_context,
18091 $support_email_address
18092 );
18093 }
18094
18095 if ( ! empty( $license_key ) ) {
18096 $this->_storage->pending_license_key = $license_key;
18097 }
18098
18099 // Remove the opt-in sticky notice.
18100 $this->_admin_notices->remove_sticky( array(
18101 'connect_account',
18102 'trial_promotion',
18103 ) );
18104
18105 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
18106
18107 if ( $redirect ) {
18108 // Reload the page with a pending activation message.
18109 fs_redirect( $next_page );
18110 }
18111
18112 return $next_page;
18113 }
18114
18115 /**
18116 * Install plugin with current logged WP user info.
18117 *
18118 * @author Vova Feldman (@svovaf)
18119 * @since 1.0.7
18120 */
18121 function _install_with_current_user() {
18122 $this->_logger->entrance();
18123
18124 if ( $this->is_registered() ) {
18125 return;
18126 }
18127
18128 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
18129 check_admin_referer( $this->get_unique_affix() . '_activate_existing' );
18130
18131 /**
18132 * @author Vova Feldman (@svovaf)
18133 * @since 1.1.9 Add license key if given.
18134 */
18135 $license_key = fs_request_get_raw( 'license_secret_key' );
18136
18137 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
18138 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
18139 FS_Permission_Manager::PERMISSION_EXTENSIONS => fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
18140 ) );
18141
18142 $this->install_with_current_user( $license_key );
18143 }
18144 }
18145
18146
18147 /**
18148 * @author Vova Feldman (@svovaf)
18149 * @since 1.1.7.4
18150 *
18151 * @param string|bool $license_key
18152 * @param number|bool $trial_plan_id
18153 * @param array $sites Since 2.0.0
18154 * @param bool $redirect
18155 *
18156 * @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.
18157 */
18158 function install_with_current_user(
18159 $license_key = false,
18160 $trial_plan_id = false,
18161 $sites = array(),
18162 $redirect = true
18163 ) {
18164 // Get current logged WP user.
18165 $current_user = self::_get_current_wp_user();
18166
18167 // Find the relevant FS user by the email.
18168 $user = self::_get_user_by_email( $current_user->user_email );
18169
18170 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
18171 }
18172
18173 /**
18174 * @author Vova Feldman (@svovaf)
18175 * @since 2.0.0
18176 *
18177 * @param \FS_User $user
18178 * @param string|bool $license_key
18179 * @param number|bool $trial_plan_id
18180 * @param bool $redirect
18181 * @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.
18182 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
18183 *
18184 * @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.
18185 */
18186 function install_with_user(
18187 FS_User $user,
18188 $license_key = false,
18189 $trial_plan_id = false,
18190 $redirect = true,
18191 $setup_account = true,
18192 $sites = array()
18193 ) {
18194 // We have to set the user before getting user scope API handler.
18195 $this->_user = $user;
18196
18197 // Install the plugin.
18198 $result = $this->create_installs_with_user(
18199 $user,
18200 $license_key,
18201 $trial_plan_id,
18202 $sites,
18203 $redirect
18204 );
18205
18206 if ( ! $this->is_api_result_entity( $result ) &&
18207 ! $this->is_api_result_object( $result, 'installs' )
18208 ) {
18209 // @todo Handler potential API error of the $result
18210 }
18211
18212 if ( empty( $sites ) ) {
18213 $site = new FS_Site( $result );
18214 $this->_site = $site;
18215
18216 if ( ! $setup_account ) {
18217 $this->_store_site();
18218
18219 $this->sync_plan_if_not_exist( $site->plan_id );
18220
18221 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
18222 $this->sync_license_if_not_exist( $site->license_id, $license_key );
18223 }
18224
18225 $this->_admin_notices->remove_sticky( 'connect_account', false );
18226
18227 return $site;
18228 }
18229
18230 return $this->setup_account( $this->_user, $this->_site, $redirect );
18231 } else {
18232 $installs = array();
18233 foreach ( $result->installs as $install ) {
18234 $installs[] = new FS_Site( $install );
18235 }
18236
18237 return $this->setup_network_account(
18238 $user,
18239 $installs,
18240 $redirect
18241 );
18242 }
18243 }
18244
18245 /**
18246 * Initiate an API request to create a collection of installs.
18247 *
18248 * @author Vova Feldman (@svovaf)
18249 * @since 2.0.0
18250 *
18251 * @param \FS_User $user
18252 * @param bool $license_key
18253 * @param bool $trial_plan_id
18254 * @param array $sites
18255 * @param bool $redirect
18256 * @param bool $silent
18257 *
18258 * @return object|mixed
18259 */
18260 private function create_installs_with_user(
18261 FS_User $user,
18262 $license_key = false,
18263 $trial_plan_id = false,
18264 $sites = array(),
18265 $redirect = false,
18266 $silent = false
18267 ) {
18268 $extra_install_params = array(
18269 'uid' => $this->get_anonymous_id(),
18270 'is_disconnected' => false,
18271 );
18272
18273 if ( ! empty( $license_key ) ) {
18274 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
18275
18276 if ( $silent ) {
18277 $extra_install_params['ignore_license_owner'] = true;
18278 }
18279 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
18280 $extra_install_params['trial_plan_id'] = $trial_plan_id;
18281 }
18282
18283 if ( ! empty( $sites ) ) {
18284 $extra_install_params['sites'] = $sites;
18285 }
18286
18287 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
18288
18289 // Install the plugin.
18290 $result = $this->get_api_user_scope_by_user( $user )->call(
18291 "/plugins/{$this->get_id()}/installs.json",
18292 'post',
18293 $args
18294 );
18295
18296 if ( ! $this->is_api_result_entity( $result ) &&
18297 ! $this->is_api_result_object( $result, 'installs' )
18298 ) {
18299 if ( ! empty( $args['license_key'] ) ) {
18300 // Pass the fully entered license key to the failure handler.
18301 $args['license_key'] = $license_key;
18302 }
18303
18304 $result = $this->apply_filters( 'after_install_failure', $result, $args );
18305
18306 if ( ! $silent ) {
18307 $this->_admin_notices->add(
18308 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18309 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
18310 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18311 'error'
18312 );
18313 }
18314
18315 if ( $redirect ) {
18316 /**
18317 * We set the user before getting the user scope API handler, so the user became temporarily
18318 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
18319 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
18320 * activation page based on the return value of `is_registered()`. In addition, in case the
18321 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
18322 * misleading plugin activation errors will be shown on the `Plugins` page.
18323 *
18324 * @author Leo Fajardo (@leorw)
18325 */
18326 $this->_user = null;
18327
18328 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
18329 }
18330 }
18331
18332 return $result;
18333 }
18334
18335 /**
18336 * Tries to activate add-on account based on parent plugin info.
18337 *
18338 * @author Vova Feldman (@svovaf)
18339 * @since 1.0.6
18340 *
18341 * @param Freemius $parent_fs
18342 * @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.
18343 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
18344 */
18345 private function _activate_addon_account(
18346 Freemius $parent_fs,
18347 $network_level_or_blog_id = null,
18348 FS_Plugin_License $bundle_license = null
18349 ) {
18350 if ( $this->is_registered() ) {
18351 // Already activated.
18352 return;
18353 }
18354
18355 $permission_ids = FS_Permission_Manager::get_all_permission_ids();
18356 $permissions = array();
18357 foreach ( $permission_ids as $permission_id ) {
18358 $permissions[ $permission_id ] = FS_Permission_Manager::instance( $parent_fs )->is_permission( $permission_id, true );
18359 }
18360
18361 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( $permissions );
18362
18363 /**
18364 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
18365 * already returns the data for the current blog.
18366 *
18367 * @author Leo Fajardo (@leorw)
18368 * @since 2.3.0
18369 */
18370 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
18371 array() :
18372 array( 'uid' => $this->get_anonymous_id() );
18373
18374 $params = $this->get_install_data_for_api(
18375 $uid_param_to_override,
18376 false,
18377 false,
18378 /**
18379 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
18380 * already includes the data for it.
18381 *
18382 * @author Leo Fajardo (@leorw)
18383 * @since 2.3.0
18384 */
18385 ( true !== $network_level_or_blog_id )
18386 );
18387
18388 if ( true === $network_level_or_blog_id ) {
18389 $params['sites'] = $this->get_sites_for_network_level_optin();
18390
18391 if ( empty( $params['sites'] ) ) {
18392 return;
18393 }
18394 }
18395
18396 if ( is_object( $bundle_license ) ) {
18397 $params['license_key'] = $bundle_license->secret_key;
18398 }
18399
18400 // Activate add-on with parent plugin credentials.
18401 $result = $parent_fs->get_api_site_scope()->call(
18402 "/addons/{$this->_plugin->id}/installs.json",
18403 'post',
18404 $params
18405 );
18406
18407 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18408 if ( is_object( $bundle_license ) ) {
18409 /**
18410 * 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.
18411 *
18412 * @author Leo Fajardo (@leorw)
18413 * @since 2.4.0
18414 */
18415 } else {
18416 $error_message = FS_Api::is_api_error_object( $result ) ?
18417 $result->error->message :
18418 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
18419
18420 $this->_admin_notices->add(
18421 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18422 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
18423 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18424 'error'
18425 );
18426 }
18427
18428 return;
18429 }
18430
18431 $addon_installs = $result->installs;
18432 foreach ( $addon_installs as $key => $addon_install ) {
18433 $addon_installs[ $key ] = new FS_Site( $addon_install );
18434 }
18435
18436 $first_install = $addon_installs[0];
18437
18438 // Get user information based on parent's plugin.
18439 $user = $parent_fs->get_user();
18440
18441 // First of all, set site and user info - otherwise we won't
18442 // be able to invoke API calls.
18443 $this->_site = $first_install;
18444 $this->_user = $user;
18445
18446 // Sync add-on plans.
18447 $this->_sync_plans();
18448
18449 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
18450
18451 // Get site's current plan.
18452 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
18453
18454 // Sync licenses.
18455 $this->_sync_licenses();
18456
18457 if ( ! fs_is_network_admin() ) {
18458 // Try to activate premium license.
18459 $this->_activate_license( true, $bundle_license );
18460
18461 if ( is_object( $bundle_license ) ) {
18462 $this->maybe_activate_bundle_license( $bundle_license );
18463 }
18464 } else {
18465 if ( is_object( $bundle_license ) ) {
18466 $premium_license = $bundle_license;
18467 } else {
18468 $license_id = fs_request_get( 'license_id' );
18469
18470 if ( is_object( $this->_site ) &&
18471 FS_Plugin_License::is_valid_id( $license_id ) &&
18472 $license_id == $this->_site->license_id
18473 ) {
18474 // License is already activated.
18475 return;
18476 }
18477
18478 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
18479 $this->_get_license_by_id( $license_id ) :
18480 $this->_get_available_premium_license();
18481 }
18482
18483 if ( is_object( $premium_license ) ) {
18484 $this->maybe_network_activate_addon_license( $premium_license );
18485 }
18486 }
18487 }
18488
18489 /**
18490 * @author Leo Fajardo (@leorw)
18491 * @since 2.3.0
18492 *
18493 * @param FS_Site[] $installs
18494 * @param bool $is_site_level
18495 */
18496 private function handle_account_connection( $installs, $is_site_level ) {
18497 $first_install = $installs[0];
18498
18499 if ( $is_site_level ) {
18500 $this->_set_account( $this->_user, $first_install );
18501
18502 $this->do_action( 'after_account_connection', $this->_user, $first_install );
18503 } else {
18504 $this->_store_user();
18505
18506 // Map site addresses to their blog IDs.
18507 $address_to_blog_map = $this->get_address_to_blog_map();
18508
18509 $first_blog_id = null;
18510 $blog_2_install_map = array();
18511 foreach ( $installs as $install ) {
18512 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
18513 $blog_id = $address_to_blog_map[ $address ];
18514
18515 $this->_store_site( true, $blog_id, $install );
18516
18517 if ( is_null( $first_blog_id ) ) {
18518 $first_blog_id = $blog_id;
18519 }
18520
18521 $blog_2_install_map[ $blog_id ] = $install;
18522 }
18523
18524 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
18525 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
18526 ) {
18527 // Store network user.
18528 $this->_storage->network_user_id = $this->_user->id;
18529 }
18530
18531 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
18532 $this->_storage->network_install_blog_id = $first_blog_id;
18533 }
18534
18535 if ( count( $installs ) === count( $address_to_blog_map ) ) {
18536 // Super admin opted in for all sites in the network.
18537 $this->_storage->is_network_connected = true;
18538 }
18539
18540 $this->_store_licenses( false );
18541
18542 self::$_accounts->store();
18543
18544 // Don't sync the installs data on network upgrade
18545 if ( ! $this->network_upgrade_mode_completed() ) {
18546 $this->send_installs_update();
18547 }
18548
18549 $current_blog = get_current_blog_id();
18550
18551 foreach ( $blog_2_install_map as $blog_id => $install ) {
18552 $this->switch_to_blog( $blog_id );
18553
18554 $this->do_action( 'after_account_connection', $this->_user, $install );
18555 }
18556
18557 // Switch install context back to the first install.
18558 $this->switch_to_blog(
18559 $current_blog,
18560 $first_install,
18561 ( $this->_site->id != $first_install->id )
18562 );
18563
18564 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
18565 }
18566 }
18567
18568 /**
18569 * Tries to activate parent account based on add-on's info.
18570 *
18571 * @author Vova Feldman (@svovaf)
18572 * @since 1.2.2.7
18573 *
18574 * @param Freemius $parent_fs
18575 */
18576 private function activate_parent_account( Freemius $parent_fs ) {
18577 if ( ! $this->is_addon() ) {
18578 // This is not an add-on.
18579 return;
18580 }
18581
18582 if ( $parent_fs->is_registered() ) {
18583 // Already activated.
18584 return;
18585 }
18586
18587 // Activate parent with add-on's user credentials.
18588 $parent_install = $this->get_api_user_scope()->call(
18589 "/plugins/{$parent_fs->_plugin->id}/installs.json",
18590 'post',
18591 $parent_fs->get_install_data_for_api( array(
18592 'uid' => $parent_fs->get_anonymous_id(),
18593 ), false, false )
18594 );
18595
18596 if ( isset( $parent_install->error ) ) {
18597 $this->_admin_notices->add(
18598 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18599 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
18600 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18601 'error'
18602 );
18603
18604 return;
18605 }
18606
18607 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
18608
18609 if ( $parent_fs->is_pending_activation() ) {
18610 $parent_fs->clear_pending_activation_mode();
18611 }
18612
18613 // Get user information based on parent's plugin.
18614 $user = $this->get_user();
18615
18616 // First of all, set site info - otherwise we won't
18617 // be able to invoke API calls.
18618 $parent_fs->_site = new FS_Site( $parent_install );
18619 $parent_fs->_user = $user;
18620
18621 // Sync add-on plans.
18622 $parent_fs->_sync_plans();
18623
18624 $parent_fs->update_license_required_permissions_if_anonymous();
18625
18626 $parent_fs->_set_account( $user, $parent_fs->_site );
18627 }
18628
18629 #endregion
18630
18631 #----------------------------------------------------------------------------------
18632 #region Admin Menu Items
18633 #----------------------------------------------------------------------------------
18634
18635 private $_menu_items = array();
18636
18637 /**
18638 * @author Vova Feldman (@svovaf)
18639 * @since 1.2.1.8
18640 *
18641 * @return array
18642 */
18643 function get_menu_items() {
18644 return $this->_menu_items;
18645 }
18646
18647 /**
18648 * @author Vova Feldman (@svovaf)
18649 * @since 1.0.7
18650 *
18651 * @return string
18652 */
18653 function get_menu_slug() {
18654 return $this->_menu->get_slug();
18655 }
18656
18657 /**
18658 * @author Vova Feldman (@svovaf)
18659 * @since 1.0.9
18660 */
18661 function _prepare_admin_menu() {
18662 // if ( ! $this->is_on() ) {
18663 // return;
18664 // }
18665
18666 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
18667 return;
18668 }
18669
18670 /**
18671 * When running from a site admin with a network activated module and the connection
18672 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
18673 * site level settings.
18674 *
18675 * @author Vova Feldman (@svovaf)
18676 * @since 2.0.0
18677 */
18678 $should_hide_site_admin_settings = (
18679 $this->_is_network_active &&
18680 ! fs_is_network_admin() &&
18681 ! $this->is_delegated_connection() &&
18682 ! $this->is_anonymous() &&
18683 ! $this->is_registered()
18684 );
18685
18686 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
18687
18688 if ( ( false === $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
18689 $should_hide_site_admin_settings
18690 ) {
18691 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
18692 } else {
18693 $this->do_action( fs_is_network_admin() ?
18694 'before_network_admin_menu_init' :
18695 'before_admin_menu_init'
18696 );
18697
18698 $this->add_menu_action();
18699
18700 $this->add_network_menu_when_missing();
18701
18702 $this->add_submenu_items();
18703 }
18704 }
18705
18706 /**
18707 * Admin dashboard menu items modifications.
18708 *
18709 * NOTE: admin_menu action executed before admin_init.
18710 *
18711 * @author Vova Feldman (@svovaf)
18712 * @since 1.0.7
18713 *
18714 */
18715 private function add_menu_action() {
18716 if ( $this->is_activation_mode() ) {
18717 if ( $this->show_opt_in_on_setting_page() ) {
18718 $this->override_plugin_menu_with_activation();
18719 } else {
18720 /**
18721 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
18722 */
18723 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18724 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
18725 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
18726 fs_request_get_bool( 'pending_activation' )
18727 ) {
18728 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
18729 }
18730 }
18731 } else {
18732 if ( ! $this->is_registered() ) {
18733 // If not registered try to install user.
18734 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18735 $this->_install_with_new_user();
18736 }
18737 } else if (
18738 fs_request_is_action( 'sync_user' ) &&
18739 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
18740 ) {
18741 $this->_handle_account_user_sync();
18742 }
18743 }
18744 }
18745
18746 /**
18747 * @author Vova Feldman (@svovaf)
18748 * @since 1.0.1
18749 */
18750 function _redirect_on_clicked_menu_link() {
18751 $this->_logger->entrance();
18752
18753 $page = fs_request_get('page');
18754 $page = is_string($page) ? strtolower($page) : '';
18755
18756 $this->_logger->log( 'page = ' . $page );
18757
18758 foreach ( $this->_menu_items as $priority => $items ) {
18759 foreach ( $items as $item ) {
18760 if ( isset( $item['url'] ) ) {
18761 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
18762 $this->_logger->log( 'Redirecting to ' . $item['url'] );
18763
18764 fs_redirect( $item['url'] );
18765 }
18766 }
18767 }
18768 }
18769 }
18770
18771 /**
18772 * Remove plugin's all admin menu items & pages, and replace with activation page.
18773 *
18774 * @author Vova Feldman (@svovaf)
18775 * @since 1.0.1
18776 */
18777 private function override_plugin_menu_with_activation() {
18778 $this->_logger->entrance();
18779
18780 $hook = false;
18781
18782 if ( ! $this->has_settings_menu() ) {
18783 // Add the opt-in page without a menu item.
18784 $hook = FS_Admin_Menu_Manager::add_subpage(
18785 '',
18786 $this->get_plugin_name(),
18787 $this->get_plugin_name(),
18788 'manage_options',
18789 $this->_slug,
18790 array( &$this, '_connect_page_render' )
18791 );
18792 } else if ( $this->_menu->is_top_level() ) {
18793 if ( $this->_menu->is_override_exact() ) {
18794 // Make sure the current page is matching the activation page.
18795 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18796 return;
18797 }
18798 }
18799
18800 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18801
18802 if ( false === $hook ) {
18803 // Create new menu item just for the opt-in.
18804 $hook = FS_Admin_Menu_Manager::add_page(
18805 $this->get_plugin_name(),
18806 $this->get_plugin_name(),
18807 'manage_options',
18808 $this->_menu->get_slug(),
18809 array( &$this, '_connect_page_render' )
18810 );
18811 }
18812 } else {
18813 $menus = array( $this->_menu->get_parent_slug() );
18814
18815 if ( $this->_menu->is_override_exact() ) {
18816 // Make sure the current page is matching the activation page.
18817 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18818 return;
18819 }
18820 }
18821
18822 foreach ( $menus as $parent_slug ) {
18823 $hook = $this->_menu->override_submenu_action(
18824 $parent_slug,
18825 $this->_menu->get_raw_slug(),
18826 array( &$this, '_connect_page_render' )
18827 );
18828
18829 if ( false !== $hook ) {
18830 // Found plugin's submenu item.
18831 break;
18832 }
18833 }
18834 }
18835
18836 if ( $this->is_activation_page() ) {
18837 // Clean admin page from distracting content.
18838 self::_clean_admin_content_section();
18839 }
18840
18841 if ( false !== $hook ) {
18842 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18843 $this->_install_with_current_user();
18844 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18845 $this->_install_with_new_user();
18846 }
18847 }
18848 }
18849
18850 /**
18851 * If a plugin was network activated and connected but don't have a network
18852 * level settings, then add an artificial menu item for the Account and other
18853 * Freemius settings.
18854 *
18855 * @author Vova Feldman (@svovaf)
18856 * @since 2.0.0
18857 */
18858 private function add_network_menu_when_missing() {
18859 $this->_logger->entrance();
18860
18861 if ( ! $this->_is_network_active ) {
18862 // Plugin wasn't activated on the network level.
18863 return;
18864 }
18865
18866 if ( ! fs_is_network_admin() ) {
18867 // The context is not the network admin.
18868 return;
18869 }
18870
18871 if ( $this->_menu->has_network_menu() ) {
18872 // Plugin already has a network level menu.
18873 return;
18874 }
18875
18876 if ( $this->is_network_activation_mode() ) {
18877 /**
18878 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
18879 * screen is being shown.
18880 *
18881 * @author Leo Fajardo (@leorw)
18882 */
18883 return;
18884 }
18885
18886 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
18887 if ( $this->is_network_delegated_connection() ) {
18888 // Super-admin delegated the connection to the site admins.
18889 return;
18890 }
18891 }
18892
18893 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
18894
18895 if ( $this->_menu->has_menu() ||
18896 ! $this->is_addon() ||
18897 $this->is_activation_mode()
18898 ) {
18899 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
18900 $this->get_plugin_name(),
18901 $this->get_plugin_name(),
18902 'manage_options',
18903 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
18904 );
18905 }
18906 } else {
18907 $this->_menu->add_subpage_and_update(
18908 $this->_menu->get_parent_slug(),
18909 $this->get_plugin_name(),
18910 $this->get_plugin_name(),
18911 'manage_options',
18912 $this->_menu->get_slug()
18913 );
18914 }
18915 }
18916
18917 /**
18918 * @author Leo Fajardo (@leorw)
18919 * @since 1.2.1
18920 *
18921 * return string
18922 */
18923 function get_top_level_menu_capability() {
18924 global $menu;
18925
18926 $top_level_menu_slug = $this->get_top_level_menu_slug();
18927
18928 foreach ( $menu as $menu_info ) {
18929 /**
18930 * The second element in the menu info array is the capability/role that has access to the menu and the
18931 * third element is the menu slug.
18932 */
18933 if ( $menu_info[2] === $top_level_menu_slug ) {
18934 return $menu_info[1];
18935 }
18936 }
18937
18938 return 'read';
18939 }
18940
18941 /**
18942 * @author Vova Feldman (@svovaf)
18943 * @since 1.0.0
18944 *
18945 * @return string
18946 */
18947 private function get_top_level_menu_slug() {
18948 return ( $this->is_addon() ?
18949 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
18950 $this->_menu->get_top_level_menu_slug() );
18951 }
18952
18953 /**
18954 * @author Vova Feldman (@svovaf)
18955 * @since 1.2.2.7
18956 *
18957 * @return string
18958 */
18959 function get_pricing_cta_label() {
18960 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
18961
18962 if ( $this->is_in_trial_promotion() &&
18963 ! $this->is_paying_or_trial()
18964 ) {
18965 // If running a trial promotion, modify the pricing to load the trial.
18966 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
18967 } else if ( $this->is_paying() ) {
18968 $label = $this->get_text_inline( 'Pricing', 'pricing' );
18969 }
18970
18971 return $label;
18972 }
18973
18974 /**
18975 * @author Vova Feldman (@svovaf)
18976 * @since 1.2.2.7
18977 *
18978 * @return bool
18979 */
18980 function is_pricing_page_visible() {
18981 return (
18982 // Has at least one paid plan.
18983 $this->has_paid_plan() &&
18984 // Didn't ask to hide the pricing page.
18985 $this->is_page_visible( 'pricing' ) &&
18986 // Don't have a valid active license or has more than one plan.
18987 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
18988 );
18989 }
18990
18991 /**
18992 * @author Leo Fajardo (@leorw)
18993 * @since 2.3.0
18994 *
18995 * @param bool $is_activation_mode
18996 *
18997 * @return bool
18998 */
18999 private function should_add_submenu_or_action_links( $is_activation_mode ) {
19000 if ( $this->is_addon() ) {
19001 // No submenu items or action links for add-ons.
19002 return false;
19003 }
19004
19005 if ( $this->show_opt_in_on_themes_page() ) {
19006 if ( ! fs_is_network_admin() ) {
19007 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
19008 return true;
19009 }
19010 } else if ( $is_activation_mode ) {
19011 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
19012 return false;
19013 }
19014
19015 if ( fs_is_network_admin() ) {
19016 /**
19017 * Add submenu items or action links to network level when plugin was network activated and the super
19018 * admin did NOT delegate the connection of all sites to site admins.
19019 */
19020 return (
19021 $this->_is_network_active &&
19022 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
19023 ! $this->is_network_delegated_connection() )
19024 );
19025 }
19026
19027 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
19028 }
19029
19030 /**
19031 * Add default Freemius menu items.
19032 *
19033 * @author Vova Feldman (@svovaf)
19034 * @since 1.0.0
19035 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
19036 */
19037 private function add_submenu_items() {
19038 $this->_logger->entrance();
19039
19040 $is_activation_mode = $this->is_activation_mode();
19041
19042 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
19043
19044 if ( $add_submenu_items ) {
19045 if ( $this->has_affiliate_program() ) {
19046 // Add affiliation page.
19047 $this->add_submenu_item(
19048 $this->get_text_inline( 'Affiliation', 'affiliation' ),
19049 array( &$this, '_affiliation_page_render' ),
19050 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
19051 'manage_options',
19052 'affiliation',
19053 'Freemius::_clean_admin_content_section',
19054 WP_FS__DEFAULT_PRIORITY,
19055 $this->is_submenu_item_visible( 'affiliation' )
19056 );
19057 }
19058 }
19059
19060 if ( $add_submenu_items ||
19061 ( $is_activation_mode &&
19062 $this->is_only_premium() &&
19063 $this->is_admin_page( 'account' ) &&
19064 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
19065 )
19066 ) {
19067 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
19068 $show_account = (
19069 $this->is_submenu_item_visible( 'account' ) &&
19070 /**
19071 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
19072 */
19073 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
19074 );
19075
19076 // Add user account page.
19077 $this->add_submenu_item(
19078 $this->get_text_inline( 'Account', 'account' ),
19079 array( &$this, '_account_page_render' ),
19080 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
19081 'manage_options',
19082 'account',
19083 array( &$this, '_account_page_load' ),
19084 WP_FS__DEFAULT_PRIORITY,
19085 ( $add_submenu_items && $show_account )
19086 );
19087 }
19088 }
19089
19090 if ( $add_submenu_items ) {
19091 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
19092 // Add contact page.
19093 $this->add_submenu_item(
19094 $this->get_text_inline( 'Contact Us', 'contact-us' ),
19095 array( &$this, '_contact_page_render' ),
19096 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
19097 'manage_options',
19098 'contact',
19099 'Freemius::_clean_admin_content_section',
19100 WP_FS__DEFAULT_PRIORITY,
19101 $this->is_submenu_item_visible( 'contact' )
19102 );
19103 }
19104
19105 if ( $this->has_addons() ) {
19106 $this->add_submenu_item(
19107 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
19108 array( &$this, '_addons_page_render' ),
19109 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
19110 'manage_options',
19111 'addons',
19112 array( &$this, '_addons_page_load' ),
19113 WP_FS__LOWEST_PRIORITY - 1,
19114 $this->is_submenu_item_visible( 'addons' )
19115 );
19116 }
19117 }
19118
19119 if ( $add_submenu_items ||
19120 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
19121 ) {
19122 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
19123 $show_pricing = (
19124 $this->is_submenu_item_visible( 'pricing' ) &&
19125 $this->is_pricing_page_visible()
19126 );
19127
19128 $pricing_cta_text = $this->get_pricing_cta_label();
19129 $pricing_class = 'upgrade-mode';
19130 if ( $show_pricing ) {
19131 if ( $this->is_in_trial_promotion() &&
19132 ! $this->is_paying_or_trial()
19133 ) {
19134 // If running a trial promotion, modify the pricing to load the trial.
19135 $pricing_class = 'trial-mode';
19136 } else if ( $this->is_paying() ) {
19137 $pricing_class = '';
19138 }
19139 }
19140
19141 // Add upgrade/pricing page.
19142 $this->add_submenu_item(
19143 $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' ) ),
19144 array( &$this, '_pricing_page_render' ),
19145 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
19146 'manage_options',
19147 'pricing',
19148 'Freemius::_clean_admin_content_section',
19149 WP_FS__LOWEST_PRIORITY,
19150 ( $add_submenu_items && $show_pricing ),
19151 $pricing_class
19152 );
19153 }
19154 }
19155
19156 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
19157 /**
19158 * Add the other menu items if there are any when not in activation mode or license activation is not
19159 * required (license activation is required for registered or anonymous users after activating the
19160 * premium version when the site is not in trial mode or there's no active valid license).
19161 *
19162 * @author Leo Fajardo (@leorw)
19163 * @since 2.2.1
19164 */
19165 if ( 0 < count( $this->_menu_items ) ) {
19166 if ( ! $this->_menu->is_top_level() ) {
19167 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
19168
19169 // Append submenu items right after the plugin's submenu item.
19170 $this->order_sub_submenu_items();
19171 } else {
19172 // Append submenu items.
19173 $this->embed_submenu_items();
19174 }
19175 }
19176 }
19177 }
19178
19179 /**
19180 * Moved the actual submenu item additions to a separated function,
19181 * in order to support sub-submenu items when the plugin's settings
19182 * only have a submenu and not top-level menu item.
19183 *
19184 * @author Vova Feldman (@svovaf)
19185 * @since 1.1.4
19186 */
19187 private function embed_submenu_items() {
19188 $item_template = $this->_menu->is_top_level() ?
19189 '<span class="fs-submenu-item %s %s %s">%s</span>' :
19190 '<span class="fs-submenu-item fs-sub %s %s %s">%s</span>';
19191
19192 $top_level_menu_capability = $this->get_top_level_menu_capability();
19193
19194 ksort( $this->_menu_items );
19195
19196 $is_first_submenu_item = true;
19197
19198 foreach ( $this->_menu_items as $priority => $items ) {
19199 foreach ( $items as $item ) {
19200 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
19201
19202 $menu_item = sprintf(
19203 $item_template,
19204 $this->get_unique_affix(),
19205 $item['menu_slug'],
19206 ! empty( $item['class'] ) ? $item['class'] : '',
19207 $item['menu_title']
19208 );
19209
19210 $top_level_menu_slug = $this->get_top_level_menu_slug();
19211 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
19212
19213 if ( ! isset( $item['url'] ) ) {
19214 $hook = FS_Admin_Menu_Manager::add_subpage(
19215 $item['show_submenu'] ?
19216 $top_level_menu_slug :
19217 '',
19218 $item['page_title'],
19219 $menu_item,
19220 $capability,
19221 $menu_slug,
19222 $item['render_function']
19223 );
19224
19225 if ( false !== $item['before_render_function'] ) {
19226 add_action( "load-$hook", $item['before_render_function'] );
19227 }
19228 } else {
19229 FS_Admin_Menu_Manager::add_subpage(
19230 $item['show_submenu'] ?
19231 $top_level_menu_slug :
19232 '',
19233 $item['page_title'],
19234 $menu_item,
19235 $capability,
19236 $menu_slug,
19237 array( $this, '' )
19238 );
19239 }
19240
19241 if ( $item['show_submenu'] && $is_first_submenu_item ) {
19242 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
19243 /**
19244 * If the top-level menu has been dynamically created, remove the first submenu item that
19245 * WordPress automatically creates when there's no submenu item whose slug matches the
19246 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
19247 *
19248 * Awesome Plugin
19249 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
19250 *
19251 * @author Leo Fajardo (@leorw)
19252 */
19253 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
19254 }
19255
19256 $is_first_submenu_item = false;
19257 }
19258 }
19259 }
19260 }
19261
19262 /**
19263 * Re-order the submenu items so all Freemius added new submenu items
19264 * are added right after the plugin's settings submenu item.
19265 *
19266 * @author Vova Feldman (@svovaf)
19267 * @since 1.1.4
19268 */
19269 private function order_sub_submenu_items() {
19270 global $submenu;
19271
19272 $menu_slug = $this->_menu->get_top_level_menu_slug();
19273
19274 /**
19275 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
19276 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
19277 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
19278 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
19279 *
19280 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
19281 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
19282 * capability needed to access the parent menu as the capability for the submenus that we will add.
19283 */
19284 if ( empty( $submenu[ $menu_slug ] ) ) {
19285 return;
19286 }
19287
19288 $top_level_menu = &$submenu[ $menu_slug ];
19289
19290 $all_submenu_items_after = array();
19291
19292 $found_submenu_item = false;
19293
19294 foreach ( $top_level_menu as $submenu_id => $meta ) {
19295 if ( $found_submenu_item ) {
19296 // Remove all submenu items after the plugin's submenu item.
19297 $all_submenu_items_after[] = $meta;
19298 unset( $top_level_menu[ $submenu_id ] );
19299 }
19300
19301 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
19302 // Found the submenu item, put all below.
19303 $found_submenu_item = true;
19304 continue;
19305 }
19306 }
19307
19308 // Embed all plugin's new submenu items.
19309 $this->embed_submenu_items();
19310
19311 // Start with specially high number to make sure it's appended.
19312 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
19313 foreach ( $all_submenu_items_after as $meta ) {
19314 $top_level_menu[ $i ] = $meta;
19315 $i ++;
19316 }
19317
19318 // Sort submenu items.
19319 ksort( $top_level_menu );
19320 }
19321
19322 /**
19323 * Helper method to return the module's support forum URL.
19324 *
19325 * @author Vova Feldman (@svovaf)
19326 * @since 1.2.2.7
19327 *
19328 * @return string
19329 */
19330 function get_support_forum_url() {
19331 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
19332 }
19333
19334 /**
19335 * Displays the Support Forum link when enabled.
19336 *
19337 * Can be filtered like so:
19338 *
19339 * function _fs_show_support_menu( $is_visible, $menu_id ) {
19340 * if ( 'support' === $menu_id ) {
19341 * return _fs->is_registered();
19342 * }
19343 * return $is_visible;
19344 * }
19345 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
19346 *
19347 */
19348 function _add_default_submenu_items() {
19349 if ( ! $this->is_on() ) {
19350 return;
19351 }
19352
19353 if ( ! $this->is_activation_mode() &&
19354 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
19355 ( ! $this->_is_network_active && is_admin() ) )
19356 ) {
19357 $this->add_submenu_link_item(
19358 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
19359 $this->get_support_forum_url(),
19360 'wp-support-forum',
19361 null,
19362 50,
19363 $this->is_submenu_item_visible( 'support' )
19364 );
19365 }
19366 }
19367
19368 /**
19369 * @author Vova Feldman (@svovaf)
19370 * @since 1.0.1
19371 *
19372 * @param string $menu_title
19373 * @param callable $render_function
19374 * @param bool|string $page_title
19375 * @param string $capability
19376 * @param bool|string $menu_slug
19377 * @param bool|callable $before_render_function
19378 * @param int $priority
19379 * @param bool $show_submenu
19380 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
19381 */
19382 function add_submenu_item(
19383 $menu_title,
19384 $render_function,
19385 $page_title = false,
19386 $capability = 'manage_options',
19387 $menu_slug = false,
19388 $before_render_function = false,
19389 $priority = WP_FS__DEFAULT_PRIORITY,
19390 $show_submenu = true,
19391 $class = ''
19392 ) {
19393 $this->_logger->entrance( 'Title = ' . $menu_title );
19394
19395 if ( $this->is_addon() ) {
19396 $parent_fs = $this->get_parent_instance();
19397
19398 if ( is_object( $parent_fs ) ) {
19399 $parent_fs->add_submenu_item(
19400 $menu_title,
19401 $render_function,
19402 $page_title,
19403 $capability,
19404 $menu_slug,
19405 $before_render_function,
19406 $priority,
19407 $show_submenu,
19408 $class
19409 );
19410
19411 return;
19412 }
19413 }
19414
19415 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19416 $this->_menu_items[ $priority ] = array();
19417 }
19418
19419 $this->_menu_items[ $priority ][] = array(
19420 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
19421 'menu_title' => $menu_title,
19422 'capability' => $capability,
19423 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19424 'render_function' => $render_function,
19425 'before_render_function' => $before_render_function,
19426 'show_submenu' => $show_submenu,
19427 'class' => $class,
19428 );
19429 }
19430
19431 /**
19432 * @author Vova Feldman (@svovaf)
19433 * @since 1.0.1
19434 *
19435 * @param string $menu_title
19436 * @param string $url
19437 * @param bool $menu_slug
19438 * @param string $capability
19439 * @param int $priority
19440 * @param bool $show_submenu
19441 */
19442 function add_submenu_link_item(
19443 $menu_title,
19444 $url,
19445 $menu_slug = false,
19446 $capability = 'read',
19447 $priority = WP_FS__DEFAULT_PRIORITY,
19448 $show_submenu = true
19449 ) {
19450 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
19451
19452 if ( $this->is_addon() ) {
19453 $parent_fs = $this->get_parent_instance();
19454
19455 if ( is_object( $parent_fs ) ) {
19456 $parent_fs->add_submenu_link_item(
19457 $menu_title,
19458 $url,
19459 $menu_slug,
19460 $capability,
19461 $priority,
19462 $show_submenu
19463 );
19464
19465 return;
19466 }
19467 }
19468
19469 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19470 $this->_menu_items[ $priority ] = array();
19471 }
19472
19473 $this->_menu_items[ $priority ][] = array(
19474 'menu_title' => $menu_title,
19475 'capability' => $capability,
19476 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19477 'url' => $url,
19478 'page_title' => $menu_title,
19479 'render_function' => 'fs_dummy',
19480 'before_render_function' => '',
19481 'show_submenu' => $show_submenu,
19482 );
19483 }
19484
19485 #endregion ------------------------------------------------------------------
19486
19487 #--------------------------------------------------------------------------------
19488 #region Admin Notices
19489 #--------------------------------------------------------------------------------
19490
19491 /**
19492 * @author Vova Feldman (@svovaf)
19493 * @since 2.3.1
19494 *
19495 * @param string|string[] $ids
19496 * @param int|null $network_level_or_blog_id
19497 *
19498 * @uses FS_Admin_Notices::remove_sticky()
19499 */
19500 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
19501 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
19502 }
19503
19504 #endregion
19505
19506 #--------------------------------------------------------------------------------
19507 #region Actions / Hooks / Filters
19508 #--------------------------------------------------------------------------------
19509
19510 /**
19511 * @author Vova Feldman (@svovaf)
19512 * @since 1.1.7
19513 *
19514 * @param string $tag
19515 *
19516 * @return string
19517 */
19518 public function get_action_tag( $tag ) {
19519 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
19520 }
19521
19522 /**
19523 * @author Vova Feldman (@svovaf)
19524 * @since 1.2.1.6
19525 *
19526 * @param string $tag
19527 * @param string $slug
19528 * @param bool $is_plugin
19529 *
19530 * @return string
19531 */
19532 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
19533 $action = "fs_{$tag}";
19534
19535 if ( ! empty( $slug ) ) {
19536 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
19537 }
19538
19539 return $action;
19540 }
19541
19542 /**
19543 * Returns a string that can be used to generate a unique action name,
19544 * option name, HTML element ID, or HTML element class.
19545 *
19546 * @author Leo Fajardo (@leorw)
19547 * @since 1.2.2
19548 *
19549 * @return string
19550 */
19551 public function get_unique_affix() {
19552 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
19553 }
19554
19555 /**
19556 * Returns a string that can be used to generate a unique action name,
19557 * option name, HTML element ID, or HTML element class.
19558 *
19559 * @author Vova Feldman (@svovaf)
19560 * @since 1.2.2.5
19561 *
19562 * @param string $slug
19563 * @param bool $is_plugin
19564 *
19565 * @return string
19566 */
19567 static function get_module_unique_affix( $slug, $is_plugin = true ) {
19568 $affix = $slug;
19569
19570 if ( ! $is_plugin ) {
19571 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
19572 }
19573
19574 return $affix;
19575 }
19576
19577 /**
19578 * @author Vova Feldman (@svovaf)
19579 * @since 1.2.1
19580 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19581 * based on the slug for backward compatibility.
19582 *
19583 * @param string $tag
19584 *
19585 * @return string
19586 */
19587 function get_ajax_action( $tag ) {
19588 return self::get_ajax_action_static( $tag, $this->_module_id );
19589 }
19590
19591 /**
19592 * @author Vova Feldman (@svovaf)
19593 * @since 1.2.1.7
19594 *
19595 * @param string $tag
19596 *
19597 * @return string
19598 */
19599 function get_ajax_security( $tag ) {
19600 return wp_create_nonce( $this->get_ajax_action( $tag ) );
19601 }
19602
19603 /**
19604 * @author Vova Feldman (@svovaf)
19605 * @since 1.2.1.7
19606 *
19607 * @param string $tag
19608 */
19609 function check_ajax_referer( $tag ) {
19610 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
19611 }
19612
19613 /**
19614 * @author Vova Feldman (@svovaf)
19615 * @since 1.2.1.6
19616 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19617 * based on the slug for backward compatibility.
19618 *
19619 * @param string $tag
19620 * @param number|null $module_id
19621 *
19622 * @return string
19623 */
19624 static function get_ajax_action_static( $tag, $module_id = null ) {
19625 $action = "fs_{$tag}";
19626
19627 if ( ! empty( $module_id ) ) {
19628 $action .= "_{$module_id}";
19629 }
19630
19631 return $action;
19632 }
19633
19634 /**
19635 * Do action, specific for the current context plugin.
19636 *
19637 * @author Vova Feldman (@svovaf)
19638 * @since 1.0.1
19639 *
19640 * @param string $tag The name of the action to be executed.
19641 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
19642 * functions hooked to the action. Default empty.
19643 *
19644 * @uses do_action()
19645 */
19646 function do_action( $tag, $arg = '' ) {
19647 $args = func_get_args();
19648
19649 $this->_logger->entrance( $tag );
19650
19651 call_user_func_array( 'do_action', array_merge(
19652 array( $this->get_action_tag( $tag ) ),
19653 array_slice( $args, 1 ) )
19654 );
19655 }
19656
19657 /**
19658 * Add action, specific for the current context plugin.
19659 *
19660 * @author Vova Feldman (@svovaf)
19661 * @since 1.0.1
19662 *
19663 * @param string $tag
19664 * @param callable $function_to_add
19665 * @param int $priority
19666 * @param int $accepted_args
19667 *
19668 * @uses add_action()
19669 */
19670 function add_action(
19671 $tag,
19672 $function_to_add,
19673 $priority = WP_FS__DEFAULT_PRIORITY,
19674 $accepted_args = 1
19675 ) {
19676 $this->_logger->entrance( $tag );
19677
19678 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19679 }
19680
19681 /**
19682 * Add AJAX action, specific for the current context plugin.
19683 *
19684 * @author Vova Feldman (@svovaf)
19685 * @since 1.2.1
19686 *
19687 * @param string $tag
19688 * @param callable $function_to_add
19689 * @param int $priority
19690 *
19691 * @uses add_action()
19692 *
19693 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19694 */
19695 function add_ajax_action(
19696 $tag,
19697 $function_to_add,
19698 $priority = WP_FS__DEFAULT_PRIORITY
19699 ) {
19700 $this->_logger->entrance( $tag );
19701
19702 return self::add_ajax_action_static(
19703 $tag,
19704 $function_to_add,
19705 $priority,
19706 $this->_module_id
19707 );
19708 }
19709
19710 /**
19711 * Add AJAX action.
19712 *
19713 * @author Vova Feldman (@svovaf)
19714 * @since 1.2.1.6
19715 *
19716 * @param string $tag
19717 * @param callable $function_to_add
19718 * @param int $priority
19719 * @param number|null $module_id
19720 *
19721 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19722 * @uses add_action()
19723 *
19724 */
19725 static function add_ajax_action_static(
19726 $tag,
19727 $function_to_add,
19728 $priority = WP_FS__DEFAULT_PRIORITY,
19729 $module_id = null
19730 ) {
19731 self::$_static_logger->entrance( $tag );
19732
19733 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
19734 return false;
19735 }
19736
19737 add_action(
19738 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
19739 $function_to_add,
19740 $priority,
19741 0
19742 );
19743
19744 self::$_static_logger->info( "$tag AJAX callback action added." );
19745
19746 return true;
19747 }
19748
19749 /**
19750 * Send a JSON response back to an Ajax request.
19751 *
19752 * @author Vova Feldman (@svovaf)
19753 * @since 1.2.1.5
19754 *
19755 * @param mixed $response
19756 */
19757 static function shoot_ajax_response( $response ) {
19758 wp_send_json( $response );
19759 }
19760
19761 /**
19762 * Send a JSON response back to an Ajax request, indicating success.
19763 *
19764 * @author Vova Feldman (@svovaf)
19765 * @since 1.2.1.5
19766 *
19767 * @param mixed $data Data to encode as JSON, then print and exit.
19768 */
19769 static function shoot_ajax_success( $data = null ) {
19770 wp_send_json_success( $data );
19771 }
19772
19773 /**
19774 * Send a JSON response back to an Ajax request, indicating failure.
19775 *
19776 * @author Vova Feldman (@svovaf)
19777 * @since 1.2.1.5
19778 *
19779 * @param mixed $error Optional error message.
19780 */
19781 static function shoot_ajax_failure( $error = '' ) {
19782 $result = array( 'success' => false );
19783 if ( ! empty( $error ) ) {
19784 $result['error'] = $error;
19785 }
19786
19787 wp_send_json( $result );
19788 }
19789
19790 /**
19791 * Returns an AJAX URL with a special extra param to indicate whether the request was triggered from the network admin or blog admin.
19792 *
19793 * @author Vova Feldman (@svovaf)
19794 * @since 2.5.1
19795 *
19796 * @param string $wrap_with By default, returns the AJAX URL wrapped with single quotes.
19797 *
19798 * @return string
19799 */
19800 static function ajax_url( $wrap_with = "'") {
19801 if ( fs_is_network_admin() ) {
19802 $param_name = '_fs_network_admin';
19803 } else {
19804 $param_name = '_fs_blog_admin';
19805 }
19806
19807 $url = admin_url( 'admin-ajax.php', 'relative' );
19808 $url .= ( false === strpos( $url, '?' ) ) ? '?' : '&';
19809 $url .= "{$param_name}=true";
19810
19811 return "{$wrap_with}{$url}{$wrap_with}";
19812 }
19813
19814 /**
19815 * Apply filter, specific for the current context plugin.
19816 *
19817 * @author Vova Feldman (@svovaf)
19818 * @since 1.0.9
19819 *
19820 * @param string $tag The name of the filter hook.
19821 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
19822 *
19823 * @return mixed The filtered value after all hooked functions are applied to it.
19824 *
19825 * @uses apply_filters()
19826 */
19827 function apply_filters( $tag, $value ) {
19828 $args = func_get_args();
19829
19830 $this->_logger->entrance( $tag );
19831
19832 array_unshift( $args, $this->get_unique_affix() );
19833
19834 return call_user_func_array( 'fs_apply_filter', $args );
19835 }
19836
19837 /**
19838 * Add filter, specific for the current context plugin.
19839 *
19840 * @author Vova Feldman (@svovaf)
19841 * @since 1.0.9
19842 *
19843 * @param string $tag
19844 * @param callable $function_to_add
19845 * @param int $priority
19846 * @param int $accepted_args
19847 *
19848 * @uses add_filter()
19849 */
19850 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
19851 $this->_logger->entrance( $tag );
19852
19853 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19854 }
19855
19856 /**
19857 * Check if has filter.
19858 *
19859 * @author Vova Feldman (@svovaf)
19860 * @since 1.1.4
19861 *
19862 * @param string $tag
19863 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
19864 *
19865 * @return false|int
19866 *
19867 * @uses has_filter()
19868 */
19869 function has_filter( $tag, $function_to_check = false ) {
19870 $this->_logger->entrance( $tag );
19871
19872 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
19873 }
19874
19875 #endregion
19876
19877 /**
19878 * Override default i18n text phrases.
19879 *
19880 * @author Vova Feldman (@svovaf)
19881 * @since 1.1.6
19882 *
19883 * @param string[] string $key_value
19884 *
19885 * @uses fs_override_i18n()
19886 */
19887 function override_i18n( $key_value ) {
19888 fs_override_i18n( $key_value, $this->_slug );
19889 }
19890
19891 /* Account Page
19892 ------------------------------------------------------------------------------------------------------------------*/
19893 /**
19894 * Update site information.
19895 *
19896 * @author Vova Feldman (@svovaf)
19897 * @since 1.0.1
19898 *
19899 * @param bool $store Flush to Database if true.
19900 * @param null|int $network_level_or_blog_id Since 2.0.0
19901 * @param \FS_Site $site Since 2.0.0
19902 */
19903 private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null, $is_backup = false ) {
19904 $this->_logger->entrance();
19905
19906 if ( is_null( $site ) ) {
19907 $site = $this->_site;
19908 }
19909
19910 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
19911 $this->_logger->error( "Empty install ID, can't store site." );
19912
19913 return;
19914 }
19915
19916 $site_clone = clone $site;
19917
19918 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id, $is_backup );
19919
19920 if (
19921 ! $is_backup &&
19922 is_object( $this->_user ) && $this->_user->id != $site->user_id
19923 ) {
19924 $this->sync_user_by_current_install( $site->user_id );
19925
19926 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
19927
19928 if ( empty( $prev_stored_user_id ) &&
19929 is_object($this->_user) && $this->_user->id != $site->user_id
19930 ) {
19931 /**
19932 * Store the current user ID as the previous user ID so that the previous user can be used
19933 * as the install's owner while the new owner's details are not yet available.
19934 *
19935 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
19936 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
19937 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
19938 * into `replica`.
19939 *
19940 * @author Leo Fajardo (@leorw)
19941 */
19942 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
19943 }
19944 }
19945
19946 $sites[ $this->_slug ] = $site_clone;
19947
19948 $this->set_account_option(
19949 ( $is_backup ? 'prev_' : '' ) . 'sites',
19950 $sites,
19951 $store,
19952 $network_level_or_blog_id
19953 );
19954 }
19955
19956 /**
19957 * 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).
19958 *
19959 * @author Leo Fajardo (@leorw)
19960 * @since 2.5.0
19961 */
19962 private function back_up_site() {
19963 $this->_logger->entrance();
19964
19965 $site_clone = clone $this->_site;
19966
19967 $this->_store_site( true, null, $site_clone, true );
19968 }
19969
19970 /**
19971 * Update plugin's plans information.
19972 *
19973 * @author Vova Feldman (@svovaf)
19974 * @since 1.0.2
19975 *
19976 * @param bool $store Flush to Database if true.
19977 */
19978 private function _store_plans( $store = true ) {
19979 $this->_logger->entrance();
19980
19981 $plans = self::get_all_plans( $this->_module_type );
19982
19983 // Copy plans.
19984 $encrypted_plans = array();
19985 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
19986 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
19987 }
19988
19989 $plans[ $this->_slug ] = $encrypted_plans;
19990
19991 $this->set_account_option( 'plans', $plans, $store );
19992 }
19993
19994 /**
19995 * Update user's plugin licenses.
19996 *
19997 * @author Vova Feldman (@svovaf)
19998 * @since 1.0.5
19999 *
20000 * @param bool $store
20001 * @param number|bool $module_id
20002 * @param FS_Plugin_License[] $licenses
20003 */
20004 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
20005 $this->_logger->entrance();
20006
20007 $all_licenses = self::get_all_licenses();
20008
20009 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
20010 $module_id = $this->_module_id;
20011
20012 $user_licenses = is_array( $this->_licenses ) ?
20013 $this->_licenses :
20014 array();
20015
20016 if ( empty( $user_licenses ) ) {
20017 // If the context user doesn't have any license, don't update the licenses collection.
20018 return;
20019 }
20020
20021 $new_user_licenses_map = array();
20022 foreach ( $user_licenses as $user_license ) {
20023 $new_user_licenses_map[ $user_license->id ] = $user_license;
20024 }
20025
20026 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
20027
20028 // Update user licenses.
20029 $licenses_to_update_count = count( $new_user_licenses_map );
20030 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
20031 if ( 0 === $licenses_to_update_count ) {
20032 break;
20033 }
20034
20035 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
20036 // Update license.
20037 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
20038 unset( $new_user_licenses_map[ $license->id ] );
20039
20040 $licenses_to_update_count --;
20041 }
20042 }
20043
20044 if ( ! empty( $new_user_licenses_map ) ) {
20045 // Add new licenses.
20046 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
20047 }
20048
20049 $licenses = $all_licenses[ $module_id ];
20050 }
20051
20052 if ( ! isset( $all_licenses[ $module_id ] ) ) {
20053 $all_licenses[ $module_id ] = array();
20054 }
20055
20056 $all_licenses[ $module_id ] = $licenses;
20057
20058 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
20059 }
20060
20061 /**
20062 * Update user information.
20063 *
20064 * @author Vova Feldman (@svovaf)
20065 * @since 1.0.1
20066 *
20067 * @param bool $store Flush to Database if true.
20068 */
20069 private function _store_user( $store = true ) {
20070 $this->_logger->entrance();
20071
20072 if ( empty( $this->_user->id ) ) {
20073 $this->_logger->error( "Empty user ID, can't store user." );
20074
20075 return;
20076 }
20077
20078 $users = self::get_all_users();
20079 $users[ $this->_user->id ] = $this->_user;
20080 self::$_accounts->set_option( 'users', $users, $store );
20081 }
20082
20083 /**
20084 * Update new updates information.
20085 *
20086 * @author Vova Feldman (@svovaf)
20087 * @since 1.0.4
20088 *
20089 * @param FS_Plugin_Tag|null $update
20090 * @param bool $store Flush to Database if true.
20091 * @param bool|number $plugin_id
20092 */
20093 private function _store_update( $update, $store = true, $plugin_id = false ) {
20094 $this->_logger->entrance();
20095
20096 if ( $update instanceof FS_Plugin_Tag ) {
20097 $update->updated = time();
20098 }
20099
20100 if ( ! is_numeric( $plugin_id ) ) {
20101 $plugin_id = $this->_plugin->id;
20102 }
20103
20104 $updates = self::get_all_updates();
20105 $updates[ $plugin_id ] = $update;
20106 self::$_accounts->set_option( 'updates', $updates, $store );
20107 }
20108
20109 /**
20110 * Update new updates information.
20111 *
20112 * @author Vova Feldman (@svovaf)
20113 * @since 1.0.6
20114 *
20115 * @param FS_Plugin[] $plugin_addons
20116 * @param bool $store Flush to Database if true.
20117 */
20118 private function _store_addons( $plugin_addons, $store = true ) {
20119 $this->_logger->entrance();
20120
20121 $addons = self::get_all_addons();
20122 $addons[ $this->_plugin->id ] = $plugin_addons;
20123 self::$_accounts->set_option( 'addons', $addons, $store );
20124 }
20125
20126 /**
20127 * Delete plugin's associated add-ons.
20128 *
20129 * @author Vova Feldman (@svovaf)
20130 * @since 1.0.8
20131 *
20132 * @param bool $store
20133 *
20134 * @return bool
20135 */
20136 private function _delete_account_addons( $store = true ) {
20137 $all_addons = self::get_all_account_addons();
20138
20139 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
20140 return false;
20141 }
20142
20143 unset( $all_addons[ $this->_plugin->id ] );
20144
20145 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20146
20147 return true;
20148 }
20149
20150 /**
20151 * Update account add-ons list.
20152 *
20153 * @author Vova Feldman (@svovaf)
20154 * @since 1.0.6
20155 *
20156 * @param FS_Plugin[] $addons
20157 * @param bool $store Flush to Database if true.
20158 */
20159 private function _store_account_addons( $addons, $store = true ) {
20160 $this->_logger->entrance();
20161
20162 $all_addons = self::get_all_account_addons();
20163 $all_addons[ $this->_plugin->id ] = $addons;
20164 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20165 }
20166
20167 /**
20168 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
20169 * the valid user licenses will be fetched again and the account add-ons may be updated.
20170 *
20171 * @author Leo Fajardo (@leorw)
20172 * @since 2.2.4
20173 */
20174 private function purge_valid_user_licenses_cache() {
20175 if ( ! $this->is_registered() ) {
20176 return;
20177 }
20178
20179 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
20180 }
20181
20182 /**
20183 * @author Leo Fajardo (@leorw)
20184 * @since 2.3.0
20185 *
20186 * @param array $all_licenses
20187 * @param number|null $site_license_id
20188 * @param bool $include_parent_licenses
20189 *
20190 * @return array
20191 */
20192 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
20193 $foreign_licenses = array(
20194 'ids' => array(),
20195 'license_keys' => array()
20196 );
20197
20198 $parent_license_ids_map = array();
20199
20200 foreach ( $all_licenses as $license ) {
20201 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
20202 continue;
20203 }
20204
20205 $foreign_licenses['ids'][] = $license->id;
20206 $foreign_licenses['license_keys'][] = $license->secret_key;
20207
20208 if (
20209 $include_parent_licenses &&
20210 is_object( $this->_license ) &&
20211 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
20212 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
20213 ) {
20214 /**
20215 * Include the parent license's info only if it has not been included before since child licenses
20216 * can have the same parent license.
20217 */
20218 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
20219 $foreign_licenses['license_keys'][] = $license->secret_key;
20220
20221 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
20222 }
20223 }
20224
20225 if ( empty( $foreign_licenses['ids'] ) ) {
20226 $foreign_licenses = array();
20227 }
20228
20229 return $foreign_licenses;
20230 }
20231
20232 /**
20233 * @author Leo Fajardo (@leorw)
20234 * @since 2.3.0
20235 *
20236 * @return string
20237 */
20238 private function get_valid_user_licenses_endpoint() {
20239 $user_licenses_endpoint = '/licenses.json?type=active' .
20240 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
20241
20242 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
20243
20244 if ( ! empty ( $foreign_licenses ) ) {
20245 $foreign_licenses = array(
20246 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20247 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20248 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20249 );
20250
20251 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20252 }
20253
20254 return $user_licenses_endpoint;
20255 }
20256
20257 /**
20258 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
20259 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
20260 * method filters out non–add-on product IDs and stores the add-on IDs.
20261 *
20262 * @author Leo Fajardo (@leorw)
20263 * @since 2.2.4
20264 *
20265 * @return stdClass[] array
20266 */
20267 private function fetch_valid_user_licenses() {
20268 $this->_logger->entrance();
20269
20270 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
20271
20272 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
20273 ! is_array( $result->licenses )
20274 ) {
20275 return array();
20276 }
20277
20278 return $result->licenses;
20279 }
20280
20281 /**
20282 * @author Leo Fajardo (@leorw)
20283 * @since 2.2.4
20284 *
20285 * @return number[] Account add-on IDs.
20286 */
20287 function get_updated_account_addons() {
20288 $addons = $this->get_addons();
20289 if ( empty( $addons ) ) {
20290 return array();
20291 }
20292
20293 $account_addons = $this->get_account_addons();
20294 if ( ! is_array( $account_addons ) ) {
20295 $account_addons = array();
20296 }
20297
20298 $user_licenses = $this->is_registered() ?
20299 $this->fetch_valid_user_licenses() :
20300 array();
20301
20302 if ( empty( $user_licenses ) ) {
20303 return $account_addons;
20304 }
20305
20306 $addon_ids = array();
20307 foreach ( $addons as $addon ) {
20308 $addon_ids[] = $addon->id;
20309 }
20310
20311 $license_product_ids = array();
20312
20313 foreach ( $user_licenses as $license ) {
20314 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
20315 $license_product_ids = array_merge( $license_product_ids, $license->products );
20316 } else {
20317 $license_product_ids[] = $license->plugin_id;
20318 }
20319 }
20320
20321 // Filter out non–add-on IDs.
20322 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
20323 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
20324 $this->_store_account_addons( array_unique( $new_account_addons ) );
20325 }
20326
20327 return $new_account_addons;
20328 }
20329
20330 /**
20331 * Store account params in the Database.
20332 *
20333 * @author Vova Feldman (@svovaf)
20334 * @since 1.0.1
20335 *
20336 * @param null|int $blog_id Since 2.0.0
20337 */
20338 private function _store_account( $blog_id = null ) {
20339 $this->_logger->entrance();
20340
20341 $this->_store_site( false, $blog_id );
20342 $this->_store_user( false );
20343 $this->_store_plans( false );
20344 $this->_store_licenses( false );
20345
20346 self::$_accounts->store( $blog_id );
20347 }
20348
20349 /**
20350 * Sync user's information.
20351 *
20352 * @author Vova Feldman (@svovaf)
20353 * @since 1.0.3
20354 * @uses FS_Api
20355 */
20356 private function _handle_account_user_sync() {
20357 $this->_logger->entrance();
20358
20359 $api = $this->get_api_user_scope();
20360
20361 // Get user's information.
20362 $user = $api->get( '/', true );
20363
20364 if ( isset( $user->id ) ) {
20365 $this->_user->first = $user->first;
20366 $this->_user->last = $user->last;
20367 $this->_user->email = $user->email;
20368
20369 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
20370
20371 if ( $user->is_verified &&
20372 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
20373 ) {
20374 $this->_user->is_verified = true;
20375
20376 $this->do_action( 'account_email_verified', $user->email );
20377
20378 $this->_admin_notices->add(
20379 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
20380 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
20381 'success',
20382 // Make admin sticky if account menu item is invisible,
20383 // since the page will be auto redirected to the plugin's
20384 // main settings page, and the non-sticky message
20385 // will disappear.
20386 ! $is_menu_item_account_visible,
20387 'email_verified'
20388 );
20389 }
20390
20391 // Flush user details to DB.
20392 $this->_store_user();
20393
20394 $this->do_action( 'after_account_user_sync', $user );
20395
20396 /**
20397 * If account menu item is hidden, redirect to plugin's main settings page.
20398 *
20399 * @author Vova Feldman (@svovaf)
20400 * @since 1.1.6
20401 *
20402 * @link https://github.com/Freemius/wordpress-sdk/issues/6
20403 */
20404 if ( ! $is_menu_item_account_visible ) {
20405 fs_redirect( $this->_get_admin_page_url() );
20406 }
20407 }
20408 }
20409
20410 /**
20411 * @author Vova Feldman (@svovaf)
20412 * @since 1.0.9
20413 * @uses FS_Api
20414 *
20415 * @param number|bool $license_id
20416 *
20417 * @return FS_Subscription|object|bool
20418 */
20419 private function _fetch_site_license_subscription( $license_id = false ) {
20420 $this->_logger->entrance();
20421 $api = $this->get_api_site_scope();
20422
20423 if ( ! is_numeric( $license_id ) ) {
20424 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
20425 $this->_license->parent_license_id :
20426 $this->_license->id;
20427 }
20428
20429 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
20430
20431 return ! isset( $result->error ) ?
20432 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
20433 new FS_Subscription( $result->subscriptions[0] ) :
20434 false
20435 ) :
20436 $result;
20437 }
20438
20439 /**
20440 * @author Vova Feldman (@svovaf)
20441 * @since 1.0.4
20442 * @uses FS_Api
20443 *
20444 * @param number|bool $plan_id
20445 *
20446 * @return FS_Plugin_Plan|object
20447 */
20448 private function _fetch_site_plan( $plan_id = false ) {
20449 $this->_logger->entrance();
20450 $api = $this->get_api_site_scope();
20451
20452 if ( ! is_numeric( $plan_id ) ) {
20453 $plan_id = $this->_site->plan_id;
20454 }
20455
20456 $plan = $api->get( "/plans/{$plan_id}.json", true );
20457
20458 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
20459 }
20460
20461 /**
20462 * @author Vova Feldman (@svovaf)
20463 * @since 1.0.5
20464 * @uses FS_Api
20465 *
20466 * @return FS_Plugin_Plan[]|object
20467 */
20468 private function _fetch_plugin_plans() {
20469 $this->_logger->entrance();
20470 $api = $this->get_current_or_network_user_api_scope();
20471
20472 /**
20473 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
20474 */
20475 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
20476
20477 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
20478 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
20479 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
20480 }
20481
20482 $result = $result->plans;
20483 }
20484
20485 return $result;
20486 }
20487
20488 /**
20489 * @author Vova Feldman (@svovaf)
20490 * @since 2.0.0
20491 *
20492 * @param number $plan_id
20493 *
20494 * @return \FS_Plugin_Plan|object
20495 */
20496 private function fetch_plan_by_id( $plan_id ) {
20497 $this->_logger->entrance();
20498 $api = $this->get_current_or_network_user_api_scope();
20499
20500 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
20501
20502 return $this->is_api_result_entity( $result ) ?
20503 new FS_Plugin_Plan( $result ) :
20504 $result;
20505 }
20506
20507 /**
20508 * @author Vova Feldman (@svovaf)
20509 * @since 1.0.5
20510 * @uses FS_Api
20511 *
20512 * @param number|bool $plugin_id
20513 * @param number|bool $site_license_id
20514 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
20515 * @param number|null $blog_id
20516 *
20517 * @return FS_Plugin_License[]|object
20518 */
20519 private function _fetch_licenses(
20520 $plugin_id = false,
20521 $site_license_id = false,
20522 $foreign_licenses = array(),
20523 $blog_id = null
20524 ) {
20525 $this->_logger->entrance();
20526
20527 $api = $this->get_api_user_scope();
20528
20529 if ( ! is_numeric( $plugin_id ) ) {
20530 $plugin_id = $this->_plugin->id;
20531 }
20532
20533 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
20534 if ( ! empty ( $foreign_licenses ) ) {
20535 $foreign_licenses = array(
20536 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20537 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20538 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20539 );
20540
20541 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20542 }
20543
20544 $result = $api->get( $user_licenses_endpoint, true );
20545
20546 $is_site_license_synced = false;
20547
20548 $api_errors = array();
20549
20550 if ( $this->is_api_result_object( $result, 'licenses' ) &&
20551 is_array( $result->licenses )
20552 ) {
20553 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
20554 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
20555
20556 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
20557 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
20558 }
20559 }
20560
20561 $result = $result->licenses;
20562 } else {
20563 $api_errors[] = $result;
20564 $result = array();
20565 }
20566
20567 if ( ! $is_site_license_synced ) {
20568 if ( ! is_null( $blog_id ) ) {
20569 /**
20570 * If blog ID is not null, the request is for syncing of the license of a single site via the
20571 * network-level "Account" page.
20572 *
20573 * @author Leo Fajardo (@leorw)
20574 */
20575 $this->switch_to_blog( $blog_id );
20576 }
20577
20578 $api = $this->get_api_site_scope();
20579
20580 if ( is_numeric( $site_license_id ) ) {
20581 // Try to retrieve a foreign license that is linked to the install.
20582 $api_result = $api->call( '/licenses.json?is_enriched=true' );
20583
20584 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
20585 is_array( $api_result->licenses )
20586 ) {
20587 $licenses = $api_result->licenses;
20588
20589 if ( ! empty( $licenses ) ) {
20590 $result[] = new FS_Plugin_License( $licenses[0] );
20591 }
20592 } else {
20593 $api_errors[] = $api_result;
20594 }
20595 } else if (
20596 is_object( $this->_license ) &&
20597 /**
20598 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
20599 * the FS instance that does the syncing is the parent FS instance.
20600 *
20601 * @author Leo Fajardo (@leorw)
20602 * @since 2.3.0
20603 */
20604 $this->_license->plugin_id == $plugin_id
20605 ) {
20606 $is_license_in_result = false;
20607 if ( ! empty( $result ) ) {
20608 foreach ( $result as $license ) {
20609 if ( $license->id == $this->_license->id ) {
20610 $is_license_in_result = true;
20611 break;
20612 }
20613 }
20614 }
20615
20616 if ( ! $is_license_in_result ) {
20617 // Fetch foreign license by ID and license key.
20618 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
20619 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
20620
20621 if ( $this->is_api_result_entity( $license ) ) {
20622 $result[] = new FS_Plugin_License( $license );
20623 } else {
20624 $api_errors[] = $license;
20625 }
20626 }
20627 }
20628
20629 if ( ! is_null( $blog_id ) ) {
20630 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20631 }
20632 }
20633
20634 if ( is_array( $result ) && 0 < count( $result ) ) {
20635 // If found at least one license, return license collection even if there are errors.
20636 return $result;
20637 }
20638
20639 if ( ! empty( $api_errors ) ) {
20640 // If found any errors and no licenses, return first error.
20641 return $api_errors[0];
20642 }
20643
20644 // Fallback to empty licenses list.
20645 return $result;
20646 }
20647
20648 /**
20649 * @author Vova Feldman (@svovaf)
20650 * @since 2.0.0
20651 *
20652 * @param number $license_id
20653 * @param string $license_key
20654 *
20655 * @return \FS_Plugin_License|object
20656 */
20657 private function fetch_license_by_key( $license_id, $license_key ) {
20658 $this->_logger->entrance();
20659
20660 $api = $this->get_current_or_network_user_api_scope();
20661
20662 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
20663
20664 return $this->is_api_result_entity( $result ) ?
20665 new FS_Plugin_License( $result ) :
20666 $result;
20667 }
20668
20669 /**
20670 * @author Vova Feldman (@svovaf)
20671 * @since 1.2.0
20672 * @uses FS_Api
20673 *
20674 * @param number|bool $plugin_id
20675 * @param bool $flush
20676 *
20677 * @return FS_Payment[]|object
20678 */
20679 function _fetch_payments( $plugin_id = false, $flush = false ) {
20680 $this->_logger->entrance();
20681
20682 $api = $this->get_api_user_scope();
20683
20684 if ( ! is_numeric( $plugin_id ) ) {
20685 $plugin_id = $this->_plugin->id;
20686 }
20687
20688 $include_bundles = (
20689 is_object( $this->_plugin ) &&
20690 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
20691 );
20692
20693 $result = $api->get(
20694 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
20695 $flush
20696 );
20697
20698 if ( ! isset( $result->error ) ) {
20699 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
20700 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
20701 }
20702 $result = $result->payments;
20703 }
20704
20705 return $result;
20706 }
20707
20708 /**
20709 * @author Vova Feldman (@svovaf)
20710 * @since 1.2.1.5
20711 * @uses FS_Api
20712 *
20713 * @param bool $flush
20714 *
20715 * @return \FS_Billing|mixed
20716 */
20717 function _fetch_billing( $flush = false ) {
20718 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
20719
20720 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
20721
20722 if ( $this->is_api_result_entity( $billing ) ) {
20723 $billing = new FS_Billing( $billing );
20724 }
20725
20726 return $billing;
20727 }
20728
20729 /**
20730 * @author Vova Feldman (@svovaf)
20731 * @since 1.0.5
20732 *
20733 * @param FS_Plugin_License[] $licenses
20734 * @param number $module_id
20735 */
20736 private function _update_licenses( $licenses, $module_id ) {
20737 $this->_logger->entrance();
20738
20739 if ( is_array( $licenses ) ) {
20740 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
20741 $licenses[ $i ]->updated = time();
20742 }
20743 }
20744
20745 $this->_store_licenses( true, $module_id, $licenses );
20746 }
20747
20748 /**
20749 * @author Vova Feldman (@svovaf)
20750 * @since 1.0.4
20751 *
20752 * @param bool|number $plugin_id
20753 * @param bool $flush Since 1.1.7.3
20754 * @param int $expiration Since 1.2.2.7
20755 * @param bool|string $newer_than Since 2.2.1
20756 *
20757 * @return object|false New plugin tag info if exist.
20758 */
20759 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
20760 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
20761
20762 if ( ! is_object( $latest_tag ) ) {
20763 return false;
20764 }
20765
20766 $plugin_version = $this->get_plugin_version();
20767
20768 // Check if version is actually newer.
20769 $has_new_version =
20770 // If it's an non-installed add-on then always return latest.
20771 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
20772 // Compare versions.
20773 version_compare( $plugin_version, $latest_tag->version, '<' );
20774
20775 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
20776
20777 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
20778
20779 $this->_storage->beta_data = array(
20780 'is_beta' => $is_latest_version_beta,
20781 'version' => $latest_tag->version
20782 );
20783
20784 return $has_new_version ? $latest_tag : false;
20785 }
20786
20787 /**
20788 * @author Vova Feldman (@svovaf)
20789 * @since 1.0.5
20790 *
20791 * @param bool|number $plugin_id
20792 * @param bool $flush Since 1.1.7.3
20793 * @param int $expiration Since 1.2.2.7
20794 * @param bool|string $newer_than Since 2.2.1
20795 *
20796 * @return bool|FS_Plugin_Tag
20797 */
20798 function get_update( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
20799 $this->_logger->entrance();
20800
20801 if ( ! is_numeric( $plugin_id ) ) {
20802 $plugin_id = $this->_plugin->id;
20803 }
20804
20805 $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than );
20806 $updates = $this->get_all_updates();
20807
20808 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
20809 }
20810
20811 /**
20812 * Check if site assigned with active license.
20813 *
20814 * @author Vova Feldman (@svovaf)
20815 * @since 1.0.6
20816 *
20817 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
20818 */
20819 function has_active_license() {
20820 return (
20821 is_object( $this->_license ) &&
20822 is_numeric( $this->_license->id ) &&
20823 ! $this->_license->is_expired()
20824 );
20825 }
20826
20827 /**
20828 * Check if site assigned with active & valid (not expired) license.
20829 *
20830 * @author Vova Feldman (@svovaf)
20831 * @since 1.2.1
20832 *
20833 * @param bool $check_expiration
20834 */
20835 function has_active_valid_license( $check_expiration = true ) {
20836 return self::is_active_valid_license( $this->_license, $check_expiration );
20837 }
20838
20839 /**
20840 * @author Leo Fajardo (@leorw)
20841 * @since 2.3.1
20842 */
20843 function is_data_debug_mode() {
20844 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
20845 return false;
20846 }
20847
20848 $fs = $this->is_addon() ?
20849 $this->get_parent_instance() :
20850 $this;
20851
20852 if ( $fs->is_network_active() && fs_is_network_admin() ) {
20853 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
20854 } else {
20855 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
20856 }
20857
20858 return ( 'true' === $is_developer_license_debug_mode );
20859 }
20860
20861 /**
20862 * @author Leo Fajardo (@leorw)
20863 * @since 2.3.1
20864 */
20865 function _set_data_debug_mode() {
20866 if ( ! $this->is_whitelabeled( true ) ) {
20867 return;
20868 }
20869
20870 $license_or_user_key = fs_request_get_raw( 'license_or_user_key' );
20871
20872 $transient_value = ( ! empty( $license_or_user_key ) ) ?
20873 'true' :
20874 'false';
20875
20876 if ( 'true' === $transient_value ) {
20877 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
20878 'last_license_key' :
20879 'last_license_user_key'
20880 );
20881
20882 if ( md5( $license_or_user_key ) !== $stored_key ) {
20883 $this->shoot_ajax_failure( sprintf(
20884 '%s... %s',
20885 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
20886 $this->get_text_inline(
20887 'seems like the key you entered doesn\'t match our records.',
20888 'developer-or-license-not-found'
20889 )
20890 ) );
20891 }
20892 }
20893
20894 if ( $this->is_network_active() && fs_is_network_admin() ) {
20895 set_site_transient(
20896 "fs_{$this->get_id()}_data_debug_mode",
20897 $transient_value,
20898 WP_FS__TIME_24_HOURS_IN_SEC / 24
20899 );
20900 } else {
20901 set_transient(
20902 "fs_{$this->get_id()}_data_debug_mode",
20903 $transient_value,
20904 WP_FS__TIME_24_HOURS_IN_SEC / 24
20905 );
20906 }
20907
20908 if ( 'true' === $transient_value ) {
20909 $this->_admin_notices->add_sticky(
20910 $this->get_text_inline(
20911 '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.',
20912 'data_debug_mode_enabled'
20913 ),
20914 'data_debug_mode_enabled'
20915 );
20916 }
20917
20918 $this->shoot_ajax_success();
20919 }
20920
20921 /**
20922 * Check if a given license is active & valid (not expired).
20923 *
20924 * @author Vova Feldman (@svovaf)
20925 * @since 2.1.3
20926 *
20927 * @param FS_Plugin_License $license
20928 * @param bool $check_expiration
20929 *
20930 * @return bool
20931 */
20932 private static function is_active_valid_license( $license, $check_expiration = true ) {
20933 return (
20934 is_object( $license ) &&
20935 FS_Plugin_License::is_valid_id( $license->id ) &&
20936 $license->is_active() &&
20937 ( ! $check_expiration || $license->is_valid() )
20938 );
20939 }
20940
20941 /**
20942 * Checks if there's any site that is associated with an active & valid license.
20943 * This logic is used to determine if the admin can download the premium code base from a network level admin.
20944 *
20945 * @author Vova Feldman (@svovaf)
20946 * @since 2.1.3
20947 *
20948 * @return bool
20949 */
20950 function has_any_active_valid_license() {
20951 if ( ! fs_is_network_admin() ) {
20952 return $this->has_active_valid_license();
20953 }
20954
20955 $installs = $this->get_blog_install_map();
20956 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
20957
20958 foreach ( $installs as $blog_id => $install ) {
20959 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20960 continue;
20961 }
20962
20963 foreach ( $all_plugin_licenses as $license ) {
20964 if ( $license->id == $install->license_id ) {
20965 if ( self::is_active_valid_license( $license ) ) {
20966 return true;
20967 }
20968 }
20969 }
20970 }
20971
20972 return false;
20973 }
20974
20975 /**
20976 * Check if site assigned with license with enabled features.
20977 *
20978 * @author Vova Feldman (@svovaf)
20979 * @since 1.0.6
20980 *
20981 * @return bool
20982 */
20983 function has_features_enabled_license() {
20984 return (
20985 is_object( $this->_license ) &&
20986 is_numeric( $this->_license->id ) &&
20987 $this->_license->is_features_enabled()
20988 );
20989 }
20990
20991 /**
20992 * Checks if the product is activated with a bundle license.
20993 *
20994 * @author Leo Fajardo (@leorw)
20995 * @since 2.4.0
20996 *
20997 * @return bool
20998 */
20999 function is_activated_with_bundle_license() {
21000 if ( ! $this->has_features_enabled_license() ) {
21001 return false;
21002 }
21003
21004 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
21005 }
21006
21007 /**
21008 * Check if user is a trial or have feature enabled license.
21009 *
21010 * @author Vova Feldman (@svovaf)
21011 * @since 1.1.7
21012 *
21013 * @return bool
21014 */
21015 function can_use_premium_code() {
21016 return $this->is_trial() || $this->has_features_enabled_license();
21017 }
21018
21019 /**
21020 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
21021 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
21022 * the context user is authenticated.
21023 *
21024 * @author Leo Fajardo (@leorw)
21025 * @since 1.2.2
21026 *
21027 * @return bool
21028 */
21029 function is_user_admin() {
21030 /**
21031 * Require a super-admin when network activated, running from the network level OR if
21032 * running from the site level but not delegated the opt-in.
21033 *
21034 * @author Vova Feldman (@svovaf)
21035 * @since 2.0.0
21036 */
21037 if ( $this->_is_network_active &&
21038 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
21039 ) {
21040 return is_super_admin();
21041 }
21042
21043 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
21044 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
21045 }
21046
21047 /**
21048 * Sync site's plan.
21049 *
21050 * @author Vova Feldman (@svovaf)
21051 * @since 1.0.3
21052 *
21053 * @uses FS_Api
21054 *
21055 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
21056 * the admin.
21057 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
21058 * network-level "Account" page.
21059 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
21060 * `_sync_plugin_license` method in order to switch to the previous blog when sending
21061 * updates for a single site in case `execute_cron` has switched to a different blog.
21062 */
21063 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
21064 $this->_logger->entrance();
21065
21066 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
21067
21068 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
21069
21070 if ( $is_addon_sync ) {
21071 $this->_sync_addon_license( $plugin_id, $background );
21072 } else {
21073 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
21074 }
21075
21076 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
21077 }
21078
21079 /**
21080 * Sync plugin's add-on license.
21081 *
21082 * @author Vova Feldman (@svovaf)
21083 * @since 1.0.6
21084 * @uses FS_Api
21085 *
21086 * @param number $addon_id
21087 * @param bool $background
21088 */
21089 private function _sync_addon_license( $addon_id, $background ) {
21090 $this->_logger->entrance();
21091
21092 if ( $this->is_addon_activated( $addon_id ) ) {
21093 // If already installed, use add-on sync.
21094 $fs_addon = self::get_instance_by_id( $addon_id );
21095
21096 if (
21097 // Add-on is network activated and network integrated.
21098 $fs_addon->is_network_active() ||
21099 // Background sync cron.
21100 self::is_cron() ||
21101 // Add-on is not network activated or not network integrated.
21102 ! fs_is_network_admin()
21103 ) {
21104 $fs_addon->_sync_license( $background );
21105
21106 return;
21107 }
21108 }
21109
21110 // Validate add-on exists.
21111 $addon = $this->get_addon( $addon_id );
21112
21113 if ( ! is_object( $addon ) ) {
21114 return;
21115 }
21116
21117 // Add add-on into account add-ons.
21118 $account_addons = $this->get_account_addons();
21119 if ( ! is_array( $account_addons ) ) {
21120 $account_addons = array();
21121 }
21122 $account_addons[] = $addon->id;
21123 $account_addons = array_unique( $account_addons );
21124 $this->_store_account_addons( $account_addons );
21125
21126 // Load add-on licenses.
21127 $licenses = $this->_fetch_licenses( $addon->id );
21128
21129 // Sync add-on licenses.
21130 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
21131 $this->_update_licenses( $licenses, $addon->id );
21132
21133 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
21134 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
21135
21136 if ( ! isset( $plans_result->error ) ) {
21137 $plans = array();
21138 foreach ( $plans_result->plans as $plan ) {
21139 $plans[] = new FS_Plugin_Plan( $plan );
21140 }
21141
21142 $this->_admin_notices->add_sticky(
21143 sprintf(
21144 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
21145 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
21146 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
21147 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
21148 $addon->title
21149 ) . ' ' . $this->get_latest_download_link(
21150 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
21151 $addon_id
21152 ),
21153 'addon_plan_upgraded_' . $addon->slug,
21154 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
21155 );
21156 }
21157 }
21158 }
21159 }
21160
21161 /**
21162 * Sync site's plugin plan.
21163 *
21164 * @author Vova Feldman (@svovaf)
21165 * @since 1.0.6
21166 * @uses FS_Api
21167 *
21168 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
21169 * @param bool $send_installs_update Since 2.0.0
21170 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
21171 * syncing its license from the network-level "Account" page (e.g.: after
21172 * activating a license only for the single install).
21173 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
21174 * can be used here to switch to the previous blog in case `execute_cron`
21175 * has switched to a different blog.
21176 */
21177 private function _sync_plugin_license(
21178 $background = false,
21179 $send_installs_update = true,
21180 $is_context_single_site = false,
21181 $current_blog_id = null
21182 ) {
21183 $this->_logger->entrance();
21184
21185 $plan_change = 'none';
21186
21187 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
21188
21189 if ( ! $send_installs_update ) {
21190 $site = $this->_site;
21191 } else {
21192 /**
21193 * Sync site info.
21194 *
21195 * @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.
21196 */
21197 if ( $is_site_level_sync ) {
21198 /**
21199 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
21200 *
21201 * @author Leo Fajardo (@leorw)
21202 * @since 2.2.3
21203 */
21204 if ( is_numeric( $current_blog_id ) ) {
21205 $this->switch_to_blog( $current_blog_id );
21206 }
21207
21208 $result = $this->send_install_update( array(), true, true );
21209 $is_valid = $this->is_api_result_entity( $result );
21210 } else {
21211 $result = $this->send_installs_update( array(), true, true );
21212 $is_valid = $this->is_api_result_object( $result, 'installs' );
21213 }
21214
21215 if ( ! $is_valid ) {
21216 if ( $is_context_single_site ) {
21217 // Switch back to the main blog so that the following logic will have the right entities.
21218 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21219 }
21220
21221 // Show API message only if not background sync or if paying customer.
21222 if ( ! $background || $this->is_paying() ) {
21223 // Try to ping API to see if not blocked.
21224 if ( FS_Api::is_blocked( $result ) ) {
21225 /**
21226 * @author Vova Feldman (@svovaf)
21227 * @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.
21228 */
21229 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
21230 // Add notice immediately if not a background sync.
21231 $add_notice = ( ! $background );
21232
21233 if ( ! $add_notice ) {
21234 $counter = (int) get_transient( '_fs_api_connection_retry_counter' );
21235
21236 // We only want to add the notice after 3 consecutive failures.
21237 $add_notice = ( 3 <= $counter );
21238
21239 if ( ! $add_notice ) {
21240 /**
21241 * 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.
21242 *
21243 * 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.
21244 */
21245 set_transient( '_fs_api_connection_retry_counter', $counter + 1, WP_FS__TIME_WEEK_IN_SEC );
21246 }
21247 }
21248
21249 // Add notice instantly for not-background sync and only after 3 failed attempts for background sync.
21250 if ( $add_notice ) {
21251 self::$_global_admin_notices->add(
21252 $this->generate_api_blocked_notice_message_from_result( $result ),
21253 '',
21254 'error',
21255 $background,
21256 'api_blocked'
21257 );
21258
21259 add_action( 'admin_footer', array( 'Freemius', '_add_api_connectivity_notice_handler_js' ) );
21260
21261 // Notice was just shown, reset connectivity counter.
21262 delete_transient( '_fs_api_connection_retry_counter' );
21263 }
21264 }
21265 } else if ( is_object( $result ) ) {
21266 // Authentication params are broken.
21267 $this->_admin_notices->add(
21268 $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 ),
21269 '',
21270 'error'
21271 );
21272 }
21273 }
21274
21275 // No reason to continue with license sync while there are API issues.
21276 return;
21277 }
21278
21279 // API is working now. Delete the transient and start afresh.
21280 delete_transient('_fs_api_connection_retry_counter');
21281
21282 if ( $is_site_level_sync ) {
21283 $site = new FS_Site( $result );
21284 } else {
21285 // Map site addresses to their blog IDs.
21286 $address_to_blog_map = $this->get_address_to_blog_map();
21287
21288 // Find the current context install.
21289 $site = null;
21290 foreach ( $result->installs as $install ) {
21291 if ( $install->id == $this->_site->id ) {
21292 $site = new FS_Site( $install );
21293 } else {
21294 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
21295 $blog_id = $address_to_blog_map[ $address ];
21296
21297 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
21298 }
21299 }
21300 }
21301
21302 // Sync plans.
21303 $this->_sync_plans();
21304 }
21305
21306 // Remove sticky API connectivity message.
21307 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
21308
21309 if ( ! $this->has_paid_plan() ) {
21310 $this->_site = $site;
21311 $this->_store_site(
21312 true,
21313 $is_site_level_sync ?
21314 null :
21315 $this->get_network_install_blog_id()
21316 );
21317 } else {
21318 $context_blog_id = 0;
21319
21320 if ( $is_context_single_site ) {
21321 $context_blog_id = get_current_blog_id();
21322
21323 // Switch back to the main blog in order to properly sync the license.
21324 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21325 }
21326
21327 /**
21328 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
21329 * associated with that ID is not included in the user's licenses collection.
21330 */
21331 $this->_sync_licenses(
21332 $site->license_id,
21333 ( $is_context_single_site ?
21334 $context_blog_id :
21335 null
21336 )
21337 );
21338
21339 if ( $is_context_single_site ) {
21340 $this->switch_to_blog( $context_blog_id );
21341 }
21342
21343 // Check if plan / license changed.
21344 if ( $site->plan_id != $this->_site->plan_id ||
21345 // Check if trial started.
21346 $site->trial_plan_id != $this->_site->trial_plan_id ||
21347 $site->trial_ends != $this->_site->trial_ends ||
21348 // Check if license changed.
21349 $site->license_id != $this->_site->license_id
21350 ) {
21351 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
21352 // New trial started.
21353 $this->_site = $site;
21354 $plan_change = 'trial_started';
21355
21356 // For trial with subscription use-case.
21357 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
21358
21359 if ( is_object( $new_license ) && $new_license->is_valid() ) {
21360 $this->_site = $site;
21361 $this->_update_site_license( $new_license );
21362 $this->_store_licenses();
21363
21364 $this->_sync_site_subscription( $this->_license );
21365 }
21366 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
21367 // Was in trial, but now trial expired and no license ID.
21368 // New trial started.
21369 $this->_site = $site;
21370 $plan_change = 'trial_expired';
21371 } else {
21372 $is_free = $this->is_free_plan();
21373
21374 // Make sure license exist and not expired.
21375 $new_license = is_null( $site->license_id ) ?
21376 null :
21377 $this->_get_license_by_id( $site->license_id );
21378
21379 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
21380 // License cancelled.
21381 $this->_site = $site;
21382 $this->_update_site_license( $new_license );
21383 $this->_store_licenses();
21384
21385 $plan_change = 'cancelled';
21386 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
21387 // The license is expired, so ignore upgrade method.
21388 $this->_site = $site;
21389 } else {
21390 // License changed.
21391 $this->_site = $site;
21392
21393 /**
21394 * IMPORTANT:
21395 * 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.
21396 *
21397 * @author Vova Feldman (@svovaf)
21398 * @since 2.0.0
21399 */
21400 $this->_update_site_license( $new_license );
21401
21402 if ( ! $is_context_single_site &&
21403 fs_is_network_admin() &&
21404 $this->_is_network_active &&
21405 $new_license->quota > 1 &&
21406 get_blog_count() > 1
21407 ) {
21408 // See if license can activated on all sites.
21409 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
21410 if ( ! fs_request_get_bool( 'auto_install' ) ) {
21411 // Open the license activation dialog box on the account page.
21412 add_action( 'admin_footer', array(
21413 &$this,
21414 '_open_license_activation_dialog_box'
21415 ) );
21416 }
21417 }
21418 }
21419
21420 $this->_store_licenses();
21421
21422 $plan_change = $is_free ?
21423 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
21424 ( is_object( $new_license ) ?
21425 'changed' :
21426 'downgraded' );
21427 }
21428 }
21429
21430 // Store updated site info.
21431 $this->_store_site(
21432 true,
21433 $is_site_level_sync ?
21434 null :
21435 $this->get_network_install_blog_id()
21436 );
21437 } else {
21438 if ( ! is_object( $this->_license ) ) {
21439 $this->maybe_update_whitelabel_flag(
21440 FS_Plugin_License::is_valid_id( $site->license_id ) ?
21441 $this->get_license_by_id( $site->license_id ) :
21442 null
21443 );
21444 } else {
21445 $this->maybe_update_whitelabel_flag( $this->_license );
21446
21447 if ( $this->_license->is_expired() ) {
21448 if ( ! $this->has_features_enabled_license() ) {
21449 $this->_deactivate_license();
21450 $plan_change = 'downgraded';
21451 } else {
21452 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
21453
21454 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
21455 /**
21456 * Show the expired license notice every 14 days.
21457 *
21458 * @author Leo Fajardo (@leorw)
21459 * @since 2.3.1
21460 */
21461 $plan_change = 'expired';
21462 }
21463 }
21464 }
21465 }
21466
21467 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
21468 $this->_sync_site_subscription( $this->_license );
21469 }
21470 }
21471
21472 if ( ! $this->is_addon() &&
21473 $this->_site->is_beta() !== $site->is_beta()
21474 ) {
21475 // Beta flag updated.
21476 $this->_site = $site;
21477
21478 $this->_store_site(
21479 true,
21480 $is_site_level_sync ?
21481 null :
21482 $this->get_network_install_blog_id()
21483 );
21484 }
21485
21486 if ( $this->is_addon() || $this->has_addons() ) {
21487 /**
21488 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21489 * an updated valid user licenses collection will be fetched from the server which is used to also
21490 * update the account add-ons (add-ons the user has licenses for).
21491 *
21492 * @author Leo Fajardo (@leorw)
21493 * @since 2.2.4
21494 */
21495 $this->purge_valid_user_licenses_cache();
21496 }
21497 }
21498
21499 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21500
21501 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
21502 switch ( $plan_change ) {
21503 case 'none':
21504 if ( ! $background && is_admin() ) {
21505 $plan = $this->is_trial() ?
21506 $this->get_trial_plan() :
21507 $this->get_plan();
21508
21509 if ( $plan->is_free() ) {
21510 $this->_admin_notices->add(
21511 sprintf(
21512 $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' ),
21513 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
21514 ) . ' ' . sprintf(
21515 '<a href="%s">%s</a>',
21516 $this->contact_url(
21517 'bug',
21518 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' ),
21519 strtoupper( $plan->name )
21520 )
21521 ),
21522 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21523 ),
21524 $hmm_text
21525 );
21526 }
21527 }
21528 break;
21529 case 'upgraded':
21530 case 'activated':
21531 $this->add_after_plan_activation_or_upgrade_instructions_notice( 'upgraded' === $plan_change );
21532
21533 $this->_admin_notices->remove_sticky( array(
21534 'trial_started',
21535 'trial_promotion',
21536 'trial_expired',
21537 'activation_complete',
21538 'license_expired',
21539 ) );
21540 break;
21541 case 'changed':
21542 $this->_admin_notices->add_sticky(
21543 sprintf(
21544 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
21545 $this->get_plan_title()
21546 ),
21547 'plan_changed'
21548 );
21549
21550 $this->_admin_notices->remove_sticky( array(
21551 'trial_started',
21552 'trial_promotion',
21553 'trial_expired',
21554 'activation_complete',
21555 ) );
21556 break;
21557 case 'downgraded':
21558 $this->_admin_notices->add_sticky(
21559 ($this->has_free_plan() ?
21560 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 ) :
21561 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21562 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) ) ),
21563 'license_expired',
21564 $hmm_text
21565 );
21566 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21567 break;
21568 case 'cancelled':
21569 $this->_admin_notices->add(
21570 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
21571 sprintf(
21572 '<a href="%s">%s</a>',
21573 $this->contact_url( 'bug' ),
21574 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21575 ),
21576 $hmm_text,
21577 'error'
21578 );
21579 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21580 break;
21581 case 'expired':
21582 $this->_admin_notices->add_sticky(
21583 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 ),
21584 'license_expired',
21585 $hmm_text
21586 );
21587
21588 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
21589
21590 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21591 break;
21592 case 'trial_started':
21593 $this->add_complete_upgrade_instructions_notice(
21594 sprintf(
21595 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
21596 '<i>' . $this->get_plugin_name() . '</i>'
21597 ),
21598 'trial_started',
21599 $this->get_trial_plan()->title
21600 );
21601
21602 $this->_admin_notices->remove_sticky( array(
21603 'trial_promotion',
21604 ) );
21605 break;
21606 case 'trial_expired':
21607 $this->_admin_notices->add_sticky(
21608 ($this->has_free_plan() ?
21609 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
21610 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21611 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))),
21612 'trial_expired',
21613 $hmm_text
21614 );
21615 $this->_admin_notices->remove_sticky( array(
21616 'trial_started',
21617 'trial_promotion',
21618 'plan_upgraded',
21619 ) );
21620 break;
21621 }
21622 }
21623
21624 if ( 'none' !== $plan_change ) {
21625 if (
21626 ! is_object( $this->_license ) ||
21627 ! $this->_license->is_whitelabeled
21628 ) {
21629 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
21630 }
21631
21632 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
21633 }
21634 }
21635
21636 /**
21637 * @author Leo Fajardo (@leorw)
21638 * @since 2.5.4
21639 *
21640 * @param mixed $result
21641 *
21642 * @return string
21643 */
21644 private function generate_api_blocked_notice_message_from_result( $result ) {
21645 $api_domains = $this->apply_filters( 'api_domains', array(
21646 'api.freemius.com',
21647 'wp.freemius.com',
21648 ) );
21649
21650 $api_domains_list_items = '';
21651
21652 foreach( $api_domains as $api_domain ) {
21653 $api_domains_list_items .= "<li>{$api_domain}</li>";
21654 }
21655
21656 $error_message = sprintf(
21657 $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' ),
21658 $this->get_plugin_name(),
21659 "<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>"
21660 );
21661
21662 $error_message =
21663 "<div>{$error_message}</div>" .
21664 '<div class="fs-api-request-error-details" style="display: none">' .
21665 '<strong>' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . '</strong><br>' .
21666 $result->error->message .
21667 '</div>';
21668
21669 return $error_message;
21670 }
21671
21672 /**
21673 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
21674 *
21675 * @author Vova Feldman (@svovaf)
21676 * @since 2.0.0
21677 */
21678 public function _open_license_activation_dialog_box() {
21679 $vars = array( 'license_id' => $this->_site->license_id );
21680 fs_require_once_template( 'js/open-license-activation.php', $vars );
21681 }
21682
21683 /**
21684 * @author Vova Feldman (@svovaf)
21685 * @since 1.0.5
21686 *
21687 * @param bool $background
21688 * @param FS_Plugin_License|null $premium_license
21689 */
21690 protected function _activate_license( $background = false, $premium_license = null ) {
21691 $this->_logger->entrance();
21692
21693 if ( is_null( $premium_license ) ) {
21694 $license_id = fs_request_get( 'license_id' );
21695
21696 if ( is_object( $this->_site ) &&
21697 FS_Plugin_License::is_valid_id( $license_id ) &&
21698 $license_id == $this->_site->license_id
21699 ) {
21700 // License is already activated.
21701 return;
21702 }
21703
21704 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
21705 $this->_get_license_by_id( $license_id ) :
21706 $this->_get_available_premium_license();
21707 }
21708
21709 if ( ! is_object( $premium_license ) ) {
21710 return;
21711 }
21712
21713 if ( ! is_object( $this->_site ) ) {
21714 // Not yet opted-in.
21715 $user = $this->get_current_or_network_user();
21716 if ( ! is_object( $user ) ) {
21717 $user = self::_get_user_by_id( $premium_license->user_id );
21718 }
21719
21720 if ( is_object( $user ) ) {
21721 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
21722 } else {
21723 $this->opt_in(
21724 false,
21725 false,
21726 false,
21727 $premium_license->secret_key
21728 );
21729
21730 return;
21731 }
21732 }
21733
21734
21735 /**
21736 * If the premium license is already associated with the install, just
21737 * update the license reference (activation is not required).
21738 *
21739 * @since 1.1.9
21740 */
21741 if ( $premium_license->id == $this->_site->license_id ) {
21742 // License is already activated.
21743 $this->_update_site_license( $premium_license );
21744 $this->_store_account();
21745
21746 return;
21747 }
21748
21749 if ( $this->_site->user_id != $premium_license->user_id ) {
21750 $api_request_params = array( 'license_key' => $premium_license->secret_key );
21751 } else {
21752 $api_request_params = array();
21753 }
21754
21755 $api = $this->get_api_site_scope();
21756 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
21757
21758 if ( ! $this->is_api_result_entity( $license ) ) {
21759 if ( ! $background ) {
21760 $this->_admin_notices->add( sprintf(
21761 '%s %s',
21762 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
21763 ( is_object( $license ) && isset( $license->error ) ?
21764 $license->error->message :
21765 sprintf( '%s<br><code>%s</code>',
21766 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
21767 var_export( $license, true )
21768 )
21769 )
21770 ),
21771 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
21772 'error'
21773 );
21774 }
21775
21776 return;
21777 }
21778
21779 $premium_license = new FS_Plugin_License( $license );
21780
21781 // Updated site plan.
21782 $site = $this->get_api_site_scope()->get( '/', true );
21783 if ( $this->is_api_result_entity( $site ) ) {
21784 $this->_site = new FS_Site( $site );
21785 }
21786 $this->_update_site_license( $premium_license );
21787
21788 $this->_store_account();
21789
21790 if ( $this->is_addon() || $this->has_addons() ) {
21791 /**
21792 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21793 * an updated valid user licenses collection will be fetched from the server which is used to also
21794 * update the account add-ons (add-ons the user has licenses for).
21795 *
21796 * @author Leo Fajardo (@leorw)
21797 * @since 2.2.4
21798 */
21799 $this->purge_valid_user_licenses_cache();
21800 }
21801
21802 if ( ! $background ) {
21803 $this->add_complete_upgrade_instructions_notice(
21804 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ),
21805 'license_activated'
21806 );
21807 }
21808
21809 $this->_admin_notices->remove_sticky( array(
21810 'trial_promotion',
21811 'license_expired',
21812 ) );
21813 }
21814
21815 /**
21816 * @author Vova Feldman (@svovaf)
21817 * @since 1.0.5
21818 *
21819 * @param bool $show_notice
21820 */
21821 protected function _deactivate_license( $show_notice = true ) {
21822 $this->_logger->entrance();
21823
21824 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21825
21826 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
21827 $this->_admin_notices->add(
21828 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() ),
21829 $hmm_text
21830 );
21831
21832 return;
21833 }
21834
21835 $api = $this->get_api_site_scope();
21836 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
21837
21838 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
21839 }
21840
21841 /**
21842 * @author Leo Fajardo (@leorw)
21843 * @since 2.2.1
21844 *
21845 * @param FS_Plugin_License $license
21846 * @param bool|string $hmm_text
21847 * @param bool $show_notice
21848 */
21849 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
21850 if ( isset( $license->error ) ) {
21851 $this->_admin_notices->add(
21852 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
21853 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
21854 $hmm_text,
21855 'error'
21856 );
21857
21858 return;
21859 }
21860
21861 // Update license cache.
21862 if ( is_array( $this->_licenses ) ) {
21863 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
21864 if ( $license->id == $this->_licenses[ $i ]->id ) {
21865 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
21866 }
21867 }
21868 }
21869
21870 // Update site plan to default.
21871 $this->_sync_plans();
21872 $this->_site->plan_id = $this->_plans[0]->id;
21873 // Unlink license from site.
21874 $this->_update_site_license( null );
21875
21876 $this->_store_account();
21877
21878 if ( $show_notice ) {
21879 $this->_admin_notices->add(
21880 sprintf( $this->is_only_premium() ?
21881 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
21882 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
21883 $this->get_plan_title()
21884 ),
21885 $this->get_text_inline( 'O.K', 'ok' )
21886 );
21887 }
21888
21889 $this->_admin_notices->remove_sticky( array(
21890 'plan_upgraded',
21891 'license_activated',
21892 ) );
21893 }
21894
21895 /**
21896 * Site plan downgrade.
21897 *
21898 * @author Vova Feldman (@svovaf)
21899 * @since 1.0.4
21900 *
21901 * @return object
21902 *
21903 * @uses FS_Api
21904 */
21905 private function _downgrade_site() {
21906 $this->_logger->entrance();
21907
21908 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
21909
21910 $api = $this->get_api_site_scope();
21911 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
21912
21913 $plan_downgraded = false;
21914 $plan = false;
21915 if ( $this->is_api_result_entity( $site ) ) {
21916 $prev_plan_id = $this->_site->plan_id;
21917
21918 // Update new site plan id.
21919 $this->_site->plan_id = $site->plan_id;
21920
21921 $plan = $this->get_plan();
21922 $subscription = $this->_sync_site_subscription( $this->_license );
21923
21924 // Plan downgraded if plan was changed or subscription was cancelled.
21925 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
21926 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
21927 } else {
21928 // handle different error cases.
21929 $this->handle_license_deactivation_result(
21930 $site,
21931 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
21932 );
21933 }
21934
21935 if ( ! $plan_downgraded ) {
21936 return (object) array(
21937 'error' => (object) array(
21938 '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' )
21939 )
21940 );
21941 }
21942
21943 // Remove previous sticky message about upgrade (if exist).
21944 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21945
21946 $this->_admin_notices->add(
21947 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
21948 $plan->title,
21949 human_time_diff( time(), strtotime( $this->_license->expiration ) )
21950 )
21951 );
21952
21953 // Store site updates.
21954 $this->_store_site();
21955
21956 if ( $deactivate_license &&
21957 ! FS_Plugin_License::is_valid_id( $site->license_id )
21958 ) {
21959 if ( $this->_site->is_localhost() ) {
21960 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
21961 } else {
21962 $this->_license->activated = max( 0, $this->_license->activated - 1 );
21963 }
21964
21965 // Handle successful license deactivation result.
21966 $this->handle_license_deactivation_result( $this->_license );
21967 }
21968
21969 return $site;
21970 }
21971
21972 /**
21973 * @author Vova Feldman (@svovaf)
21974 * @since 1.1.8.1
21975 *
21976 * @param bool|string $plan_name
21977 *
21978 * @return bool If trial was successfully started.
21979 */
21980 function start_trial( $plan_name = false ) {
21981 $this->_logger->entrance();
21982
21983 // Alias.
21984 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
21985
21986 if ( $this->is_trial() ) {
21987 // Already in trial mode.
21988 $this->_admin_notices->add(
21989 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
21990 $oops_text,
21991 'error'
21992 );
21993
21994 return false;
21995 }
21996
21997 if ( $this->_site->is_trial_utilized() ) {
21998 // Trial was already utilized.
21999 $this->_admin_notices->add(
22000 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
22001 $oops_text,
22002 'error'
22003 );
22004
22005 return false;
22006 }
22007
22008 if ( false !== $plan_name ) {
22009 $plan = $this->get_plan_by_name( $plan_name );
22010
22011 if ( false === $plan ) {
22012 // Plan doesn't exist.
22013 $this->_admin_notices->add(
22014 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
22015 $oops_text,
22016 'error'
22017 );
22018
22019 return false;
22020 }
22021
22022 if ( ! $plan->has_trial() ) {
22023 // Plan doesn't exist.
22024 $this->_admin_notices->add(
22025 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
22026 $oops_text,
22027 'error'
22028 );
22029
22030 return false;
22031 }
22032 } else {
22033 if ( ! $this->has_trial_plan() ) {
22034 // None of the plans have a trial.
22035 $this->_admin_notices->add(
22036 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
22037 $oops_text,
22038 'error'
22039 );
22040
22041 return false;
22042 }
22043
22044 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
22045
22046 $plan = $plans_with_trial[0];
22047 }
22048
22049 $api = $this->get_api_site_scope();
22050 $plan = $api->call( "plans/{$plan->id}/trials.json", 'post' );
22051
22052 if ( ! $this->is_api_result_entity( $plan ) ) {
22053 // Some API error while trying to start the trial.
22054 $this->_admin_notices->add(
22055 $this->get_api_error_message( $plan ),
22056 $oops_text,
22057 'error'
22058 );
22059
22060 return false;
22061 }
22062
22063 // Sync license.
22064 $this->_sync_license();
22065
22066 return $this->is_trial();
22067 }
22068
22069 /**
22070 * Cancel site trial.
22071 *
22072 * @author Vova Feldman (@svovaf)
22073 * @since 1.0.9
22074 *
22075 * @return object
22076 *
22077 * @uses FS_Api
22078 */
22079 private function _cancel_trial() {
22080 $this->_logger->entrance();
22081
22082 if ( ! $this->is_trial() ) {
22083 return (object) array(
22084 'error' => (object) array(
22085 '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' )
22086 )
22087 );
22088 }
22089
22090 $trial_plan = $this->get_trial_plan();
22091
22092 $api = $this->get_api_site_scope();
22093 $site = $api->call( 'trials.json', 'delete' );
22094
22095 $trial_cancelled = false;
22096
22097 if ( $this->is_api_result_entity( $site ) ) {
22098 $prev_trial_ends = $this->_site->trial_ends;
22099
22100 if ( $this->is_paid_trial() ) {
22101 $this->_license->expiration = $site->trial_ends;
22102 $this->_license->is_cancelled = true;
22103 $this->_update_site_license( $this->_license );
22104 $this->_store_licenses();
22105
22106 // Clear subscription reference.
22107 $this->_sync_site_subscription( null );
22108 }
22109
22110 // Update site info.
22111 $this->_site = new FS_Site( $site );
22112
22113 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
22114 } else {
22115 // @todo handle different error cases.
22116 }
22117
22118 if ( ! $trial_cancelled ) {
22119 return (object) array(
22120 'error' => (object) array(
22121 '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' )
22122 )
22123 );
22124 }
22125
22126 // Remove previous sticky messages about upgrade or trial (if exist).
22127 $this->_admin_notices->remove_sticky( array(
22128 'trial_started',
22129 'trial_promotion',
22130 'plan_upgraded',
22131 ) );
22132
22133 // Store site updates.
22134 $this->_store_site();
22135
22136 if ( ! $this->is_addon() ||
22137 ! $this->deactivate_premium_only_addon_without_license( true )
22138 ) {
22139 $this->_admin_notices->add(
22140 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
22141 );
22142 }
22143
22144 return $site;
22145 }
22146
22147 /**
22148 * @author Vova Feldman (@svovaf)
22149 * @since 1.0.6
22150 *
22151 * @param bool|number $plugin_id
22152 *
22153 * @return bool
22154 */
22155 private function _is_addon_id( $plugin_id ) {
22156 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
22157 }
22158
22159 /**
22160 * Check if user eligible to download premium version updates.
22161 *
22162 * @author Vova Feldman (@svovaf)
22163 * @since 1.0.6
22164 *
22165 * @return bool
22166 */
22167 private function _can_download_premium() {
22168 return $this->has_any_active_valid_license() ||
22169 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
22170 }
22171
22172 /**
22173 *
22174 * @author Vova Feldman (@svovaf)
22175 * @since 1.0.6
22176 *
22177 * @param bool|number $addon_id
22178 * @param string $type "json" or "zip"
22179 *
22180 * @return string
22181 */
22182 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
22183
22184 $is_addon = $this->_is_addon_id( $addon_id );
22185
22186 $is_premium = null;
22187 if ( ! $is_addon ) {
22188 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
22189 } else if ( $this->is_addon_activated( $addon_id ) ) {
22190 $fs_addon = self::get_instance_by_id( $addon_id );
22191 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
22192 }
22193
22194 // If add-on, then append add-on ID.
22195 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
22196 '/updates/latest.' . $type;
22197
22198 // If add-on and not yet activated, try to fetch based on server licensing.
22199 if ( is_bool( $is_premium ) ) {
22200 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
22201 }
22202
22203 if ( $this->has_secret_key() ) {
22204 $endpoint = add_query_arg( 'type', 'all', $endpoint );
22205 } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) {
22206 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
22207 }
22208
22209 return $endpoint;
22210 }
22211
22212 /**
22213 * @author Vova Feldman (@svovaf)
22214 * @since 1.0.4
22215 *
22216 * @param bool|number $addon_id
22217 * @param bool $flush Since 1.1.7.3
22218 * @param int $expiration Since 1.2.2.7
22219 * @param bool|string $newer_than Since 2.2.1
22220 * @param bool|string $fetch_readme Since 2.2.1
22221 *
22222 * @return object|false Plugin latest tag info.
22223 */
22224 function _fetch_latest_version(
22225 $addon_id = false,
22226 $flush = true,
22227 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22228 $newer_than = false,
22229 $fetch_readme = true
22230 ) {
22231 $this->_logger->entrance();
22232
22233 if ( $this->is_unresolved_clone( true ) ) {
22234 return false;
22235 }
22236
22237 $switch_to_blog_id = null;
22238
22239 /**
22240 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
22241 * @since 1.1.7.4 Also check updates for add-ons.
22242 */
22243 if (
22244 ( ! $this->is_registered() || ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed() ) &&
22245 ! $this->_is_addon_id( $addon_id )
22246 ) {
22247 if ( ! is_multisite() ) {
22248 return false;
22249 }
22250
22251 $installs_map = $this->get_blog_install_map();
22252
22253 foreach ( $installs_map as $blog_id => $install ) {
22254 if ( ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed( $blog_id ) ) {
22255 continue;
22256 }
22257
22258 /**
22259 * @var FS_Site $install
22260 */
22261 if ( $install->is_trial() ) {
22262 $switch_to_blog_id = $blog_id;
22263 break;
22264 }
22265
22266 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
22267 $license = $this->get_license_by_id( $install->license_id );
22268
22269 if ( is_object( $license ) && $license->is_features_enabled() ) {
22270 $switch_to_blog_id = $blog_id;
22271 break;
22272 }
22273 }
22274 }
22275
22276 if ( is_null( $switch_to_blog_id ) ) {
22277 return false;
22278 }
22279 }
22280
22281 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
22282 get_current_blog_id() :
22283 0;
22284
22285 if ( is_numeric( $switch_to_blog_id ) ) {
22286 $this->switch_to_blog( $switch_to_blog_id );
22287 }
22288
22289 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
22290
22291 if ( ! empty( $newer_than ) ) {
22292 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
22293 }
22294
22295 if ( true === $fetch_readme ) {
22296 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
22297 }
22298
22299 $tag = $this->get_api_site_or_plugin_scope()->get(
22300 $latest_version_endpoint,
22301 $flush,
22302 $expiration
22303 );
22304
22305 if ( is_numeric( $switch_to_blog_id ) ) {
22306 $this->switch_to_blog( $current_blog_id );
22307 }
22308
22309 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
22310
22311 $this->_logger->departure( 'Latest version ' . $latest_version );
22312
22313 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
22314 }
22315
22316 #----------------------------------------------------------------------------------
22317 #region Download Plugin
22318 #----------------------------------------------------------------------------------
22319
22320 /**
22321 * Download latest plugin version, based on plan.
22322 *
22323 * Not like _download_latest(), this will redirect the page
22324 * to secure download url to prevent dual download (from FS to WP server,
22325 * and then from WP server to the client / browser).
22326 *
22327 * @author Vova Feldman (@svovaf)
22328 * @since 1.0.9
22329 *
22330 * @param bool|number $plugin_id
22331 *
22332 * @uses FS_Api
22333 * @uses wp_redirect()
22334 */
22335 private function download_latest_directly( $plugin_id = false ) {
22336 $this->_logger->entrance();
22337
22338 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
22339 }
22340
22341 /**
22342 * Get latest plugin FS API download URL.
22343 *
22344 * @author Vova Feldman (@svovaf)
22345 * @since 1.0.9
22346 *
22347 * @param bool|number $plugin_id
22348 *
22349 * @return string
22350 */
22351 private function get_latest_download_api_url( $plugin_id = false ) {
22352 $this->_logger->entrance();
22353
22354 $download_api_url = $this->get_api_site_scope()->get_signed_url(
22355 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
22356 );
22357
22358 return str_replace( 'http:', 'https:', $download_api_url );
22359 }
22360
22361 /**
22362 * Get payment invoice URL.
22363 *
22364 * @author Vova Feldman (@svovaf)
22365 * @since 1.2.0
22366 *
22367 * @param bool|number $payment_id
22368 *
22369 * @return string
22370 */
22371 function _get_invoice_api_url( $payment_id = false ) {
22372 $this->_logger->entrance();
22373
22374 $url = $this->get_api_user_scope()->get_signed_url(
22375 "/payments/{$payment_id}/invoice.pdf"
22376 );
22377
22378 if ( ! fs_starts_with( $url, 'https://' ) ) {
22379 // Always use HTTPS for invoices.
22380 $url = 'https' . substr( $url, 4 );
22381 }
22382
22383 return $url;
22384 }
22385
22386 /**
22387 * Get latest plugin download link.
22388 *
22389 * @author Vova Feldman (@svovaf)
22390 * @since 1.0.9
22391 *
22392 * @param string $label
22393 * @param bool|number $plugin_id
22394 *
22395 * @return string
22396 */
22397 private function get_latest_download_link( $label, $plugin_id = false ) {
22398 return sprintf(
22399 '<a target="_blank" rel="noopener" href="%s">%s</a>',
22400 $this->_get_latest_download_local_url( $plugin_id ),
22401 $label
22402 );
22403 }
22404
22405 /**
22406 * Get latest plugin download local URL.
22407 *
22408 * @author Vova Feldman (@svovaf)
22409 * @since 1.0.9
22410 *
22411 * @param bool|number $plugin_id
22412 *
22413 * @return string
22414 */
22415 function _get_latest_download_local_url( $plugin_id = false ) {
22416 // Add timestamp to protect from caching.
22417 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
22418
22419 if ( ! empty( $plugin_id ) ) {
22420 $params['plugin_id'] = $plugin_id;
22421 } else if ( $this->is_addon() ) {
22422 $params['plugin_id'] = $this->get_id();
22423 }
22424
22425 $fs = $this->is_addon() ?
22426 $this->get_parent_instance() :
22427 $this;
22428
22429 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
22430 }
22431
22432 #endregion Download Plugin ------------------------------------------------------------------
22433
22434 /**
22435 * @author Vova Feldman (@svovaf)
22436 * @since 1.0.4
22437 *
22438 * @uses FS_Api
22439 *
22440 * @param bool $background Hints the method if it's a background updates check. If false, it means that
22441 * was initiated by the admin.
22442 * @param bool|number $plugin_id
22443 * @param bool $flush Since 1.1.7.3
22444 * @param int $expiration Since 1.2.2.7
22445 * @param bool|string $newer_than Since 2.2.1
22446 */
22447 private function check_updates(
22448 $background = false,
22449 $plugin_id = false,
22450 $flush = true,
22451 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22452 $newer_than = false
22453 ) {
22454 $this->_logger->entrance();
22455
22456 // Check if there's a newer version for download.
22457 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than );
22458
22459 $update = null;
22460 if ( is_object( $new_version ) ) {
22461 $update = new FS_Plugin_Tag( $new_version );
22462
22463 if ( ! $background ) {
22464 $this->_admin_notices->add(
22465 sprintf(
22466 /* translators: %s: Numeric version number (e.g. '2.1.9' */
22467 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
22468 $update->version,
22469 sprintf(
22470 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22471 $this->get_account_url( 'download_latest' ),
22472 sprintf(
22473 /* translators: %s: plan name (e.g. latest "Professional" version) */
22474 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
22475 $this->get_plan_title()
22476 )
22477 )
22478 ),
22479 $this->get_text_inline( 'New', 'new' ) . '!'
22480 );
22481 }
22482 } else if ( false === $new_version && ! $background ) {
22483 $this->_admin_notices->add(
22484 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
22485 $this->get_text_inline( 'You are all good!', 'you-are-good' )
22486 );
22487 }
22488
22489 $this->_store_update( $update, true, $plugin_id );
22490 }
22491
22492 /**
22493 * @author Vova Feldman (@svovaf)
22494 * @since 1.0.4
22495 *
22496 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
22497 *
22498 * @return FS_Plugin[]
22499 *
22500 * @uses FS_Api
22501 */
22502 private function sync_addons( $flush = false ) {
22503 $this->_logger->entrance();
22504
22505 $api = $this->get_api_site_or_plugin_scope();
22506
22507 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
22508
22509 /**
22510 * @since 1.2.1
22511 *
22512 * If there's a cached version of the add-ons and not asking
22513 * for a flush, just use the currently stored add-ons.
22514 */
22515 if ( ! $flush && $api->is_cached( $path ) ) {
22516 $addons = self::get_all_addons();
22517
22518 return isset( $addons[ $this->_plugin->id ] ) ?
22519 $addons[ $this->_plugin->id ] :
22520 array();
22521 }
22522
22523 $result = $api->get( $path, $flush );
22524
22525 $addons = array();
22526 if ( $this->is_api_result_object( $result, 'plugins' ) &&
22527 is_array( $result->plugins )
22528 ) {
22529 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
22530 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
22531 }
22532
22533 $this->_store_addons( $addons, true );
22534 }
22535
22536 return $addons;
22537 }
22538
22539 /**
22540 * Handle user email update.
22541 *
22542 * @author Vova Feldman (@svovaf)
22543 * @since 1.0.3
22544 * @uses FS_Api
22545 *
22546 * @param string $new_email
22547 *
22548 * @return object
22549 */
22550 private function update_email( $new_email ) {
22551 $this->_logger->entrance();
22552
22553 $api = $this->get_api_user_scope();
22554 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
22555 'email' => $new_email,
22556 'after_email_confirm_url' => $this->_get_admin_page_url(
22557 'account',
22558 array( 'fs_action' => 'sync_user' )
22559 ),
22560 ) );
22561
22562 if ( ! isset( $user->error ) ) {
22563 $this->_user->email = $user->email;
22564 $this->_user->is_verified = $user->is_verified;
22565 $this->_store_user();
22566 } else {
22567 // handle different error cases.
22568 }
22569
22570 return $user;
22571 }
22572
22573 #----------------------------------------------------------------------------------
22574 #region API Error Handling
22575 #----------------------------------------------------------------------------------
22576
22577 /**
22578 * @author Vova Feldman (@svovaf)
22579 * @since 1.1.1
22580 *
22581 * @param mixed $result
22582 *
22583 * @return bool Is API result contains an error.
22584 */
22585 private function is_api_error( $result ) {
22586 return FS_Api::is_api_error( $result );
22587 }
22588
22589 /**
22590 * Checks if given API result is a non-empty and not an error object.
22591 *
22592 * @author Vova Feldman (@svovaf)
22593 * @since 1.2.1.5
22594 *
22595 * @param mixed $result
22596 * @param string|null $required_property Optional property we want to verify that is set.
22597 *
22598 * @return bool
22599 */
22600 function is_api_result_object( $result, $required_property = null ) {
22601 return FS_Api::is_api_result_object( $result, $required_property );
22602 }
22603
22604 /**
22605 * Checks if given API result is a non-empty entity object with non-empty ID.
22606 *
22607 * @author Vova Feldman (@svovaf)
22608 * @since 1.2.1.5
22609 *
22610 * @param mixed $result
22611 *
22612 * @return bool
22613 */
22614 private function is_api_result_entity( $result ) {
22615 return FS_Api::is_api_result_entity( $result );
22616 }
22617
22618 #endregion
22619
22620 /**
22621 * Make sure a given argument is an array of a specific type.
22622 *
22623 * @author Vova Feldman (@svovaf)
22624 * @since 1.2.1.5
22625 *
22626 * @param mixed $array
22627 * @param string $class
22628 *
22629 * @return bool
22630 */
22631 private function is_array_instanceof( $array, $class ) {
22632 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
22633 }
22634
22635 /**
22636 * Start install ownership change.
22637 *
22638 * @author Vova Feldman (@svovaf)
22639 * @since 1.1.1
22640 * @uses FS_Api
22641 *
22642 * @param string $candidate_email
22643 * @param string $transfer_type
22644 *
22645 * @return bool Is ownership change successfully initiated.
22646 */
22647 private function init_change_owner( $candidate_email, $transfer_type ) {
22648 $this->_logger->entrance();
22649
22650 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
22651 $install_ids = array();
22652
22653 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
22654 $install = $install_info['install'];
22655
22656 if ( $this->_user->id != $install->user_id ) {
22657 // Skip add-on installs that are not owned by the parent product's install's owner.
22658 continue;
22659 }
22660
22661 $install_ids[ $slug ] = $install->id;
22662 }
22663
22664 $api = $this->get_api_site_scope();
22665 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
22666 'email' => $candidate_email,
22667 'transfer_type' => $transfer_type,
22668 'install_ids' => implode( ',', array_values( $install_ids ) ),
22669 'after_confirm_url' => $this->_get_admin_page_url(
22670 'account',
22671 array( 'fs_action' => 'change_owner' )
22672 ),
22673 ) );
22674
22675 return ! $this->is_api_error( $result );
22676 }
22677
22678 /**
22679 * Handle install ownership change.
22680 *
22681 * @author Vova Feldman (@svovaf)
22682 * @since 1.1.1
22683 * @uses FS_Api
22684 *
22685 * @return bool Was ownership change successfully complete.
22686 */
22687 private function complete_change_owner() {
22688 $this->_logger->entrance();
22689
22690 $install_ids = fs_request_get( 'install_ids' );
22691
22692 if ( ! empty( $install_ids ) ) {
22693 $install_ids = explode( ',', $install_ids );
22694
22695 foreach ( $install_ids as $key => $install_id ) {
22696 if ( ! FS_Site::is_valid_id( $install_id ) ) {
22697 unset( $install_ids[ $key ] );
22698 }
22699 }
22700 }
22701
22702 if ( ! is_array( $install_ids ) ) {
22703 $install_ids = array();
22704 }
22705
22706 $user = new FS_User();
22707 $user->id = fs_request_get( 'user_id' );
22708 $user->public_key = fs_request_get_raw( 'user_public_key' );
22709 $user->secret_key = fs_request_get_raw( 'user_secret_key' );
22710
22711 $prev_user = $this->_user;
22712 $this->_user = $user;
22713
22714 $result = $this->get_api_user_scope( true )->get(
22715 "/installs.json?install_ids=" . implode( ',', $install_ids )
22716 );
22717
22718 $current_blog_sites = self::get_all_sites( $this->get_module_type() );
22719
22720 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22721 $site_id_slug_map = array();
22722
22723 foreach ( $current_blog_sites as $slug => $site ) {
22724 $site_id_slug_map[ $site->id ] = $slug;
22725 }
22726
22727 foreach ( $result->installs as $install ) {
22728 $site = new FS_Site( $install );
22729
22730 if ( ! isset( $site_id_slug_map[ $install->id ] ) ) {
22731 continue;
22732 }
22733
22734 $current_blog_sites[ $site_id_slug_map[ $install->id ] ] = clone $site;
22735
22736 if ( $this->_site->id == $site->id ) {
22737 $this->_site = $site;
22738 }
22739 }
22740 }
22741
22742 // Validate install's user and given user.
22743 if ( $user->id != $this->_site->user_id ) {
22744 $this->_user = $prev_user;
22745
22746 return false;
22747 }
22748
22749 $this->set_account_option( 'sites', $current_blog_sites, true );
22750
22751 // Fetch new user information.
22752 $user_result = $this->get_api_user_scope( true )->get();
22753 $user = new FS_User( $user_result );
22754 $this->_user = $user;
22755
22756 $this->_set_account( $user, $this->_site );
22757
22758 $remove_user = true;
22759 $all_modules_sites = self::get_all_modules_sites();
22760
22761 foreach ( $all_modules_sites as $sites_by_module_type ) {
22762 foreach ( $sites_by_module_type as $sites_by_slug ) {
22763 foreach ( $sites_by_slug as $site ) {
22764 if ( $prev_user->id == $site->user_id ) {
22765 $remove_user = false;
22766 break;
22767 }
22768 }
22769
22770 if ( ! $remove_user ) {
22771 break;
22772 }
22773 }
22774
22775 if ( ! $remove_user ) {
22776 break;
22777 }
22778 }
22779
22780 if ( $remove_user ) {
22781 $users = self::get_all_users();
22782
22783 if ( isset( $users[ $prev_user->id ] ) ) {
22784 unset( $users[ $prev_user->id ] );
22785 } else {
22786 // If the prev user wasn't found by the key, iterate over the users collection.
22787 foreach ( $users as $key => $user ) {
22788 if ( $user->id == $prev_user->id ) {
22789 unset( $users[ $key ] );
22790 break;
22791 }
22792 }
22793 }
22794
22795 $this->set_account_option( 'users', $users, true );
22796 }
22797
22798 return true;
22799 }
22800
22801 /**
22802 * Completes ownership change by license.
22803 *
22804 * @author Leo Fajardo (@leorw)
22805 * @since 2.3.2
22806 *
22807 * @param number $user_id
22808 * @param array[string]number $install_ids_by_slug_map
22809 *
22810 */
22811 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
22812 $this->_logger->entrance();
22813
22814 $this->sync_user_by_current_install( $user_id );
22815
22816 $result = $this->get_api_user_scope( true )->get(
22817 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
22818 );
22819
22820 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22821 $sites = self::get_all_sites( $this->get_module_type() );
22822 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
22823
22824 foreach ( $result->installs as $install ) {
22825 $site = new FS_Site( $install );
22826
22827 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
22828 }
22829
22830 $this->set_account_option( 'sites', $sites, true );
22831 }
22832 }
22833
22834 /**
22835 * Handle user name update.
22836 *
22837 * @author Vova Feldman (@svovaf)
22838 * @since 1.0.9
22839 * @uses FS_Api
22840 *
22841 * @return object
22842 */
22843 private function update_user_name() {
22844 $this->_logger->entrance();
22845 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
22846
22847 $api = $this->get_api_user_scope();
22848 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
22849 'name' => $name,
22850 ) );
22851
22852 if ( ! isset( $user->error ) ) {
22853 $this->_user->first = $user->first;
22854 $this->_user->last = $user->last;
22855 $this->_store_user();
22856 } else {
22857 // handle different error cases.
22858
22859 }
22860
22861 return $user;
22862 }
22863
22864 /**
22865 * Verify user email.
22866 *
22867 * @author Vova Feldman (@svovaf)
22868 * @since 1.0.3
22869 * @uses FS_Api
22870 */
22871 private function verify_email() {
22872 $this->_handle_account_user_sync();
22873
22874 if ( $this->_user->is_verified() ) {
22875 return;
22876 }
22877
22878 $api = $this->get_api_site_scope();
22879 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
22880 'after_email_confirm_url' => $this->_get_admin_page_url(
22881 'account',
22882 array( 'fs_action' => 'sync_user' )
22883 )
22884 ) );
22885
22886 if ( ! isset( $result->error ) ) {
22887 $this->_admin_notices->add( sprintf(
22888 $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' ),
22889 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
22890 ) );
22891 } else {
22892 // handle different error cases.
22893
22894 }
22895 }
22896
22897 /**
22898 * @author Vova Feldman (@svovaf)
22899 * @since 1.1.2
22900 *
22901 * @param array $params
22902 * @param bool|null $network
22903 *
22904 * @return string
22905 */
22906 function get_activation_url( $params = array(), $network = null ) {
22907 if ( $this->is_addon() && $this->has_free_plan() ) {
22908 /**
22909 * @author Vova Feldman (@svovaf)
22910 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
22911 */
22912 return $this->get_parent_instance()->get_activation_url( $params );
22913 }
22914
22915 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
22916 }
22917
22918 /**
22919 * @author Vova Feldman (@svovaf)
22920 * @since 1.2.1.5
22921 *
22922 * @param array $params
22923 *
22924 * @return string
22925 */
22926 function get_reconnect_url( $params = array() ) {
22927 $params['fs_action'] = 'reset_anonymous_mode';
22928 $params['fs_unique_affix'] = $this->get_unique_affix();
22929
22930 return $this->get_activation_url( $params );
22931 }
22932
22933 /**
22934 * Get the URL of the page that should be loaded after the user connect
22935 * or skip in the opt-in screen.
22936 *
22937 * @author Vova Feldman (@svovaf)
22938 * @since 1.1.3
22939 *
22940 * @param string $filter Filter name.
22941 * @param array $params Since 1.2.2.7
22942 * @param bool|null $network
22943 *
22944 * @return string
22945 */
22946 function get_after_activation_url( $filter, $params = array(), $network = null ) {
22947 if ( $this->show_opt_in_on_themes_page() &&
22948 ( fs_request_has( 'pending_activation' ) ||
22949 // For cases when the first time path is set, even though it's a WP.org theme.
22950 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
22951 ) {
22952 $first_time_path = '';
22953 } else {
22954 $first_time_path = $this->_menu->get_first_time_path(
22955 fs_is_network_admin() && $this->_is_network_active
22956 );
22957 }
22958
22959 if ( $this->_is_network_active &&
22960 fs_is_network_admin() &&
22961 ! $this->_menu->has_network_menu() &&
22962 $this->is_network_registered()
22963 ) {
22964 $target_url = $this->get_account_url();
22965 } else {
22966 // Default plugin's page.
22967 $target_url = $this->_get_admin_page_url( '', array(), $network );
22968 }
22969
22970 return add_query_arg( $params, $this->apply_filters(
22971 $filter,
22972 empty( $first_time_path ) ?
22973 $target_url :
22974 $first_time_path
22975 ) );
22976 }
22977
22978 /**
22979 * Handle account page updates / edits / actions.
22980 *
22981 * @author Vova Feldman (@svovaf)
22982 * @since 1.0.2
22983 *
22984 */
22985 private function _handle_account_edits() {
22986 if ( ! $this->is_user_admin() ) {
22987 return;
22988 }
22989
22990 $action = fs_get_action();
22991
22992 if ( empty( $action ) ) {
22993 return;
22994 }
22995
22996 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
22997 $install_id = fs_request_get( 'install_id', '' );
22998
22999 // Alias.
23000 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
23001
23002 $is_network_action = $this->is_network_level_action();
23003 $blog_id = $this->is_network_level_site_specific_action();
23004 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
23005
23006 if ( is_numeric( $blog_id ) ) {
23007 $this->switch_to_blog( $blog_id );
23008 } else {
23009 $blog_id = '';
23010 }
23011
23012 switch ( $action ) {
23013 case 'opt_in':
23014 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23015
23016 if ( $is_parent_plugin_action ) {
23017 if ( $is_network_action && ! empty( $blog_id ) ) {
23018 if ( ! $this->is_registered() ) {
23019 $this->install_with_user(
23020 $this->get_network_user(),
23021 false,
23022 false,
23023 false,
23024 false
23025 );
23026
23027 $this->_admin_notices->add(
23028 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
23029 $this->get_text_inline( 'Awesome', 'awesome' )
23030 );
23031 }
23032 }
23033 }
23034 break;
23035
23036 case 'toggle_tracking':
23037 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23038
23039 if ( $is_parent_plugin_action ) {
23040 if ( $is_network_action && ! empty( $blog_id ) ) {
23041 if ( $this->is_registered( true ) ) {
23042 if ( $this->is_tracking_prohibited( $blog_id ) ) {
23043 if ( $this->toggle_site_tracking( true, $blog_id ) ) {
23044 $this->_admin_notices->add(
23045 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>" ),
23046 $this->get_text_inline( 'Thank you!', 'thank-you' )
23047 );
23048 }
23049 } else {
23050 if ( $this->toggle_site_tracking( false, $blog_id ) ) {
23051 $install = $this->get_install_by_blog_id( $blog_id );
23052
23053 $this->_admin_notices->add(
23054 sprintf(
23055 $this->get_text_inline( 'Diagnostic data will no longer be sent from %s to %s.', 'opted-out-successfully' ),
23056 self::get_unfiltered_site_url( $blog_id, true ),
23057 "<b>{$this->get_plugin_title()}</b>"
23058 )
23059 );
23060 }
23061 }
23062 }
23063 }
23064 }
23065
23066 break;
23067
23068 case 'delete_account':
23069 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23070
23071 $is_network_deletion = $is_network_action && empty( $blog_id );
23072
23073 if ( $is_parent_plugin_action ) {
23074 // Delete add-on installs if have any.
23075 $installed_addons = $this->get_installed_addons();
23076 foreach ( $installed_addons as $fs_addon ) {
23077 if ( $is_network_deletion ) {
23078 $fs_addon->delete_network_account_event();
23079 } else {
23080 $fs_addon->delete_account_event();
23081 }
23082 }
23083
23084 if ( $is_network_deletion ) {
23085 $this->delete_network_account_event();
23086 } else {
23087 $this->delete_account_event();
23088 }
23089
23090 // Clear user and site.
23091 $this->_site = null;
23092 $this->_user = null;
23093
23094 $this->maybe_set_slug_and_network_menu_exists_flag();
23095
23096 fs_redirect( $this->get_activation_url() );
23097 } else {
23098 if ( $this->is_addon_activated( $plugin_id ) ) {
23099 $fs_addon = self::get_instance_by_id( $plugin_id );
23100
23101 if ( $is_network_deletion ) {
23102 $fs_addon->delete_network_account_event();
23103 } else {
23104 $fs_addon->delete_account_event();
23105 }
23106
23107 fs_redirect( $this->_get_admin_page_url( 'account' ) );
23108 }
23109 }
23110
23111 return;
23112
23113 case 'downgrade_account':
23114 if ( is_numeric( $blog_id ) ) {
23115 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23116 } else {
23117 check_admin_referer( $action );
23118 }
23119
23120 $switch_to_network_install_blog_after_cancellation = (
23121 is_numeric( $blog_id ) &&
23122 $plugin_id == $this->get_id() &&
23123 ! $this->is_trial()
23124 );
23125
23126 $result = $this->cancel_subscription_or_trial( $plugin_id );
23127 if ( $this->is_api_error( $result ) ) {
23128 $this->_admin_notices->add(
23129 $result->error->message,
23130 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23131 'error'
23132 );
23133 }
23134
23135 if ( $switch_to_network_install_blog_after_cancellation ) {
23136 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23137 }
23138
23139 return;
23140
23141 case 'activate_license':
23142 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23143
23144 $fs = $this;
23145 if ( $plugin_id != $this->get_id() ) {
23146 $fs = $this->is_addon_activated( $plugin_id ) ?
23147 self::get_instance_by_id( $plugin_id ) :
23148 null;
23149 }
23150
23151 if ( is_object( $fs ) ) {
23152 $fs->_activate_license();
23153
23154 /**
23155 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
23156 *
23157 * @author Leo Fajardo (@leorw)
23158 * @since 2.4.0
23159 */
23160 unset( $_REQUEST['plugin_id'] );
23161
23162 if ( $this->is_bundle_license_auto_activation_enabled() ) {
23163 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
23164 }
23165 }
23166
23167 return;
23168
23169 case 'deactivate_license':
23170 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23171
23172 if ( $plugin_id == $this->get_id() ) {
23173 $this->_deactivate_license();
23174
23175 if ( $this->is_only_premium() ) {
23176 // Clear user and site.
23177 $this->_site = null;
23178 $this->_user = null;
23179
23180 if ( ! $is_network_action ) {
23181 fs_redirect( $this->get_activation_url() );
23182 } else if ( is_numeric( $blog_id ) ) {
23183 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23184 }
23185 }
23186 } else {
23187 if ( $this->is_addon_activated( $plugin_id ) ) {
23188 $fs_addon = self::get_instance_by_id( $plugin_id );
23189 $fs_addon->_deactivate_license();
23190 }
23191 }
23192
23193 return;
23194
23195 case 'check_updates':
23196 check_admin_referer( $action );
23197 $this->check_updates();
23198
23199 return;
23200
23201 case 'change_owner':
23202 $state = fs_request_get( 'state', 'init' );
23203 switch ( $state ) {
23204 case 'init':
23205 // The nonce is injected by the error handler in `_email_address_update_ajax_handler` function.
23206 check_admin_referer( 'change_owner' );
23207
23208 $candidate_email = fs_request_get( 'candidate_email' );
23209 $transfer_type = fs_request_get( 'transfer_type' );
23210
23211 if ( $this->init_change_owner( $candidate_email, $transfer_type ) ) {
23212 if ( 'transfer' === $transfer_type ) {
23213 $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>' ) );
23214 } else {
23215 $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>' ) );
23216 }
23217 }
23218 break;
23219 case 'owner_confirmed':
23220 // We cannot (or need not to) check the nonce and referer here, because the link comes from the email sent by our API.
23221 $candidate_email = fs_request_get( 'candidate_email', '' );
23222
23223 if ( ! is_email($candidate_email ) ) {
23224 return;
23225 }
23226
23227 $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>' ) );
23228 break;
23229 case 'candidate_confirmed':
23230 // We do not need to validate the authenticity of this request here, because the `complete_change_owner` does that for us through API calls.
23231 if ( $this->complete_change_owner() ) {
23232 $this->_admin_notices->add_sticky(
23233 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
23234 'ownership_changed',
23235 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
23236 );
23237 } else {
23238 // @todo Handle failed ownership change message.
23239 }
23240 break;
23241 }
23242
23243 return;
23244
23245 case 'update_user_name':
23246 check_admin_referer( 'update_user_name' );
23247
23248 $result = $this->update_user_name();
23249
23250 if ( isset( $result->error ) ) {
23251 $this->_admin_notices->add(
23252 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
23253 $oops_text,
23254 'error'
23255 );
23256 } else {
23257 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
23258 }
23259
23260 return;
23261
23262 #region Actions that might be called from external links (e.g. email)
23263
23264 /**
23265 * !!IMPORTANT!!: We cannot check for a valid nonce in this region, because the links could be coming from emails.
23266 */
23267
23268 case 'cancel_trial':
23269 $result = $this->cancel_subscription_or_trial( $plugin_id );
23270 if ( $this->is_api_error( $result ) ) {
23271 $this->_admin_notices->add(
23272 $result->error->message,
23273 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23274 'error'
23275 );
23276 }
23277
23278 return;
23279
23280 case 'verify_email':
23281 $this->verify_email();
23282
23283 return;
23284
23285 case 'sync_user':
23286 $this->_handle_account_user_sync();
23287
23288 return;
23289
23290 case $this->get_unique_affix() . '_sync_license':
23291 $this->_sync_license();
23292
23293 return;
23294
23295 case 'download_latest':
23296 $this->download_latest_directly( $plugin_id );
23297
23298 return;
23299
23300 #endregion
23301 }
23302
23303 if ( WP_FS__IS_POST_REQUEST ) {
23304 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
23305 foreach ( $properties as $p ) {
23306 if ( 'update_' . $p === $action ) {
23307 check_admin_referer( $action );
23308
23309 $this->_logger->log( $action );
23310
23311 $site_property = substr( $p, strlen( 'site_' ) );
23312 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
23313 $this->get_site()->{$site_property} = $site_property_value;
23314
23315 // Store account after modification.
23316 $this->_store_site();
23317
23318 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
23319
23320 $this->_admin_notices->add( sprintf(
23321 /* translators: %s: User's account property (e.g. email address, name) */
23322 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
23323 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
23324 ) );
23325
23326 return;
23327 }
23328 }
23329 }
23330 }
23331
23332 /**
23333 * Adds CSS classes for the body tag in the admin.
23334 *
23335 * @param string $classes Space-separated string of class names.
23336 *
23337 * @return string $classes FS Admin body tag class names.
23338 */
23339 public function fs_addons_body_class( $classes ) {
23340 $classes .= ' plugins-php';
23341 return $classes;
23342 }
23343
23344 /**
23345 * Account page resources load.
23346 *
23347 * @author Vova Feldman (@svovaf)
23348 * @since 1.0.6
23349 */
23350 function _account_page_load() {
23351 $this->_logger->entrance();
23352
23353 $this->_logger->info( var_export( $_REQUEST, true ) );
23354
23355 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
23356
23357 if ( $this->has_addons() ) {
23358 wp_enqueue_script( 'plugin-install' );
23359 add_thickbox();
23360 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23361 }
23362
23363 if ( $this->has_paid_plan() &&
23364 ! $this->has_any_license() &&
23365 ! $this->is_sync_executed() &&
23366 $this->is_tracking_allowed()
23367 ) {
23368 /**
23369 * If no licenses found and no sync job was executed during the last 24 hours,
23370 * just execute the sync job right away (blocking execution).
23371 *
23372 * @since 1.1.7.3
23373 */
23374 $this->run_manual_sync();
23375 }
23376
23377 $this->_handle_account_edits();
23378
23379 if (
23380 is_object( $this->_license ) &&
23381 $this->_license->user_id == $this->_user->id &&
23382 ! $this->is_whitelabeled( true )
23383 ) {
23384 $this->_admin_notices->add(
23385 sprintf(
23386 $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' ),
23387 sprintf(
23388 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
23389 $this->get_text_inline( 'Click here', 'click-here' )
23390 )
23391 ),
23392 '',
23393 'success',
23394 false,
23395 'license_not_whitelabeled'
23396 );
23397 }
23398
23399 $this->do_action( 'account_page_load_before_departure' );
23400 }
23401
23402 /**
23403 * Renders the "Affiliation" page.
23404 *
23405 * @author Leo Fajardo (@leorw)
23406 * @since 1.2.3
23407 */
23408 function _affiliation_page_render() {
23409 $this->_logger->entrance();
23410
23411 $this->fetch_affiliate_and_terms();
23412
23413 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
23414
23415 $is_bundle_context = $this->has_bundle_context();
23416
23417 $plugin_title = $this->get_plugin_title();
23418
23419 if ( $is_bundle_context ) {
23420 $plugin_title = $this->plugin_affiliate_terms->plugin_title;
23421
23422 // Add the suffix "Bundle" only if the word is not present in the title itself.
23423 if ( false === mb_stripos( $plugin_title, fs_text_inline( 'Bundle', 'bundle' ) ) ) {
23424 $plugin_title = $this->apply_filters(
23425 'formatted_bundle_title',
23426 $plugin_title . ' ' . fs_text_inline( 'Bundle', 'bundle' )
23427 );
23428 }
23429 }
23430
23431 $vars = array(
23432 'id' => $this->_module_id,
23433 'plugin_title' => $plugin_title,
23434 );
23435 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
23436 }
23437
23438
23439 /**
23440 * Render account page.
23441 *
23442 * @author Vova Feldman (@svovaf)
23443 * @since 1.0.0
23444 */
23445 function _account_page_render() {
23446 $this->_logger->entrance();
23447
23448 $template = 'account.php';
23449 $vars = array( 'id' => $this->_module_id );
23450
23451 /**
23452 * Added filter to the template to allow developers wrapping the template
23453 * in custom HTML (e.g. within a wizard/tabs).
23454 *
23455 * @author Vova Feldman (@svovaf)
23456 * @since 1.2.1.6
23457 */
23458 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
23459 }
23460
23461 /**
23462 * Render account connect page.
23463 *
23464 * @author Vova Feldman (@svovaf)
23465 * @since 1.0.7
23466 */
23467 function _connect_page_render() {
23468 $this->_logger->entrance();
23469
23470 $vars = array( 'id' => $this->_module_id );
23471
23472 /**
23473 * Added filter to the template to allow developers wrapping the template
23474 * in custom HTML (e.g. within a wizard/tabs).
23475 *
23476 * @author Vova Feldman (@svovaf)
23477 * @since 1.2.1.6
23478 */
23479 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
23480 }
23481
23482 /**
23483 * Load required resources before add-ons page render.
23484 *
23485 * @author Vova Feldman (@svovaf)
23486 * @since 1.0.6
23487 */
23488 function _addons_page_load() {
23489 $this->_logger->entrance();
23490
23491 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
23492
23493 wp_enqueue_script( 'plugin-install' );
23494 add_thickbox();
23495 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23496
23497 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
23498 $this->_admin_notices->add(
23499 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>' ),
23500 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
23501 'update-nag'
23502 );
23503 }
23504 }
23505
23506 /**
23507 * Render add-ons page.
23508 *
23509 * @author Vova Feldman (@svovaf)
23510 * @since 1.0.6
23511 */
23512 function _addons_page_render() {
23513 $this->_logger->entrance();
23514
23515 $vars = array( 'id' => $this->_module_id );
23516
23517 /**
23518 * Added filter to the template to allow developers wrapping the template
23519 * in custom HTML (e.g. within a wizard/tabs).
23520 *
23521 * @author Vova Feldman (@svovaf)
23522 * @since 1.2.1.6
23523 */
23524 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
23525 }
23526
23527 /* Pricing & Upgrade
23528 ------------------------------------------------------------------------------------------------------------------*/
23529 /**
23530 * Render pricing page.
23531 *
23532 * @author Vova Feldman (@svovaf)
23533 * @since 1.0.0
23534 */
23535 function _pricing_page_render() {
23536 $this->_logger->entrance();
23537
23538 $vars = array( 'id' => $this->_module_id );
23539
23540 if ( 'true' === fs_request_get( 'checkout', false ) ) {
23541 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
23542 } else {
23543 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
23544 }
23545 }
23546
23547 /**
23548 * @author Leo Fajardo (@leorw)
23549 * @since 2.3.1
23550 */
23551 function _maybe_add_pricing_ajax_handler() {
23552 if ( ! $this->should_use_external_pricing() ) {
23553 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
23554 }
23555 }
23556
23557 /**
23558 * @author Leo Fajardo (@leorw)
23559 * @since 2.3.1
23560 */
23561 function _fs_pricing_ajax_action_handler() {
23562 $this->check_ajax_referer( 'pricing_ajax_action' );
23563
23564 $result = null;
23565 $pricing_action = fs_request_get( 'pricing_action' );
23566
23567 switch ( $pricing_action ) {
23568 case 'fetch_pricing_data':
23569 $params = array(
23570 'is_enriched' => true,
23571 'trial' => fs_request_get_bool( 'trial' ),
23572 'sandbox' => fs_request_get_raw( 'sandbox' ),
23573 's_ctx_type' => fs_request_get_raw( 's_ctx_type' ),
23574 's_ctx_id' => fs_request_get_raw( 's_ctx_id' ),
23575 's_ctx_ts' => fs_request_get_raw( 's_ctx_ts' ),
23576 's_ctx_secure' => fs_request_get_raw( 's_ctx_secure' ),
23577 );
23578
23579 $bundle_id = $this->get_bundle_id();
23580 $bundle_public_key = $this->get_bundle_public_key();
23581
23582 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
23583
23584 if ( ! $has_bundle_context ) {
23585 $api = $this->get_api_plugin_scope();
23586 } else {
23587 $api = FS_Api::instance(
23588 $bundle_id,
23589 'plugin',
23590 $bundle_id,
23591 $bundle_public_key,
23592 ! $this->is_live(),
23593 false,
23594 $this->get_sdk_version()
23595 );
23596
23597 $params['plugin_id'] = $this->get_id();
23598 $params['plugin_public_key'] = $this->get_public_key();
23599 }
23600
23601 $result = $api->get( 'pricing.json?' . http_build_query( $params ) );
23602 break;
23603 case 'start_trial':
23604 $result = $this->opt_in(
23605 false,
23606 false,
23607 false,
23608 false,
23609 false,
23610 fs_request_get( 'plan_id' )
23611 );
23612 }
23613
23614 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
23615 $this->_logger->api_error( $result );
23616
23617 self::shoot_ajax_failure(
23618 isset( $result->error ) ?
23619 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
23620 var_export( $result, true )
23621 );
23622 }
23623
23624 $this->shoot_ajax_success( $result );
23625 }
23626
23627 #----------------------------------------------------------------------------------
23628 #region Contact Us
23629 #----------------------------------------------------------------------------------
23630
23631 /**
23632 * Render contact-us page.
23633 *
23634 * @author Vova Feldman (@svovaf)
23635 * @since 1.0.3
23636 */
23637 function _contact_page_render() {
23638 $this->_logger->entrance();
23639
23640 $vars = array( 'id' => $this->_module_id );
23641
23642 /**
23643 * Added filter to the template to allow developers wrapping the template
23644 * in custom HTML (e.g. within a wizard/tabs).
23645 *
23646 * @author Vova Feldman (@svovaf)
23647 * @since 2.1.3
23648 */
23649 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
23650 }
23651
23652 #endregion ------------------------------------------------------------------------
23653
23654 /**
23655 * Hide all admin notices to prevent distractions.
23656 *
23657 * @author Vova Feldman (@svovaf)
23658 * @since 1.0.3
23659 *
23660 * @uses remove_all_actions()
23661 */
23662 private static function _hide_admin_notices() {
23663 remove_all_actions( 'admin_notices' );
23664 remove_all_actions( 'network_admin_notices' );
23665 remove_all_actions( 'all_admin_notices' );
23666 remove_all_actions( 'user_admin_notices' );
23667 }
23668
23669 static function _clean_admin_content_section_hook() {
23670 $hide_admin_notices = true;
23671
23672 if ( fs_request_is_action( 'allow_clone_resolution_notice' ) ) {
23673 check_admin_referer( 'fs_allow_clone_resolution_notice' );
23674
23675 $hide_admin_notices = false;
23676 }
23677
23678 if ( $hide_admin_notices ) {
23679 self::_hide_admin_notices();
23680 }
23681
23682 // Hide footer.
23683 echo '<style>#wpfooter { display: none !important; }</style>';
23684 }
23685
23686 /**
23687 * Attach to admin_head hook to hide all admin notices.
23688 *
23689 * @author Vova Feldman (@svovaf)
23690 * @since 1.0.3
23691 */
23692 static function _clean_admin_content_section() {
23693 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
23694 }
23695
23696 /* CSS & JavaScript
23697 ------------------------------------------------------------------------------------------------------------------*/
23698 /* function _enqueue_script($handle, $src) {
23699 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
23700
23701 $this->_logger->entrance( 'script = ' . $url );
23702
23703 wp_enqueue_script( $handle, $url );
23704 }*/
23705
23706 /* SDK
23707 ------------------------------------------------------------------------------------------------------------------*/
23708 private $_user_api;
23709
23710 /**
23711 *
23712 * @author Vova Feldman (@svovaf)
23713 * @since 1.0.2
23714 *
23715 * @param bool $flush
23716 *
23717 * @return FS_Api
23718 */
23719 function get_api_user_scope( $flush = false ) {
23720 if ( ! isset( $this->_user_api ) || $flush ) {
23721 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
23722 }
23723
23724 return $this->_user_api;
23725 }
23726
23727 /**
23728 * @author Vova Feldman (@svovaf)
23729 * @since 2.0.0
23730 *
23731 * @param \FS_User $user
23732 *
23733 * @return \FS_Api
23734 */
23735 private function get_api_user_scope_by_user( FS_User $user ) {
23736 return FS_Api::instance(
23737 $this->_module_id,
23738 'user',
23739 $user->id,
23740 $user->public_key,
23741 ! $this->is_live(),
23742 $user->secret_key,
23743 $this->get_sdk_version()
23744 );
23745 }
23746
23747 /**
23748 *
23749 * @author Leo Fajardo (@leorw)
23750 * @since 2.0.0
23751 *
23752 * @param bool $flush
23753 *
23754 * @return FS_Api
23755 */
23756 private function get_current_or_network_user_api_scope( $flush = false ) {
23757 if ( ! $this->_is_network_active ||
23758 ( isset( $this->_user ) && $this->_user instanceof FS_User )
23759 ) {
23760 return $this->get_api_user_scope( $flush );
23761 }
23762
23763 $user = $this->get_current_or_network_user();
23764
23765 $this->_user_api = FS_Api::instance(
23766 $this->_module_id,
23767 'user',
23768 $user->id,
23769 $user->public_key,
23770 ! $this->is_live(),
23771 $user->secret_key,
23772 $this->get_sdk_version()
23773 );
23774
23775 return $this->_user_api;
23776 }
23777
23778 private $_site_api;
23779
23780 /**
23781 *
23782 * @author Vova Feldman (@svovaf)
23783 * @since 1.0.2
23784 *
23785 * @param bool $flush
23786 *
23787 * @return FS_Api
23788 */
23789 private function get_api_site_scope( $flush = false ) {
23790 if ( ! isset( $this->_site_api ) || $flush ) {
23791 $this->_site_api = FS_Api::instance(
23792 $this->_module_id,
23793 'install',
23794 $this->_site->id,
23795 $this->_site->public_key,
23796 ! $this->is_live(),
23797 $this->_site->secret_key,
23798 $this->get_sdk_version(),
23799 self::get_unfiltered_site_url()
23800 );
23801 }
23802
23803 return $this->_site_api;
23804 }
23805
23806 /**
23807 * @author Leo Fajardo (@leorw)
23808 * @since 2.5.0
23809 *
23810 * @param string $path
23811 * @param string $method
23812 * @param array $params
23813 * @param bool $flush_instance
23814 *
23815 * @return array|mixed|string|void
23816 * @throws Freemius_Exception
23817 */
23818 private function api_site_call( $path, $method = 'GET', $params = array(), $flush_instance = false ) {
23819 $result = $this->get_api_site_scope( $flush_instance )->call( $path, $method, $params );
23820
23821 /**
23822 * 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.
23823 *
23824 * @author Leo Fajardo (@leorw)
23825 * @since 2.5.0
23826 */
23827 if (
23828 $this->is_registered() &&
23829 FS_Api::is_api_result_entity( $result ) &&
23830 isset( $result->url )
23831 ) {
23832 $stored_local_url = trailingslashit( $this->_site->url );
23833 $stored_remote_url = trailingslashit( $result->url );
23834
23835 if ( $stored_local_url !== $stored_remote_url ) {
23836 $this->_site->url = $result->url;
23837 $this->_store_site();
23838 }
23839
23840 if ( fs_strip_url_protocol( $stored_remote_url ) !== self::get_unfiltered_site_url( null, true, true ) ) {
23841 FS_Clone_Manager::instance()->maybe_run_clone_resolution();
23842 }
23843 }
23844
23845 return $result;
23846 }
23847
23848 private $_plugin_api;
23849
23850 /**
23851 * Get plugin public API scope.
23852 *
23853 * @author Vova Feldman (@svovaf)
23854 * @since 1.0.7
23855 *
23856 * @return FS_Api
23857 */
23858 function get_api_plugin_scope() {
23859 if ( ! isset( $this->_plugin_api ) ) {
23860 $this->_plugin_api = FS_Api::instance(
23861 $this->_module_id,
23862 'plugin',
23863 $this->_plugin->id,
23864 $this->_plugin->public_key,
23865 ! $this->is_live(),
23866 false,
23867 $this->get_sdk_version()
23868 );
23869 }
23870
23871 return $this->_plugin_api;
23872 }
23873
23874 /**
23875 * Get bundle public API scope.
23876 *
23877 * @author Vova Feldman (@svovaf)
23878 * @since 2.3.1
23879 *
23880 * @return FS_Api
23881 */
23882 function get_api_bundle_scope() {
23883 return FS_Api::instance(
23884 $this->get_bundle_id(),
23885 'plugin',
23886 $this->get_bundle_id(),
23887 $this->get_bundle_public_key(),
23888 ! $this->is_live(),
23889 false,
23890 $this->get_sdk_version()
23891 );
23892 }
23893
23894 /**
23895 * Get site API scope object (fallback to public plugin scope when not registered).
23896 *
23897 * @author Vova Feldman (@svovaf)
23898 * @since 1.0.7
23899 *
23900 * @return FS_Api
23901 */
23902 function get_api_site_or_plugin_scope() {
23903 return $this->is_registered() ?
23904 $this->get_api_site_scope() :
23905 $this->get_api_plugin_scope();
23906 }
23907
23908 /**
23909 * @author Leo Fajardo (@leorw)
23910 * @since 2.2.3.1
23911 *
23912 * @param object $result
23913 */
23914 private function maybe_modify_api_curl_error_message( $result ) {
23915 if (
23916 'cUrlMissing' !== $result->error->type &&
23917 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
23918 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
23919 ) {
23920 return;
23921 }
23922
23923 $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' ) .
23924 ' ' .
23925 $this->esc_html_inline(
23926 sprintf(
23927 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
23928 implode(
23929 ', ',
23930 $this->apply_filters( 'api_domains', array(
23931 'api.freemius.com',
23932 'wp.freemius.com'
23933 ) )
23934 )
23935 ),
23936 'connectivity-whitelist'
23937 ) .
23938 ' ' .
23939 sprintf(
23940 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
23941 $this->get_module_type()
23942 );
23943 }
23944
23945 /**
23946 * Show trial promotional notice (if any trial exist).
23947 *
23948 * @author Vova Feldman (@svovaf)
23949 * @since 1.0.9
23950 *
23951 * @param FS_Plugin_Plan[] $plans
23952 */
23953 function _check_for_trial_plans( $plans ) {
23954 /**
23955 * 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.
23956 *
23957 * @author Vova Feldman (@svovaf)
23958 * @since 2.1.2
23959 */
23960 if ( ! is_array( $plans ) && is_object( $plans ) ) {
23961 $plans = array( $plans );
23962 }
23963
23964 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
23965 $plans = array();
23966 }
23967
23968 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
23969 }
23970
23971 /**
23972 * During trial promotion the "upgrade" submenu item turns to
23973 * "start trial" to encourage the trial. Since we want to keep
23974 * the same menu item handler and there's no robust way to
23975 * add new arguments to the menu item link's querystring,
23976 * use JavaScript to find the menu item and update the href of
23977 * the link.
23978 *
23979 * @author Vova Feldman (@svovaf)
23980 * @since 1.2.1.5
23981 */
23982 function _fix_start_trial_menu_item_url() {
23983 $template_args = array( 'id' => $this->_module_id );
23984 fs_require_template( 'add-trial-to-pricing.php', $template_args );
23985 }
23986
23987 /**
23988 * Check if module is currently in a trial promotion mode.
23989 *
23990 * @author Vova Feldman (@svovaf)
23991 * @since 1.2.2.7
23992 *
23993 * @return bool
23994 */
23995 function is_in_trial_promotion() {
23996 return $this->_admin_notices->has_sticky( 'trial_promotion' );
23997 }
23998
23999 /**
24000 * Show trial promotional notice (if any trial exist).
24001 *
24002 * @author Vova Feldman (@svovaf)
24003 * @since 1.0.9
24004 *
24005 * @return bool If trial notice added.
24006 */
24007 function _add_trial_notice() {
24008 if ( ! $this->is_user_admin() ) {
24009 return false;
24010 }
24011
24012 if ( ! $this->is_user_in_admin() ) {
24013 return false;
24014 }
24015
24016 if ( $this->_is_network_active ) {
24017 if ( fs_is_network_admin() ) {
24018 // Network level trial is disabled at the moment.
24019 return false;
24020 }
24021
24022 if ( ! $this->is_delegated_connection() ) {
24023 // Only delegated sites should support trials.
24024 return false;
24025 }
24026 }
24027
24028 // Check if trial message is already shown.
24029 if ( $this->is_in_trial_promotion() ) {
24030 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
24031
24032 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
24033
24034 return false;
24035 }
24036
24037 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
24038 // Don't show trial if running the premium code, unless running in DEV mode.
24039 return false;
24040 }
24041
24042 if ( ! $this->has_trial_plan() ) {
24043 // No plans with trial.
24044 return false;
24045 }
24046
24047 if ( ! $this->apply_filters( 'show_trial', true ) ) {
24048 // Developer explicitly asked not to show the trial promo.
24049 return false;
24050 }
24051
24052 if ( $this->is_registered() ) {
24053 // Check if trial already utilized.
24054 if ( $this->_site->is_trial_utilized() ) {
24055 return false;
24056 }
24057
24058 if ( $this->is_paying_or_trial() ) {
24059 // Don't show trial if paying or already in trial.
24060 return false;
24061 }
24062 }
24063
24064 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24065 // If not yet opted-in/skipped, or pending activation, don't show trial.
24066 return false;
24067 }
24068
24069 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
24070 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
24071
24072 // Show promotion if never shown before and 24 hours after initial activation with FS.
24073 if ( ! $was_promotion_shown_before &&
24074 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
24075 ) {
24076 return false;
24077 }
24078
24079 // OR if promotion was shown before, try showing it every 30 days.
24080 if ( $was_promotion_shown_before &&
24081 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
24082 ) {
24083 return false;
24084 }
24085
24086 $trial_period = $this->_trial_days;
24087 $require_payment = $this->_is_trial_require_payment;
24088 $trial_url = $this->get_trial_url();
24089 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
24090
24091 if ( $this->is_registered() ) {
24092 // If opted-in, override trial with up to date data from API.
24093 $trial_plans = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
24094 $trial_plans_count = count( $trial_plans );
24095
24096 if ( 0 === $trial_plans_count ) {
24097 // If there's no plans with a trial just exit.
24098 return false;
24099 }
24100
24101 /**
24102 * @var FS_Plugin_Plan $paid_plan
24103 */
24104 $paid_plan = $trial_plans[0];
24105 $require_payment = $paid_plan->is_require_subscription;
24106 $trial_period = $paid_plan->trial_period;
24107
24108 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
24109
24110 if ( $total_paid_plans !== $trial_plans_count ) {
24111 // Not all paid plans have a trial - generate a string of those that have it.
24112 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
24113 $plans_string .= sprintf(
24114 ' <a href="%s">%s</a>',
24115 $trial_url,
24116 $trial_plans[ $i ]->title
24117 );
24118
24119 if ( $i < $trial_plans_count - 2 ) {
24120 $plans_string .= ', ';
24121 } else if ( $i == $trial_plans_count - 2 ) {
24122 $plans_string .= ' and ';
24123 }
24124 }
24125 }
24126 }
24127
24128 $message = sprintf(
24129 $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' ),
24130 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
24131 $plans_string,
24132 $trial_period
24133 );
24134
24135 // "No Credit-Card Required" or "No Commitment for N Days".
24136 $cc_string = $require_payment ?
24137 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
24138 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
24139
24140
24141 // Start trial button.
24142 $button = ' ' . sprintf(
24143 '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24144 $trial_url,
24145 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
24146 );
24147
24148 $this->_admin_notices->add_sticky(
24149 $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
24150 'trial_promotion',
24151 '',
24152 'promotion'
24153 );
24154
24155 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
24156
24157 return true;
24158 }
24159
24160 /**
24161 * Lets users/customers know that the product has an affiliate program.
24162 *
24163 * @author Leo Fajardo (@leorw)
24164 * @since 1.2.2.11
24165 *
24166 * @return bool Returns true if the notice has been added.
24167 */
24168 function _add_affiliate_program_notice() {
24169 if ( ! $this->is_user_admin() ) {
24170 return false;
24171 }
24172
24173 if ( ! $this->is_user_in_admin() ) {
24174 return false;
24175 }
24176
24177 // Check if the notice is already shown.
24178 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
24179 return false;
24180 }
24181
24182 if (
24183 // Product has no affiliate program.
24184 ! $this->has_affiliate_program() ||
24185 // User has applied for an affiliate account.
24186 ! empty( $this->_storage->affiliate_application_data )
24187 ) {
24188 return false;
24189 }
24190
24191 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
24192 // Developer explicitly asked not to show the notice about the affiliate program.
24193 return false;
24194 }
24195
24196 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24197 // If not yet opted in/skipped, or pending activation, don't show the notice.
24198 return false;
24199 }
24200
24201 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
24202 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
24203
24204 /**
24205 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
24206 * activation with FS.
24207 */
24208 if ( $was_notice_shown_before ||
24209 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
24210 ) {
24211 return false;
24212 }
24213
24214 if ( ! $this->is_paying() &&
24215 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
24216 ) {
24217 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
24218 return false;
24219 }
24220
24221 $message = sprintf(
24222 $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' ),
24223 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
24224 $this->get_module_label( true )
24225 );
24226
24227 // HTML code for the "Learn more..." button.
24228 $button = ' ' . sprintf(
24229 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24230 $this->_get_admin_page_url( 'affiliation' ),
24231 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
24232 );
24233
24234 $this->_admin_notices->add_sticky(
24235 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
24236 'affiliate_program',
24237 '',
24238 'promotion'
24239 );
24240
24241 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
24242
24243 return true;
24244 }
24245
24246 /**
24247 * @author Vova Feldman (@svovaf)
24248 * @since 1.2.1.5
24249 */
24250 function _enqueue_common_css() {
24251 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
24252 // Add basic CSS for admin-notices and menu-item colors.
24253 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
24254 }
24255 }
24256
24257 /**
24258 * @author Leo Fajardo (@leorw)
24259 * @since 1.2.2
24260 */
24261 function _show_theme_activation_optin_dialog() {
24262 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
24263
24264 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
24265 }
24266
24267 /**
24268 * @author Leo Fajardo (@leorw)
24269 * @since 1.2.2
24270 */
24271 function _add_fs_theme_activation_dialog() {
24272 global $pagenow;
24273
24274 if ( 'themes.php' !== $pagenow ) {
24275 return;
24276 }
24277
24278 $vars = array( 'id' => $this->_module_id );
24279 fs_require_once_template( 'connect.php', $vars );
24280 }
24281
24282 /* Action Links
24283 ------------------------------------------------------------------------------------------------------------------*/
24284 private $_action_links_hooked = false;
24285 private $_action_links = array();
24286
24287 /**
24288 * Hook to plugin action links filter.
24289 *
24290 * @author Vova Feldman (@svovaf)
24291 * @since 1.0.0
24292 */
24293 private function hook_plugin_action_links() {
24294 $this->_logger->entrance();
24295
24296 $this->_action_links_hooked = true;
24297
24298 $this->_logger->log( 'Adding action links hooks.' );
24299
24300 // Add action link to settings page.
24301 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
24302 &$this,
24303 '_modify_plugin_action_links_hook'
24304 ), WP_FS__DEFAULT_PRIORITY, 2 );
24305 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
24306 &$this,
24307 '_modify_plugin_action_links_hook'
24308 ), WP_FS__DEFAULT_PRIORITY, 2 );
24309 }
24310
24311 /**
24312 * Add plugin action link.
24313 *
24314 * @author Vova Feldman (@svovaf)
24315 * @since 1.0.0
24316 *
24317 * @param $label
24318 * @param $url
24319 * @param bool $external
24320 * @param int $priority
24321 * @param bool $key
24322 */
24323 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
24324 $this->_logger->entrance();
24325
24326 if ( ! isset( $this->_action_links[ $priority ] ) ) {
24327 $this->_action_links[ $priority ] = array();
24328 }
24329
24330 if ( false === $key ) {
24331 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
24332 }
24333
24334 $this->_action_links[ $priority ][] = array(
24335 'label' => $label,
24336 'href' => $url,
24337 'key' => $key,
24338 'external' => $external
24339 );
24340 }
24341
24342 /**
24343 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
24344 *
24345 * @author Vova Feldman (@svovaf)
24346 * @since 1.0.0
24347 */
24348 function _add_upgrade_action_link() {
24349 $this->_logger->entrance();
24350
24351 $is_activation_mode = $this->is_activation_mode();
24352
24353 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
24354
24355 /**
24356 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
24357 * and "Add-Ons" menus should be added.
24358 *
24359 * @author Leo Fajardo (@leorw)
24360 * @since 2.3.0
24361 */
24362 $add_upgrade_link = (
24363 $add_action_links ||
24364 ( $is_activation_mode && $this->is_only_premium() )
24365 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
24366
24367 $add_addons_link = ( $add_action_links && $this->has_addons() );
24368
24369 if ( ! $add_upgrade_link && ! $add_addons_link ) {
24370 return;
24371 }
24372
24373 if (
24374 $add_upgrade_link &&
24375 $this->is_pricing_page_visible() &&
24376 $this->is_submenu_item_visible( 'pricing' )
24377 ) {
24378 $this->add_plugin_action_link(
24379 $this->get_text_inline( 'Upgrade', 'upgrade' ),
24380 $this->get_upgrade_url(),
24381 false,
24382 7,
24383 'upgrade'
24384 );
24385 }
24386
24387 if (
24388 $add_addons_link &&
24389 $this->has_addons() &&
24390 $this->is_submenu_item_visible( 'addons' )
24391 ) {
24392 $this->add_plugin_action_link(
24393 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
24394 $this->_get_admin_page_url( 'addons' ),
24395 false,
24396 9,
24397 'addons'
24398 );
24399 }
24400 }
24401
24402 /**
24403 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
24404 *
24405 * @author Leo Fajardo (@leorw)
24406 * @since 1.1.9
24407 */
24408 function _add_license_action_link() {
24409 $this->_logger->entrance();
24410
24411 if ( ! self::is_ajax() ) {
24412 // Inject license activation dialog UI and client side code.
24413 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
24414 }
24415
24416 $link_text = $this->is_free_plan() ?
24417 $this->get_text_inline( 'Activate License', 'activate-license' ) :
24418 $this->get_text_inline( 'Change License', 'change-license' );
24419
24420 $this->add_plugin_action_link(
24421 $link_text,
24422 '#',
24423 false,
24424 11,
24425 ( 'activate-license ' . $this->get_unique_affix() )
24426 );
24427 }
24428
24429 /**
24430 * @author Leo Fajardo (@leorw)
24431 * @since 2.0.2
24432 */
24433 function _add_premium_version_upgrade_selection_action() {
24434 $this->_logger->entrance();
24435
24436 if ( ! self::is_ajax() ) {
24437 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
24438 }
24439 }
24440
24441 /**
24442 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
24443 *
24444 * @author Leo Fajardo (@leorw)
24445 * @since 1.2.1.5
24446 */
24447 function _add_tracking_links() {
24448 if ( ! current_user_can( 'manage_options' ) ) {
24449 return;
24450 }
24451
24452 $this->_logger->entrance();
24453
24454 if ( $this->is_only_premium() && $this->is_free_plan() ) {
24455 // 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.
24456 return;
24457 }
24458
24459 if (
24460 $this->is_addon() &&
24461 ! $this->is_only_premium()
24462 ) {
24463 $parent = $this->get_parent_instance();
24464
24465 if ( is_object( $parent ) && $parent->is_anonymous() ) {
24466 return;
24467 }
24468 }
24469
24470 if ( fs_is_network_admin() ) {
24471 if ( ! $this->_is_network_active ) {
24472 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
24473 return;
24474 } else if ( $this->is_network_delegated_connection() ) {
24475 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
24476 return;
24477 }
24478 } else {
24479 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
24480 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
24481 return;
24482 }
24483 }
24484
24485 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
24486 if ( ! $this->is_registered() && $this->is_anonymous() ) {
24487 $this->connect_again();
24488
24489 return;
24490 }
24491 }
24492
24493 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
24494 ( $this->is_theme() && ! self::is_themes_page() )
24495 ) {
24496 // Only show tracking links on the plugins and themes pages.
24497 return;
24498 }
24499
24500 if (
24501 $this->is_activation_mode() &&
24502 $this->is_premium() &&
24503 ! $this->is_registered()
24504 ) {
24505 // If not yet registered and running the premium code base, a license activation link will already be shown.
24506 return;
24507 }
24508
24509 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24510 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
24511 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
24512 return;
24513 }
24514 }
24515
24516 if ( $this->add_ajax_action( 'toggle_permission_tracking', array( &$this, '_toggle_permission_tracking_callback' ) ) ) {
24517 return;
24518 }
24519
24520 $link_text_id = '';
24521 $url = '#';
24522
24523 if ( $this->is_registered( true ) ) {
24524 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24525 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
24526 } else {
24527 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24528 }
24529 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
24530 /**
24531 * Show opt-in link only if skipped or in activation mode.
24532 */
24533 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24534
24535 $params = ! $this->is_anonymous() ?
24536 array() :
24537 array(
24538 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
24539 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
24540 );
24541
24542 $url = $this->get_activation_url( $params );
24543 }
24544
24545 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
24546
24547 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
24548 $this->add_plugin_action_link(
24549 $link_text_id,
24550 $url,
24551 false,
24552 13,
24553 "opt-in-or-opt-out {$this->_slug}"
24554 );
24555 }
24556 }
24557
24558 /**
24559 * Get the URL of the page that should be loaded right after the plugin activation.
24560 *
24561 * @author Vova Feldman (@svovaf)
24562 * @since 1.1.7.4
24563 *
24564 * @return string
24565 */
24566 function get_after_plugin_activation_redirect_url() {
24567 $url = false;
24568
24569 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
24570 $first_time_path = $this->_menu->get_first_time_path(
24571 fs_is_network_admin() && $this->_is_network_active
24572 );
24573
24574 if ( $this->is_activation_mode() ) {
24575 $url = $this->get_activation_url();
24576 } else if ( ! empty( $first_time_path ) ) {
24577 $url = $first_time_path;
24578 } else {
24579 $page = '';
24580 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
24581 if ( $this->is_network_registered() ) {
24582 $page = 'account';
24583 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
24584 $this->maybe_set_slug_and_network_menu_exists_flag();
24585 }
24586 }
24587
24588 $url = $this->_get_admin_page_url( $page );
24589 }
24590 } else {
24591 $plugin_fs = false;
24592
24593 if ( $this->is_parent_plugin_installed() ) {
24594 $plugin_fs = self::get_parent_instance();
24595 }
24596
24597 if ( is_object( $plugin_fs ) ) {
24598 if ( ! $plugin_fs->is_registered() ) {
24599 // Forward to parent plugin connect when parent not registered.
24600 $url = $plugin_fs->get_activation_url();
24601 } else {
24602 // Forward to account page.
24603 $url = $plugin_fs->_get_admin_page_url( 'account' );
24604 }
24605 }
24606 }
24607
24608 return $url;
24609 }
24610
24611 /**
24612 * Forward page to activation page.
24613 *
24614 * @author Vova Feldman (@svovaf)
24615 * @since 1.0.3
24616 */
24617 function _redirect_on_activation_hook() {
24618 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
24619 $url = $this->get_after_plugin_activation_redirect_url();
24620
24621 if ( is_string( $url ) ) {
24622 fs_redirect( $url );
24623 }
24624 }
24625 }
24626
24627 /**
24628 * Modify plugin's page action links collection.
24629 *
24630 * @author Vova Feldman (@svovaf)
24631 * @since 1.0.0
24632 *
24633 * @param array $links
24634 * @param $file
24635 *
24636 * @return array
24637 */
24638 function _modify_plugin_action_links_hook( $links, $file ) {
24639 $this->_logger->entrance();
24640
24641 $passed_deactivate = false;
24642 $deactivate_link = '';
24643 $before_deactivate = array();
24644 $after_deactivate = array();
24645 foreach ( $links as $key => $link ) {
24646 if ( 'deactivate' === $key ) {
24647 $deactivate_link = $link;
24648 $passed_deactivate = true;
24649 continue;
24650 }
24651
24652 if ( ! $passed_deactivate ) {
24653 $before_deactivate[ $key ] = $link;
24654 } else {
24655 $after_deactivate[ $key ] = $link;
24656 }
24657 }
24658
24659 ksort( $this->_action_links );
24660
24661 foreach ( $this->_action_links as $new_links ) {
24662 foreach ( $new_links as $link ) {
24663 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
24664 }
24665 }
24666
24667 if ( ! empty( $deactivate_link ) ) {
24668 /**
24669 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
24670 *
24671 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
24672 */
24673 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
24674
24675 // Append deactivation link.
24676 $before_deactivate['deactivate'] = $deactivate_link;
24677 }
24678
24679 return array_merge( $before_deactivate, $after_deactivate );
24680 }
24681
24682 /**
24683 * Adds admin message.
24684 *
24685 * @author Vova Feldman (@svovaf)
24686 * @since 1.0.4
24687 *
24688 * @param string $message
24689 * @param string $title
24690 * @param string $type
24691 */
24692 function add_admin_message( $message, $title = '', $type = 'success' ) {
24693 $this->_admin_notices->add( $message, $title, $type );
24694 }
24695
24696 /**
24697 * Adds sticky admin message.
24698 *
24699 * @author Vova Feldman (@svovaf)
24700 * @since 1.1.0
24701 *
24702 * @param string $message
24703 * @param string $id
24704 * @param string $title
24705 * @param string $type
24706 */
24707 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
24708 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
24709 }
24710
24711 /**
24712 * Check if the paid version of the module is installed.
24713 *
24714 * @author Vova Feldman (@svovaf)
24715 * @since 2.2.0
24716 *
24717 * @return bool
24718 */
24719 private function is_premium_version_installed() {
24720 $premium_plugin_basename = $this->premium_plugin_basename();
24721
24722 if ( $this->is_theme() ) {
24723 return $this->can_activate_theme( $this->get_premium_slug() );
24724 }
24725
24726 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_plugin_basename ) );
24727 }
24728
24729 /**
24730 * Helper function that returns the final steps for the upgrade completion.
24731 *
24732 * If the module is already running the premium code, returns an empty string.
24733 *
24734 * @author Vova Feldman (@svovaf)
24735 * @since 1.2.1
24736 *
24737 * @param string $plan_title
24738 *
24739 * @return string
24740 */
24741 private function get_complete_upgrade_instructions( $plan_title = '' ) {
24742 $this->_logger->entrance();
24743
24744 $activate_license_string = $this->get_license_network_activation_notice();
24745
24746 if ( ! $this->has_premium_version() || $this->is_premium() ) {
24747 return '' . $activate_license_string;
24748 }
24749
24750 if ( empty( $plan_title ) ) {
24751 $plan_title = $this->get_plan_title();
24752 }
24753
24754 if ( $this->is_premium_version_installed() ) {
24755 /**
24756 * If the premium version is already installed, instead of showing the installation instructions,
24757 * tell the current user to activate it.
24758 *
24759 * @author Leo Fajardo (@leorw)
24760 * @since 2.2.1
24761 */
24762 $premium_theme_slug_or_plugin_basename = $this->is_theme() ?
24763 $this->get_premium_slug() :
24764 $this->premium_plugin_basename();
24765
24766 return sprintf(
24767 /* translators: %1$s: Product title; %2$s: Plan title */
24768 $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' ),
24769 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24770 $plan_title,
24771 sprintf(
24772 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
24773 ( $this->is_theme() ?
24774 wp_nonce_url( 'themes.php?action=activate&amp;stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) :
24775 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ),
24776 esc_html( sprintf(
24777 /* translators: %s: Plan title */
24778 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
24779 $plan_title
24780 ) )
24781 )
24782 );
24783 } else {
24784 // @since 1.2.1.5 The free version is auto deactivated.
24785 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
24786 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
24787 '';
24788
24789 return sprintf(
24790 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
24791 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
24792 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
24793 $this->get_latest_download_link( sprintf(
24794 /* translators: %s: Plan title */
24795 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
24796 $plan_title
24797 ) ),
24798 $deactivation_step,
24799 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
24800 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/wp-' . $this->_module_type . '-upload' ),
24801 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
24802 );
24803 }
24804 }
24805
24806 /**
24807 * @author Leo Fajardo (@leorw)
24808 * @since 2.5.3
24809 *
24810 * @param string $message_before_the_instructions
24811 * @param string $message_id
24812 * @param string $plan_title
24813 */
24814 private function add_complete_upgrade_instructions_notice(
24815 $message_before_the_instructions,
24816 $message_id,
24817 $plan_title = ''
24818 ) {
24819 $this->_admin_notices->add_sticky(
24820 $message_before_the_instructions .
24821 $this->get_complete_upgrade_instructions( $plan_title ),
24822 $message_id,
24823 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
24824 );
24825 }
24826
24827 /**
24828 * @author Leo Fajardo (@leorw)
24829 * @since 2.5.3
24830 *
24831 * @param bool $is_upgrade
24832 */
24833 private function add_after_plan_activation_or_upgrade_instructions_notice( $is_upgrade = true ) {
24834 $this->add_complete_upgrade_instructions_notice(
24835 $is_upgrade ?
24836 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) :
24837 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ),
24838 'plan_upgraded'
24839 );
24840 }
24841
24842 /**
24843 * @author Leo Fajardo (@leorw)
24844 * @since 2.1.0
24845 *
24846 * @param string $url
24847 * @param array $request
24848 */
24849 private static function enrich_request_for_debug( &$url, &$request ) {
24850 if ( WP_FS__DEBUG_SDK || isset( $_COOKIE['XDEBUG_SESSION'] ) ) {
24851 $url = add_query_arg( 'XDEBUG_SESSION_START', rand( 0, 9999999 ), $url );
24852 $url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url );
24853
24854 $request['cookies'] = array(
24855 new WP_Http_Cookie( array(
24856 'name' => 'XDEBUG_SESSION',
24857 'value' => 'PHPSTORM',
24858 ) )
24859 );
24860 }
24861 }
24862
24863 /**
24864 * @author Leo Fajardo (@leorw)
24865 * @since 2.1.0
24866 *
24867 * @param string $url
24868 * @param array $request
24869 * @param int $success_cache_expiration
24870 * @param int $failure_cache_expiration
24871 * @param bool $maybe_enrich_request_for_debug
24872 *
24873 * @return WP_Error|array
24874 */
24875 static function safe_remote_post(
24876 &$url,
24877 $request,
24878 $success_cache_expiration = 0,
24879 $failure_cache_expiration = 0,
24880 $maybe_enrich_request_for_debug = true
24881 ) {
24882 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
24883
24884 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
24885
24886 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
24887 get_transient( $cache_key ) :
24888 false;
24889
24890 if ( false === $response ) {
24891 if ( $maybe_enrich_request_for_debug ) {
24892 self::enrich_request_for_debug( $url, $request );
24893 }
24894
24895 if ( ! isset( $request['method'] ) ) {
24896 $request['method'] = 'POST';
24897 }
24898
24899 $response = FS_Api::remote_request( $url, $request );
24900
24901 if (
24902 'https://' === substr( $url, 0, 8 ) &&
24903 FS_Api::is_ssl_error_response( $response )
24904 ) {
24905 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
24906 $url = 'http://' . substr( $url, 8 );
24907
24908 $request['timeout'] = 15;
24909 $response = FS_Api::remote_request( $url, $request );
24910 }
24911
24912 if ( false !== $cache_key ) {
24913 set_transient(
24914 $cache_key,
24915 $response,
24916 ( ( $response instanceof WP_Error ) ?
24917 $failure_cache_expiration :
24918 $success_cache_expiration )
24919 );
24920 }
24921 }
24922
24923 return $response;
24924 }
24925
24926 /**
24927 * This method is used to enrich the after upgrade notice instructions when the upgraded
24928 * license cannot be activated network wide (license quota isn't large enough).
24929 *
24930 * @author Vova Feldman (@svovaf)
24931 * @since 2.0.0
24932 *
24933 * @return string
24934 */
24935 private function get_license_network_activation_notice() {
24936 if ( ! $this->_is_network_active ) {
24937 // Module isn't network level activated.
24938 return '';
24939 }
24940
24941 if ( ! fs_is_network_admin() ) {
24942 // Not network level admin.
24943 return '';
24944 }
24945
24946 if ( get_blog_count() == 1 ) {
24947 // There's only a single site in the network so if there's a context license it was already activated.
24948 return '';
24949 }
24950
24951 if ( ! is_object( $this->_license ) ) {
24952 // No context license.
24953 return '';
24954 }
24955
24956 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
24957 // 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).
24958 return '';
24959 }
24960
24961 if ( $this->can_activate_license_on_network( $this->_license ) ) {
24962 // 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).
24963 return '';
24964 }
24965
24966 return sprintf(
24967 $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' ),
24968 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
24969 '</a>'
24970 );
24971 }
24972
24973 /**
24974 * @author Vova Feldman (@svovaf)
24975 * @since 1.2.1.7
24976 *
24977 * @param string $key
24978 *
24979 * @return string
24980 */
24981 function get_text( $key ) {
24982 return fs_text( $key, $this->_slug );
24983 }
24984
24985 /**
24986 * @author Vova Feldman (@svovaf)
24987 * @since 1.2.3
24988 *
24989 * @param string $text Translatable string.
24990 * @param string $key String key for overrides.
24991 *
24992 * @return string
24993 */
24994 function get_text_inline( $text, $key = '' ) {
24995 return _fs_text_inline( $text, $key, $this->_slug );
24996 }
24997
24998 /**
24999 * @author Vova Feldman (@svovaf)
25000 * @since 1.2.3
25001 *
25002 * @param string $text Translatable string.
25003 * @param string $context Context information for the translators.
25004 * @param string $key String key for overrides.
25005 *
25006 * @return string
25007 */
25008 function get_text_x_inline( $text, $context, $key ) {
25009 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
25010 }
25011
25012 /**
25013 * @author Vova Feldman (@svovaf)
25014 * @since 1.2.3
25015 *
25016 * @param string $text Translatable string.
25017 * @param string $key String key for overrides.
25018 *
25019 * @return string
25020 */
25021 function esc_html_inline( $text, $key ) {
25022 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
25023 }
25024
25025 #----------------------------------------------------------------------------------
25026 #region Versioning
25027 #----------------------------------------------------------------------------------
25028
25029 /**
25030 * Check if Freemius in SDK upgrade mode.
25031 *
25032 * @author Vova Feldman (@svovaf)
25033 * @since 1.0.9
25034 *
25035 * @return bool
25036 */
25037 function is_sdk_upgrade_mode() {
25038 return isset( $this->_storage->sdk_upgrade_mode ) ?
25039 $this->_storage->sdk_upgrade_mode :
25040 false;
25041 }
25042
25043 /**
25044 * Turn SDK upgrade mode off.
25045 *
25046 * @author Vova Feldman (@svovaf)
25047 * @since 1.0.9
25048 */
25049 function set_sdk_upgrade_complete() {
25050 $this->_storage->sdk_upgrade_mode = false;
25051 }
25052
25053 /**
25054 * Check if plugin upgrade mode.
25055 *
25056 * @author Vova Feldman (@svovaf)
25057 * @since 1.0.9
25058 *
25059 * @return bool
25060 */
25061 function is_plugin_upgrade_mode() {
25062 return isset( $this->_storage->plugin_upgrade_mode ) ?
25063 $this->_storage->plugin_upgrade_mode :
25064 false;
25065 }
25066
25067 /**
25068 * Turn plugin upgrade mode off.
25069 *
25070 * @author Vova Feldman (@svovaf)
25071 * @since 1.0.9
25072 */
25073 function set_plugin_upgrade_complete() {
25074 $this->_storage->plugin_upgrade_mode = false;
25075
25076 $license_migration = ! empty( $this->_storage->license_migration ) ?
25077 $this->_storage->license_migration :
25078 array();
25079
25080 $license_migration['is_migrating'] = false;
25081
25082 $this->_storage->license_migration = $license_migration;
25083 }
25084
25085 #endregion
25086
25087 #----------------------------------------------------------------------------------
25088 #region Permissions
25089 #----------------------------------------------------------------------------------
25090
25091 /**
25092 * Check if specific permission requested.
25093 *
25094 * @author Vova Feldman (@svovaf)
25095 * @since 1.1.6
25096 *
25097 * @param string $permission
25098 *
25099 * @return bool
25100 */
25101 function is_permission_requested( $permission ) {
25102 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
25103 }
25104
25105 #endregion
25106
25107 #----------------------------------------------------------------------------------
25108 #region Auto Activation
25109 #----------------------------------------------------------------------------------
25110
25111 /**
25112 * Hints the SDK if running an auto-installation.
25113 *
25114 * @var bool
25115 */
25116 private $_isAutoInstall = false;
25117
25118 /**
25119 * After upgrade callback to install and auto activate a plugin.
25120 * This code will only be executed on explicit request from the user,
25121 * following the practice Jetpack are using with their theme installations.
25122 *
25123 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
25124 *
25125 * @author Vova Feldman (@svovaf)
25126 * @since 1.2.1.7
25127 */
25128 function _install_premium_version_ajax_action() {
25129 $this->_logger->entrance();
25130
25131 $this->check_ajax_referer( 'install_premium_version' );
25132
25133 if ( ! $this->is_registered() ) {
25134 // Not registered.
25135 self::shoot_ajax_failure( array(
25136 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
25137 'code' => 'premium_installed',
25138 ) );
25139 }
25140
25141 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
25142
25143 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25144 // Invalid ID.
25145 self::shoot_ajax_failure( array(
25146 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25147 'code' => 'invalid_module_id',
25148 ) );
25149 }
25150
25151 if ( $plugin_id == $this->get_id() ) {
25152 if ( $this->is_premium() ) {
25153 // Already using the premium code version.
25154 self::shoot_ajax_failure( array(
25155 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
25156 'code' => 'premium_installed',
25157 ) );
25158 }
25159 if ( ! $this->can_use_premium_code() ) {
25160 // Don't have access to the premium code.
25161 self::shoot_ajax_failure( array(
25162 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
25163 'code' => 'invalid_license',
25164 ) );
25165 }
25166 if ( ! $this->has_release_on_freemius() ) {
25167 // Plugin is a serviceware, no premium code version.
25168 self::shoot_ajax_failure( array(
25169 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
25170 'code' => 'premium_version_missing',
25171 ) );
25172 }
25173 } else {
25174 $addon = $this->get_addon( $plugin_id );
25175
25176 if ( ! is_object( $addon ) ) {
25177 // Invalid add-on ID.
25178 self::shoot_ajax_failure( array(
25179 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25180 'code' => 'invalid_module_id',
25181 ) );
25182 }
25183
25184 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25185 // Premium add-on version is already activated.
25186 self::shoot_ajax_failure( array(
25187 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
25188 'code' => 'premium_installed',
25189 ) );
25190 }
25191 }
25192
25193 $this->_isAutoInstall = true;
25194
25195 // Try to install and activate.
25196 $updater = FS_Plugin_Updater::instance( $this );
25197 $result = $updater->install_and_activate_plugin( $plugin_id );
25198
25199 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
25200 self::shoot_ajax_failure( array(
25201 'message' => $result['message'],
25202 'code' => $result['code'],
25203 ) );
25204 }
25205
25206 self::shoot_ajax_success( $result );
25207 }
25208
25209 /**
25210 * Displays module activation dialog box after a successful upgrade
25211 * where the user explicitly requested to auto download and install
25212 * the premium version.
25213 *
25214 * @author Vova Feldman (@svovaf)
25215 * @since 1.2.1.7
25216 */
25217 function _add_auto_installation_dialog_box() {
25218 $this->_logger->entrance();
25219
25220 if ( ! $this->is_registered() ) {
25221 // Not registered.
25222 return;
25223 }
25224
25225 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
25226
25227 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25228 // Invalid module ID.
25229 return;
25230 }
25231
25232 if ( $plugin_id == $this->get_id() ) {
25233 if ( $this->is_premium() ) {
25234 // Already using the premium code version.
25235 return;
25236 }
25237 if ( ! $this->can_use_premium_code() ) {
25238 // Don't have access to the premium code.
25239 return;
25240 }
25241 if ( ! $this->has_release_on_freemius() ) {
25242 // Plugin is a serviceware, no premium code version.
25243 return;
25244 }
25245 } else {
25246 $addon = $this->get_addon( $plugin_id );
25247
25248 if ( ! is_object( $addon ) ) {
25249 // Invalid add-on ID.
25250 return;
25251 }
25252
25253 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25254 // Premium add-on version is already activated.
25255 return;
25256 }
25257 }
25258
25259 $vars = array(
25260 'id' => $this->_module_id,
25261 'target_module_id' => $plugin_id,
25262 'slug' => $this->_slug,
25263 );
25264
25265 fs_require_template( 'auto-installation.php', $vars );
25266 }
25267
25268 #endregion
25269
25270 #--------------------------------------------------------------------------------
25271 #region Tabs Integration
25272 #--------------------------------------------------------------------------------
25273
25274 #region Module's Original Tabs
25275
25276 /**
25277 * Inject a JavaScript logic to capture the theme tabs HTML.
25278 *
25279 * @author Vova Feldman (@svovaf)
25280 * @since 1.2.2.7
25281 */
25282 function _tabs_capture() {
25283 $this->_logger->entrance();
25284
25285 if (
25286 ! $this->is_product_settings_page() ||
25287 ! $this->should_page_include_tabs() ||
25288 ! $this->is_matching_url( $this->main_menu_url() )
25289 ) {
25290 return;
25291 }
25292
25293 $params = array(
25294 'id' => $this->_module_id,
25295 );
25296
25297 fs_require_once_template( 'tabs-capture-js.php', $params );
25298 }
25299
25300 /**
25301 * Cache theme's tabs HTML for a week. The cache will also be set as expired
25302 * after version and type (free/premium) changes, in addition to the week period.
25303 *
25304 * @author Vova Feldman (@svovaf)
25305 * @since 1.2.2.7
25306 */
25307 function _store_tabs_ajax_action() {
25308 $this->_logger->entrance();
25309
25310 $this->check_ajax_referer( 'store_tabs' );
25311
25312 // Init filesystem if not yet initiated.
25313 WP_Filesystem();
25314
25315 // Get POST body HTML data.
25316 global $wp_filesystem;
25317 $tabs_html = $wp_filesystem->get_contents( "php://input" );
25318
25319 if ( is_string( $tabs_html ) ) {
25320 $tabs_html = trim( $tabs_html );
25321 }
25322
25323 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
25324 self::shoot_ajax_failure();
25325 }
25326
25327 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25328
25329 self::shoot_ajax_success();
25330 }
25331
25332 /**
25333 * Cache theme's settings page custom styles. The cache will also be set as expired
25334 * after version and type (free/premium) changes, in addition to the week period.
25335 *
25336 * @author Vova Feldman (@svovaf)
25337 * @since 1.2.2.7
25338 */
25339 function _store_tabs_styles() {
25340 $this->_logger->entrance();
25341
25342 if (
25343 ! $this->is_product_settings_page() ||
25344 ! $this->should_page_include_tabs() ||
25345 ! $this->is_matching_url( $this->main_menu_url() )
25346 ) {
25347 return;
25348 }
25349
25350 $wp_styles = wp_styles();
25351
25352 $theme_styles_url = get_template_directory_uri();
25353
25354 $stylesheets = array();
25355 foreach ( $wp_styles->queue as $handler ) {
25356 if ( fs_starts_with( $handler, 'fs_' ) ) {
25357 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
25358 continue;
25359 }
25360
25361 /**
25362 * @var _WP_Dependency $stylesheet
25363 */
25364 $stylesheet = $wp_styles->registered[ $handler ];
25365
25366 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
25367 $stylesheets[] = $stylesheet->src;
25368 }
25369 }
25370
25371 if ( ! empty( $stylesheets ) ) {
25372 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25373 }
25374 }
25375
25376 /**
25377 * Check if module's original settings page has any tabs.
25378 *
25379 * @author Vova Feldman (@svovaf)
25380 * @since 1.2.2.7
25381 *
25382 * @return bool
25383 */
25384 private function has_tabs() {
25385 return $this->_cache->has( 'tabs' );
25386 }
25387
25388 /**
25389 * Get module's settings page HTML content, starting
25390 * from the beginning of the <div class="wrap"> element,
25391 * until the tabs HTML (including).
25392 *
25393 * @author Vova Feldman (@svovaf)
25394 * @since 1.2.2.7
25395 *
25396 * @return string
25397 */
25398 private function get_tabs_html() {
25399 $this->_logger->entrance();
25400
25401 return $this->_cache->get( 'tabs' );
25402 }
25403
25404 /**
25405 * Check if page should include tabs.
25406 *
25407 * @author Vova Feldman (@svovaf)
25408 * @since 1.2.2.7
25409 *
25410 * @return bool
25411 */
25412 private function should_page_include_tabs() {
25413 if ( ! $this->has_settings_menu() ) {
25414 // Don't add tabs if no settings at all.
25415 return false;
25416 }
25417
25418 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
25419 // Only add tabs to themes for now.
25420 return false;
25421 }
25422
25423 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
25424 // Only add tabs to monetizing themes.
25425 return false;
25426 }
25427
25428 if ( ! $this->is_product_settings_page() ) {
25429 // Only add tabs if browsing one of the product's setting pages.
25430 return false;
25431 }
25432
25433 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
25434 // Don't include tabs in the activation page.
25435 return false;
25436 }
25437
25438 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
25439 // Don't add tabs on checkout page, we want to reduce distractions
25440 // as much as possible.
25441 return false;
25442 }
25443
25444 return true;
25445 }
25446
25447 /**
25448 * Add the tabs HTML before the setting's page content and
25449 * enqueue any required stylesheets.
25450 *
25451 * @author Vova Feldman (@svovaf)
25452 * @since 1.2.2.7
25453 *
25454 * @return bool If tabs were included.
25455 */
25456 function _add_tabs_before_content() {
25457 $this->_logger->entrance();
25458
25459 if ( ! $this->should_page_include_tabs() ) {
25460 return false;
25461 }
25462
25463 $tabs_html = $this->get_tabs_html();
25464
25465 if ( empty( $tabs_html ) ) {
25466 return false;
25467 }
25468
25469 /**
25470 * Enqueue the original stylesheets that are included in the
25471 * theme settings page. That way, if the theme settings has
25472 * some custom _styled_ content above the tabs UI, this
25473 * will make sure that the styling is preserved.
25474 */
25475 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
25476 if ( is_array( $stylesheets ) ) {
25477 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
25478 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
25479 }
25480 }
25481
25482 // Cut closing </div> tag.
25483 echo substr( trim( $tabs_html ), 0, - 6 );
25484
25485 return true;
25486 }
25487
25488 /**
25489 * Add the tabs closing HTML after the setting's page content.
25490 *
25491 * @author Vova Feldman (@svovaf)
25492 * @since 1.2.2.7
25493 *
25494 * @return bool If tabs closing HTML was included.
25495 */
25496 function _add_tabs_after_content() {
25497 $this->_logger->entrance();
25498
25499 if ( ! $this->should_page_include_tabs() ) {
25500 return false;
25501 }
25502
25503 echo '</div>';
25504
25505 return true;
25506 }
25507
25508 #endregion
25509
25510 /**
25511 * Add in-page JavaScript to inject the Freemius tabs into
25512 * the module's setting tabs section.
25513 *
25514 * @author Vova Feldman (@svovaf)
25515 * @since 1.2.2.7
25516 */
25517 function _add_freemius_tabs() {
25518 $this->_logger->entrance();
25519
25520 if ( ! $this->should_page_include_tabs() ) {
25521 return;
25522 }
25523
25524 $params = array( 'id' => $this->_module_id );
25525 fs_require_once_template( 'tabs.php', $params );
25526 }
25527
25528 #endregion
25529
25530 #--------------------------------------------------------------------------------
25531 #region Customizer Integration for Themes
25532 #--------------------------------------------------------------------------------
25533
25534 /**
25535 * @author Vova Feldman (@svovaf)
25536 * @since 1.2.2.7
25537 *
25538 * @param WP_Customize_Manager $customizer
25539 */
25540 function _customizer_register( $customizer ) {
25541 $this->_logger->entrance();
25542
25543 if ( $this->is_pricing_page_visible() ) {
25544 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
25545
25546 $customizer->add_section( 'freemius_upsell', array(
25547 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
25548 'priority' => 1,
25549 ) );
25550 $customizer->add_setting( 'freemius_upsell', array(
25551 'sanitize_callback' => 'esc_html',
25552 ) );
25553
25554 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
25555 'fs' => $this,
25556 'section' => 'freemius_upsell',
25557 'priority' => 100,
25558 ) ) );
25559 }
25560
25561 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
25562 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
25563
25564 // Main Documentation Link In Customizer Root.
25565 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
25566 'fs' => $this,
25567 'priority' => 1000,
25568 ) ) );
25569 }
25570 }
25571
25572 #endregion
25573
25574 /**
25575 * If the theme has a paid version, add some custom
25576 * styling to the theme's premium version (if exists)
25577 * to highlight that it's the premium version of the
25578 * same theme, making it easier for identification
25579 * after the user upgrades and upload it to the site.
25580 *
25581 * @author Vova Feldman (@svovaf)
25582 * @since 1.2.2.7
25583 */
25584 function _style_premium_theme() {
25585 $this->_logger->entrance();
25586
25587 if ( ! self::is_themes_page() ) {
25588 // Only include in the themes page.
25589 return;
25590 }
25591
25592 if ( ! $this->has_paid_plan() ) {
25593 // Only include if has any paid plans.
25594 return;
25595 }
25596
25597 $params = null;
25598 fs_require_once_template( '/js/jquery.content-change.php', $params );
25599
25600 $params = array(
25601 'slug' => $this->_slug,
25602 'id' => $this->_module_id,
25603 );
25604
25605 fs_require_template( '/js/style-premium-theme.php', $params );
25606 }
25607
25608 /**
25609 * This method will return the absolute URL of the module's local icon.
25610 *
25611 * When you are running your plugin or theme on a **localhost** environment, if the icon
25612 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
25613 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
25614 * If an icon is found, this method will automatically attempt to download the icon and store it
25615 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
25616 *
25617 * It's important to mention that this method is NOT phoning home since the developer will deploy
25618 * the product with the local icon in the assets folder. The download process just simplifies
25619 * the process for the developer.
25620 *
25621 * @author Vova Feldman (@svovaf)
25622 * @since 2.0.0
25623 *
25624 * @return string
25625 */
25626 function get_local_icon_url() {
25627 global $fs_active_plugins;
25628
25629 /**
25630 * @since 1.1.7.5
25631 */
25632 $local_path = $this->apply_filters( 'plugin_icon', false );
25633
25634 if ( is_string( $local_path ) ) {
25635 $icons = array( $local_path );
25636 } else {
25637 $img_dir = WP_FS__DIR_IMG;
25638
25639 // Locate the main assets folder.
25640 if ( 1 < count( $fs_active_plugins->plugins ) ) {
25641 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
25642
25643 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
25644 if ( $data->plugin_path == $this->get_plugin_basename() ) {
25645 $img_dir = $plugin_or_theme_img_dir
25646 . '/'
25647 /**
25648 * The basename will be `themes` or the basename of a custom themes directory.
25649 *
25650 * @author Leo Fajardo (@leorw)
25651 * @since 2.2.3
25652 */
25653 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
25654 . '/assets/img';
25655
25656 break;
25657 }
25658 }
25659 }
25660
25661 // Try to locate the icon in the assets folder.
25662 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
25663
25664 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
25665 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
25666 $icons = array(
25667 fs_normalize_path( $img_dir . '/theme-icon.png' )
25668 );
25669 } else {
25670 $icon_found = false;
25671 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
25672
25673 if ( ! function_exists( 'get_filesystem_method' ) ) {
25674 require_once ABSPATH . 'wp-admin/includes/file.php';
25675 }
25676
25677 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
25678
25679 /**
25680 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
25681 *
25682 * This code will only be executed once during the testing
25683 * of the plugin in a local environment. The plugin icon file WILL
25684 * already exist in the assets folder when the plugin is deployed to
25685 * the repository.
25686 */
25687 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
25688 // Fetch icon from Freemius.
25689 $icon = $this->fetch_remote_icon_url();
25690
25691 // Fetch icon from WordPress.org.
25692 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
25693 if ( ! function_exists( 'plugins_api' ) ) {
25694 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
25695 }
25696
25697 $plugin_information = plugins_api( 'plugin_information', array(
25698 'slug' => $this->_slug,
25699 'fields' => array(
25700 'sections' => false,
25701 'tags' => false,
25702 'icons' => true
25703 )
25704 ) );
25705
25706 if (
25707 ! is_wp_error( $plugin_information )
25708 && isset( $plugin_information->icons )
25709 && ! empty( $plugin_information->icons )
25710 ) {
25711 /**
25712 * Get the smallest icon.
25713 *
25714 * @author Leo Fajardo (@leorw)
25715 * @since 1.2.2
25716 */
25717 $icon = end( $plugin_information->icons );
25718 }
25719 }
25720
25721 if ( ! empty( $icon ) ) {
25722 if ( 0 !== strpos( $icon, 'http' ) ) {
25723 $icon = 'http:' . $icon;
25724 }
25725
25726 /**
25727 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
25728 *
25729 * @author Leo Fajardo (@leorw)
25730 * @since 1.2.2
25731 */
25732 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
25733
25734 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
25735
25736 // Try to download the icon.
25737 $icon_found = fs_download_image( $icon, $local_path );
25738 }
25739 }
25740
25741 if ( ! $icon_found ) {
25742 // No icons found, fallback to default icon.
25743 if ( $have_write_permissions ) {
25744 // If have write permissions, copy default icon.
25745 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
25746 } else {
25747 // If doesn't have write permissions, use default icon path.
25748 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
25749 }
25750 }
25751
25752 $icons = array( $local_path );
25753 }
25754 }
25755 }
25756
25757 $icon_dir = dirname( $icons[0] );
25758
25759 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
25760 }
25761
25762 /**
25763 * Fetch module's extended info.
25764 *
25765 * @author Vova Feldman (@svovaf)
25766 * @since 2.0.0
25767 *
25768 * @return object|mixed
25769 */
25770 private function fetch_module_info() {
25771 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
25772 }
25773
25774 /**
25775 * Fetch module's remote icon URL.
25776 *
25777 * @author Vova Feldman (@svovaf)
25778 * @since 2.0.0
25779 *
25780 * @return string
25781 */
25782 function fetch_remote_icon_url() {
25783 $info = $this->fetch_module_info();
25784
25785 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
25786 $info->icon :
25787 '';
25788 }
25789
25790 #--------------------------------------------------------------------------------
25791 #region GDPR
25792 #--------------------------------------------------------------------------------
25793
25794 /**
25795 * @author Leo Fajardo (@leorw)
25796 * @since 2.1.0
25797 *
25798 * @param array $user_plugins
25799 *
25800 * @return string
25801 */
25802 private function get_gdpr_admin_notice_string( $user_plugins ) {
25803 $this->_logger->entrance();
25804
25805 $addons = self::get_all_addons();
25806
25807 foreach ( $user_plugins as $user_plugin ) {
25808 $has_addons = isset( $addons[ $user_plugin->id ] );
25809
25810 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
25811 if ( $this->_module_id == $user_plugin->id ) {
25812 $addons = $this->get_addons();
25813 $has_addons = ( ! empty( $addons ) );
25814 } else {
25815 $plugin_api = FS_Api::instance(
25816 $user_plugin->id,
25817 'plugin',
25818 $user_plugin->id,
25819 $user_plugin->public_key,
25820 ! $user_plugin->is_live,
25821 false,
25822 $this->get_sdk_version()
25823 );
25824
25825 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
25826
25827 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
25828 is_array( $addons_result->plugins ) &&
25829 ! empty( $addons_result->plugins )
25830 ) {
25831 $has_addons = true;
25832 }
25833 }
25834 }
25835
25836 $user_plugin->has_addons = $has_addons;
25837 }
25838
25839 $is_single_parent_product = ( 1 === count( $user_plugins ) );
25840
25841 $multiple_products_text = '';
25842
25843 if ( $is_single_parent_product ) {
25844 $single_parent_product = reset( $user_plugins );
25845
25846 $thank_you = sprintf(
25847 "<span data-plugin-id='%d'>%s</span>",
25848 $single_parent_product->id,
25849 sprintf(
25850 $single_parent_product->has_addons ?
25851 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
25852 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
25853 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
25854 )
25855 );
25856
25857 $already_opted_in = sprintf(
25858 $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' ),
25859 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
25860 );
25861 } else {
25862 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
25863 $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' );
25864
25865 $products_and_add_ons = '';
25866 foreach ( $user_plugins as $user_plugin ) {
25867 if ( ! empty( $products_and_add_ons ) ) {
25868 $products_and_add_ons .= ', ';
25869 }
25870
25871 if ( ! $user_plugin->has_addons ) {
25872 $products_and_add_ons .= sprintf(
25873 "<span data-plugin-id='%d'>%s</span>",
25874 $user_plugin->id,
25875 $user_plugin->title
25876 );
25877 } else {
25878 $products_and_add_ons .= sprintf(
25879 "<span data-plugin-id='%d'>%s</span>",
25880 $user_plugin->id,
25881 sprintf(
25882 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
25883 $user_plugin->title
25884 )
25885 );
25886 }
25887 }
25888
25889 $multiple_products_text = sprintf(
25890 "<small class='products'><strong>%s:</strong> %s</small>",
25891 $this->get_text_inline( 'Products', 'products' ),
25892 $products_and_add_ons
25893 );
25894 }
25895
25896 $actions = sprintf(
25897 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
25898 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
25899 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
25900 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
25901 sprintf(
25902 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
25903 '<span class="underlined">',
25904 '</span>'
25905 )
25906 );
25907
25908 return sprintf(
25909 '%s %s %s',
25910 $thank_you,
25911 $already_opted_in,
25912 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>' ) .
25913 '<br><br>' .
25914 '<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>' .
25915 $actions .
25916 ( $is_single_parent_product ? '' : $multiple_products_text )
25917 );
25918 }
25919
25920 /**
25921 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
25922 * plugins and themes they've opted in to.
25923 *
25924 * @author Leo Fajardo (@leorw)
25925 * @since 2.1.0
25926 *
25927 * @param string $user_email
25928 * @param string $license_key
25929 * @param array $plugin_ids
25930 * @param string|null $license_key
25931 *
25932 * @return array|false
25933 */
25934 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
25935 $request = array(
25936 'method' => 'POST',
25937 'body' => array(),
25938 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
25939 );
25940
25941 if ( is_string( $user_email ) ) {
25942 $request['body']['email'] = $user_email;
25943 } else {
25944 $request['body']['license_key'] = $license_key;
25945 }
25946
25947 $result = array();
25948
25949 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
25950 $total_plugin_ids = count( $plugin_ids );
25951
25952 $plugin_ids_count_per_request = 10;
25953 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
25954 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
25955
25956 $request['body']['plugin_ids'] = $plugin_ids_set;
25957
25958 $response = self::safe_remote_post(
25959 $url,
25960 $request,
25961 WP_FS__TIME_24_HOURS_IN_SEC,
25962 WP_FS__TIME_12_HOURS_IN_SEC
25963 );
25964
25965 if ( ! is_wp_error( $response ) ) {
25966 $decoded = is_string( $response['body'] ) ?
25967 json_decode( $response['body'] ) :
25968 null;
25969
25970 if (
25971 !is_object($decoded) ||
25972 !isset($decoded->success) ||
25973 true !== $decoded->success ||
25974 !isset( $decoded->data ) ||
25975 !is_array( $decoded->data )
25976 ) {
25977 return false;
25978 }
25979
25980 $result = array_merge( $result, $decoded->data );
25981 }
25982 }
25983
25984 return $result;
25985 }
25986
25987 /**
25988 * @author Leo Fajardo (@leorw)
25989 * @since 2.1.0
25990 */
25991 function _maybe_show_gdpr_admin_notice() {
25992 if ( ! $this->is_user_in_admin() ) {
25993 return;
25994 }
25995
25996 if ( ! $this->should_handle_gdpr_admin_notice() ) {
25997 return;
25998 }
25999
26000 if ( ! $this->is_user_admin() ) {
26001 return;
26002 }
26003
26004 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26005
26006 $lock = FS_User_Lock::instance();
26007
26008 /**
26009 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
26010 */
26011 if ( ! $lock->try_lock( 60 ) ) {
26012 return;
26013 }
26014
26015 /**
26016 * @var $current_wp_user WP_User
26017 */
26018 $current_wp_user = self::_get_current_wp_user();
26019
26020 /**
26021 * @var FS_User $current_fs_user
26022 */
26023 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
26024
26025 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
26026
26027 if ( ! is_object( $current_fs_user ) ) {
26028 // 10-year lock.
26029 $lock->lock( $ten_years_in_sec );
26030
26031 return;
26032 }
26033
26034 $gdpr = FS_GDPR_Manager::instance();
26035
26036 if ( $gdpr->is_opt_in_notice_shown() ) {
26037 // 30-day lock.
26038 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26039
26040 return;
26041 }
26042
26043 if ( ! $gdpr->should_show_opt_in_notice() ) {
26044 // 10-year lock.
26045 $lock->lock( $ten_years_in_sec );
26046
26047 return;
26048 }
26049
26050 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
26051 $was_notice_shown_before = ( false !== $last_time_notice_shown );
26052
26053 if ( $was_notice_shown_before &&
26054 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
26055 ) {
26056 // If the notice was shown before, show it again after 30 days from the last time it was shown.
26057 return;
26058 }
26059
26060 /**
26061 * Find all plugin IDs that were installed by the current admin.
26062 */
26063 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
26064
26065 if ( empty( $plugin_ids_map )) {
26066 $lock->lock( $ten_years_in_sec );
26067
26068 return;
26069 }
26070
26071 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26072 $current_fs_user->email,
26073 null,
26074 array_keys( $plugin_ids_map )
26075 );
26076
26077 if ( empty( $user_plugins ) ) {
26078 $lock->lock(
26079 is_array($user_plugins) ?
26080 $ten_years_in_sec :
26081 // Lock for 24-hours on errors.
26082 WP_FS__TIME_24_HOURS_IN_SEC
26083 );
26084
26085 return;
26086 }
26087
26088 $has_unset_marketing_optin = false;
26089
26090 foreach ( $user_plugins as $user_plugin ) {
26091 if ( true == $user_plugin->is_marketing_allowed ) {
26092 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
26093 }
26094
26095 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
26096 $has_unset_marketing_optin = true;
26097 }
26098 }
26099
26100 if ( empty( $plugin_ids_map ) ||
26101 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
26102 ) {
26103 $lock->lock( $ten_years_in_sec );
26104
26105 return;
26106 }
26107
26108 $modules = array_merge(
26109 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26110 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26111 );
26112
26113 foreach ( $modules as $module ) {
26114 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
26115 $plugin_ids_map[ $module->id ] = $module;
26116 }
26117 }
26118
26119 $plugin_title = null;
26120 if ( 1 === count( $plugin_ids_map ) ) {
26121 $module = reset( $plugin_ids_map );
26122 $plugin_title = $module->title;
26123 }
26124
26125 $gdpr->add_opt_in_sticky_notice(
26126 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
26127 $plugin_title
26128 );
26129
26130 $this->add_gdpr_optin_ajax_handler_and_style();
26131
26132 $gdpr->notice_was_just_shown();
26133
26134 // 30-day lock.
26135 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26136 }
26137
26138 /**
26139 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
26140 * marketing.
26141 *
26142 * @author Leo Fajardo (@leorw)
26143 * @since 2.1.0
26144 */
26145 private function disable_opt_in_notice_and_lock_user() {
26146 FS_GDPR_Manager::instance()->disable_opt_in_notice();
26147
26148 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26149
26150 // 10-year lock.
26151 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26152 }
26153
26154 /**
26155 * @author Leo Fajardo (@leorw)
26156 * @since 2.5.4
26157 */
26158 static function _add_api_connectivity_notice_handler_js() {
26159 fs_require_once_template( 'api-connectivity-message-js.php' );
26160 }
26161
26162 /**
26163 * @author Leo Fajardo (@leorw)
26164 * @since 2.1.0
26165 */
26166 function _add_gdpr_optin_js() {
26167 $vars = array( 'id' => $this->_module_id );
26168
26169 fs_require_once_template( 'gdpr-optin-js.php', $vars );
26170 }
26171
26172 /**
26173 * @author Leo Fajardo (@leorw)
26174 * @since 2.1.0
26175 */
26176 function enqueue_gdpr_optin_notice_style() {
26177 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
26178 }
26179
26180 /**
26181 * @author Leo Fajardo (@leorw)
26182 * @since 2.1.0
26183 */
26184 function _maybe_add_gdpr_optin_ajax_handler() {
26185 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
26186
26187 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
26188 $this->add_gdpr_optin_ajax_handler_and_style();
26189 }
26190 }
26191
26192 /**
26193 * @author Leo Fajardo (@leorw)
26194 * @since 2.1.0
26195 */
26196 function _fetch_is_marketing_required_flag_value_ajax_action() {
26197 $this->_logger->entrance();
26198
26199 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
26200
26201 $license_key = fs_request_get_raw( 'license_key' );
26202
26203 if ( empty($license_key) ) {
26204 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
26205 }
26206
26207 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26208 null,
26209 $license_key,
26210 array( $this->_module_id )
26211 );
26212
26213 if ( ! is_array( $user_plugins ) ||
26214 empty($user_plugins) ||
26215 !isset($user_plugins[0]->plugin_id) ||
26216 $user_plugins[0]->plugin_id != $this->_module_id
26217 ) {
26218 /**
26219 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
26220 *
26221 * @author Vova Feldman (@svovaf)
26222 */
26223 self::shoot_ajax_success( array(
26224 'is_marketing_allowed' => null,
26225 'license_owner_id' => null
26226 ) );
26227 }
26228
26229 self::shoot_ajax_success( array(
26230 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
26231 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
26232 ) );
26233 }
26234
26235 /**
26236 * @author Leo Fajardo (@leorw)
26237 * @since 2.3.2
26238 *
26239 * @param number[] $install_ids
26240 *
26241 * @return array {
26242 * An array of objects containing the installs' licenses owners data.
26243 *
26244 * @property number $id User ID.
26245 * @property string $email User email (can be masked email).
26246 * }
26247 */
26248 private function fetch_installs_licenses_owners_data( $install_ids ) {
26249 $this->_logger->entrance();
26250
26251 $response = $this->get_api_user_scope()->get(
26252 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
26253 );
26254
26255 $license_owners = array();
26256
26257 if ( $this->is_api_result_object( $response, 'owners' ) ) {
26258 $license_owners = $response->owners;
26259 }
26260
26261 return $license_owners;
26262 }
26263
26264 /**
26265 * @author Leo Fajardo (@leorw)
26266 * @since 2.1.0
26267 */
26268 private function add_gdpr_optin_ajax_handler_and_style() {
26269 // Add GDPR action AJAX callback.
26270 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
26271
26272 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
26273 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
26274 }
26275
26276 /**
26277 * @author Leo Fajardo (@leorw)
26278 * @since 2.1.0
26279 */
26280 function _gdpr_optin_ajax_action() {
26281 $this->_logger->entrance();
26282
26283 $this->check_ajax_referer( 'gdpr_optin_action' );
26284
26285 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
26286 self::shoot_ajax_failure();
26287 }
26288
26289 $current_wp_user = self::_get_current_wp_user();
26290
26291 $plugin_ids = fs_request_get( 'plugin_ids', array() );
26292 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
26293 self::shoot_ajax_failure();
26294 }
26295
26296 $modules = array_merge(
26297 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26298 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26299 );
26300
26301 foreach ( $modules as $key => $module ) {
26302 if ( ! in_array( $module->id, $plugin_ids ) ) {
26303 unset( $modules[ $key ] );
26304 }
26305 }
26306
26307 if ( empty( $modules ) ) {
26308 self::shoot_ajax_failure();
26309 }
26310
26311 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
26312
26313 foreach ( $modules as $module ) {
26314 $user_api->call( "?plugin_id={$module->id}", 'put', array(
26315 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
26316 ) );
26317 }
26318
26319 FS_GDPR_Manager::instance()->remove_opt_in_notice();
26320
26321 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26322
26323 // 10-year lock.
26324 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26325
26326 self::shoot_ajax_success();
26327 }
26328
26329 /**
26330 * 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.
26331 *
26332 * @author Vova Feldman (@svovaf)
26333 * @since 2.1.0
26334 *
26335 * @return bool
26336 */
26337 private function should_handle_gdpr_admin_notice() {
26338 return $this->apply_filters(
26339 'handle_gdpr_admin_notice',
26340 // Default to false.
26341 false
26342 );
26343 }
26344
26345 #endregion
26346
26347 #----------------------------------------------------------------------------------
26348 #region Marketing
26349 #----------------------------------------------------------------------------------
26350
26351 /**
26352 * Check if current user purchased any other plugins before.
26353 *
26354 * @author Vova Feldman (@svovaf)
26355 * @since 1.0.9
26356 *
26357 * @return bool
26358 */
26359 function has_purchased_before() {
26360 // TODO: Implement has_purchased_before() method.
26361 throw new Exception( 'not implemented' );
26362 }
26363
26364 /**
26365 * Check if current user classified as an agency.
26366 *
26367 * @author Vova Feldman (@svovaf)
26368 * @since 1.0.9
26369 *
26370 * @return bool
26371 */
26372 function is_agency() {
26373 // TODO: Implement is_agency() method.
26374 throw new Exception( 'not implemented' );
26375 }
26376
26377 /**
26378 * Check if current user classified as a developer.
26379 *
26380 * @author Vova Feldman (@svovaf)
26381 * @since 1.0.9
26382 *
26383 * @return bool
26384 */
26385 function is_developer() {
26386 // TODO: Implement is_developer() method.
26387 throw new Exception( 'not implemented' );
26388 }
26389
26390 /**
26391 * Check if current user classified as a business.
26392 *
26393 * @author Vova Feldman (@svovaf)
26394 * @since 1.0.9
26395 *
26396 * @return bool
26397 */
26398 function is_business() {
26399 // TODO: Implement is_business() method.
26400 throw new Exception( 'not implemented' );
26401 }
26402
26403 #endregion
26404
26405 #----------------------------------------------------------------------------------
26406 #region Helper
26407 #----------------------------------------------------------------------------------
26408
26409 /**
26410 * If running with a secret key, assume it's the developer and show pending plans as well.
26411 *
26412 * @author Vova Feldman (@svovaf)
26413 * @since 2.1.2
26414 *
26415 * @param string $path
26416 *
26417 * @return string
26418 */
26419 function add_show_pending( $path ) {
26420 if ( ! $this->has_secret_key() ) {
26421 return $path;
26422 }
26423
26424 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
26425 }
26426
26427 #endregion
26428 }
26429