PluginProbe
StreamCast – bring live radio to your site with a sleek player / 2.2.3
StreamCast – bring live radio to your site with a sleek player v2.2.3
2.4.5 2.4.4 trunk 1.0 1.1 2.0.0 2.1.10 2.1.2 2.1.4 2.1.5 2.1.8 2.2.1 2.2.3 2.2.4 2.2.5 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.8 2.3.9 All 29 releases
streamcast / freemius / includes / class-freemius.php

class-freemius.php in StreamCast – bring live radio to your site with a sleek player 2.2.3, at freemius/includes/class-freemius.php

26,474 lines 994.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_connected = isset( $this->_storage->connectivity_test['is_connected'] ) ?
4178 $this->_storage->connectivity_test['is_connected'] :
4179 null;
4180 $is_active = ( $this->_storage->connectivity_test['is_active'] || is_object( $this->_site ) );
4181 } else {
4182 $is_connected = null;
4183 $is_active = $this->should_turn_fs_on( $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() ) );
4184 }
4185
4186 $this->store_connectivity_info( (object) array( 'is_active' => $is_active ), $is_connected );
4187
4188 if ( $is_active ) {
4189 $this->_is_on = true;
4190 }
4191
4192 return $this->_has_api_connection;
4193 }
4194
4195 /**
4196 * @author Leo Fajardo (@leorw)
4197 * @since 2.5.4
4198 */
4199 private function clear_connectivity_info() {
4200 unset( $this->_storage->connectivity_test );
4201
4202 FS_Api::clear_force_http_flag();
4203 }
4204
4205 /**
4206 * @author Vova Feldman (@svovaf)
4207 * @since 1.1.7.4
4208 *
4209 * @param object $pong
4210 * @param bool|null $is_connected
4211 */
4212 private function store_connectivity_info( $pong, $is_connected ) {
4213 $this->_logger->entrance();
4214
4215 $version = $this->get_plugin_version();
4216
4217 if ( false === $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
4218 $is_active = false;
4219 } else {
4220 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
4221 }
4222
4223 $is_active = $this->apply_filters(
4224 'is_on',
4225 $is_active,
4226 $this->is_plugin_update(),
4227 $version
4228 );
4229
4230 $this->_storage->connectivity_test = array(
4231 'is_connected' => $is_connected,
4232 'host' => $_SERVER['HTTP_HOST'],
4233 'server_ip' => WP_FS__REMOTE_ADDR,
4234 'is_active' => $is_active,
4235 'timestamp' => WP_FS__SCRIPT_START_TIME,
4236 // Last version with connectivity attempt.
4237 'version' => $version,
4238 );
4239
4240 $this->_has_api_connection = $is_connected;
4241 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
4242 }
4243
4244 /**
4245 * @author Leo Fajardo (@leorw)
4246 * @since 2.5.4
4247 *
4248 * @param bool $is_connected
4249 */
4250 private function update_connectivity_info( $is_connected ) {
4251 $this->store_connectivity_info(
4252 // 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.
4253 (object) array( 'is_active' => true ),
4254 $is_connected
4255 );
4256 }
4257
4258 /**
4259 * Force turning Freemius on.
4260 *
4261 * @author Vova Feldman (@svovaf)
4262 * @since 1.1.8.1
4263 *
4264 * @return bool TRUE if successfully turned on.
4265 */
4266 private function turn_on() {
4267 $this->_logger->entrance();
4268
4269 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
4270 return false;
4271 }
4272
4273 $updated_connectivity = $this->_storage->connectivity_test;
4274 $updated_connectivity['is_active'] = true;
4275 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
4276 $this->_storage->connectivity_test = $updated_connectivity;
4277
4278 $this->_is_on = true;
4279
4280 return true;
4281 }
4282
4283 /**
4284 * Anonymous and unique site identifier (Hash).
4285 *
4286 * @author Vova Feldman (@svovaf)
4287 * @since 1.1.0
4288 *
4289 * @param null|int $blog_id Since 2.0.0
4290 *
4291 * @return string
4292 */
4293 function get_anonymous_id( $blog_id = null ) {
4294 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
4295
4296 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
4297 $key = self::get_unfiltered_site_url( $blog_id, true );
4298
4299 $secure_auth = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
4300 if ( empty( $secure_auth ) ||
4301 false !== strpos( $secure_auth, ' ' ) ||
4302 'put your unique phrase here' === $secure_auth
4303 ) {
4304 // Protect against default auth key.
4305 $secure_auth = md5( microtime() );
4306 }
4307
4308 /**
4309 * Base the unique identifier on the WP secure authentication key. Which
4310 * turns the key into a secret anonymous identifier. This will help us
4311 * to avoid duplicate installs generation on the backend upon opt-in.
4312 *
4313 * @author Vova Feldman (@svovaf)
4314 * @since 1.2.3
4315 */
4316 $unique_id = md5( $key . $secure_auth );
4317
4318 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
4319 }
4320
4321 $this->_logger->departure( $unique_id );
4322
4323 return $unique_id;
4324 }
4325
4326 /**
4327 * Returns anonymous network ID.
4328 *
4329 * @since 2.4.3
4330 *
4331 * @return string
4332 */
4333 function get_anonymous_network_id() {
4334 return $this->get_anonymous_id( get_network()->site_id );
4335 }
4336
4337 /**
4338 * @author Vova Feldman (@svovaf)
4339 * @since 1.1.7.4
4340 *
4341 * @return \WP_User
4342 */
4343 static function _get_current_wp_user() {
4344 self::require_pluggable_essentials();
4345 self::wp_cookie_constants();
4346
4347 return wp_get_current_user();
4348 }
4349
4350 /**
4351 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
4352 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
4353 * is network activated the cookie constants are only configured after the network
4354 * plugins activation, therefore, if we don't define those constants WP will throw
4355 * PHP warnings/notices.
4356 *
4357 * @author Vova Feldman (@svovaf)
4358 * @since 2.1.1
4359 */
4360 private static function wp_cookie_constants() {
4361 if ( defined( 'LOGGED_IN_COOKIE' ) &&
4362 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
4363 ) {
4364 return;
4365 }
4366
4367 /**
4368 * Used to guarantee unique hash cookies
4369 *
4370 * @since 1.5.0
4371 */
4372 if ( ! defined( 'COOKIEHASH' ) ) {
4373 $siteurl = get_site_option( 'siteurl' );
4374 if ( $siteurl ) {
4375 define( 'COOKIEHASH', md5( $siteurl ) );
4376 } else {
4377 define( 'COOKIEHASH', '' );
4378 }
4379 }
4380
4381 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
4382 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
4383 }
4384
4385 /**
4386 * @since 2.5.0
4387 */
4388 if ( ! defined( 'AUTH_COOKIE' ) ) {
4389 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
4390 }
4391
4392 /**
4393 * @since 2.6.0
4394 */
4395 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
4396 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
4397 }
4398 }
4399
4400 /**
4401 * @author Vova Feldman (@svovaf)
4402 * @since 2.1.0
4403 *
4404 * @return int
4405 */
4406 static function get_current_wp_user_id() {
4407 $wp_user = self::_get_current_wp_user();
4408
4409 return $wp_user->ID;
4410 }
4411
4412 /**
4413 * @author Vova Feldman (@svovaf)
4414 * @since 1.2.1.7
4415 *
4416 * @param string $email
4417 *
4418 * @return bool
4419 */
4420 static function is_valid_email( $email ) {
4421 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4422 return false;
4423 }
4424
4425 $parts = explode( '@', $email );
4426
4427 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4428 return false;
4429 }
4430
4431 $blacklist = array(
4432 'admin.',
4433 'webmaster.',
4434 'localhost.',
4435 'dev.',
4436 'development.',
4437 'test.',
4438 'stage.',
4439 'staging.',
4440 );
4441
4442 // Make sure domain is not one of the blacklisted.
4443 foreach ( $blacklist as $invalid ) {
4444 if ( 0 === strpos( $parts[1], $invalid ) ) {
4445 return false;
4446 }
4447 }
4448
4449 // Get the UTF encoded domain name.
4450 /**
4451 * @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.
4452 * @author - @swashata
4453 */
4454 $is_new_idn_available = (
4455 version_compare( PHP_VERSION, '5.6.40') > 0 &&
4456 defined( 'IDNA_DEFAULT' ) &&
4457 defined( 'INTL_IDNA_VARIANT_UTS46' )
4458 );
4459 if ( $is_new_idn_available ) {
4460 $domain = idn_to_ascii( $parts[1], IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 );
4461 } else {
4462 $domain = idn_to_ascii( $parts[1] ); // phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet
4463 }
4464
4465 $domain = $domain . '.';
4466
4467 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4468 }
4469
4470 #endregion
4471
4472 #----------------------------------------------------------------------------------
4473 #region Email
4474 #----------------------------------------------------------------------------------
4475
4476 /**
4477 * Generates and sends an HTML email with customizable sections.
4478 *
4479 * @author Leo Fajardo (@leorw)
4480 * @since 1.1.2
4481 *
4482 * @param string $to_address
4483 * @param string $subject
4484 * @param array $sections
4485 * @param array $headers
4486 *
4487 * @return bool Whether the email contents were sent successfully.
4488 */
4489 private function send_email(
4490 $to_address,
4491 $subject,
4492 $sections = array(),
4493 $headers = array()
4494 ) {
4495 $default_sections = $this->get_email_sections();
4496
4497 // Insert new sections or replace the default email sections.
4498 if ( is_array( $sections ) && ! empty( $sections ) ) {
4499 foreach ( $sections as $section_id => $custom_section ) {
4500 if ( ! isset( $default_sections[ $section_id ] ) ) {
4501 // If the section does not exist, add it.
4502 $default_sections[ $section_id ] = $custom_section;
4503 } else {
4504 // If the section already exists, override it.
4505 $current_section = $default_sections[ $section_id ];
4506
4507 // Replace the current section's title if a custom section title exists.
4508 if ( isset( $custom_section['title'] ) ) {
4509 $current_section['title'] = $custom_section['title'];
4510 }
4511
4512 // Insert new rows under the current section or replace the default rows.
4513 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4514 foreach ( $custom_section['rows'] as $row_id => $row ) {
4515 $current_section['rows'][ $row_id ] = $row;
4516 }
4517 }
4518
4519 $default_sections[ $section_id ] = $current_section;
4520 }
4521 }
4522 }
4523
4524 $vars = array( 'sections' => $default_sections );
4525 $message = fs_get_template( 'email.php', $vars );
4526
4527 // Set the type of email to HTML.
4528 $headers[] = 'Content-type: text/html; charset=UTF-8';
4529
4530 $header_string = implode( "\r\n", $headers );
4531
4532 return wp_mail(
4533 $to_address,
4534 $subject,
4535 $message,
4536 $header_string
4537 );
4538 }
4539
4540 /**
4541 * Generates the data for the sections of the email content.
4542 *
4543 * @author Leo Fajardo (@leorw)
4544 * @since 1.1.2
4545 *
4546 * @return array
4547 */
4548 private function get_email_sections() {
4549 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4550 $current_user = self::_get_current_wp_user();
4551
4552 // Retrieve the cURL version information so that we can get the version number below.
4553 $curl_version_information = curl_version();
4554
4555 $active_plugin = self::get_active_plugins();
4556
4557 // Generate the list of active plugins separated by new line.
4558 $active_plugin_string = '';
4559 foreach ( $active_plugin as $plugin ) {
4560 $active_plugin_string .= sprintf(
4561 '<a href="%s">%s</a> [v%s]<br>',
4562 $plugin['PluginURI'],
4563 $plugin['Name'],
4564 $plugin['Version']
4565 );
4566 }
4567
4568 $server_ip = WP_FS__REMOTE_ADDR;
4569
4570 // Add PHP info for deeper investigation.
4571 ob_start();
4572 phpinfo();
4573 $php_info = ob_get_clean();
4574
4575 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4576
4577 // Generate the default email sections.
4578 $sections = array(
4579 'sdk' => array(
4580 'title' => 'SDK',
4581 'rows' => array(
4582 'fs_version' => array( 'FS Version', $this->version ),
4583 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4584 )
4585 ),
4586 'plugin' => array(
4587 'title' => ucfirst( $this->get_module_type() ),
4588 'rows' => array(
4589 'name' => array( 'Name', $this->get_plugin_name() ),
4590 'version' => array( 'Version', $this->get_plugin_version() )
4591 )
4592 ),
4593 'api' => array(
4594 'title' => 'API Subdomain',
4595 'rows' => array(
4596 'dns' => array(
4597 'DNS_CNAME',
4598 function_exists( 'dns_get_record' ) ?
4599 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4600 'dns_get_record() disabled/blocked'
4601 ),
4602 'ip' => array(
4603 'IP',
4604 function_exists( 'gethostbyname' ) ?
4605 gethostbyname( $api_domain ) :
4606 'gethostbyname() disabled/blocked'
4607 ),
4608 ),
4609 ),
4610 'site' => array(
4611 'title' => 'Site',
4612 'rows' => array(
4613 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4614 'address' => array( 'Address', site_url() ),
4615 'host' => array(
4616 'HTTP_HOST',
4617 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4618 ),
4619 'hosting' => array(
4620 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4621 fs_request_get( 'hosting_company' ) :
4622 'Unknown',
4623 ),
4624 'server_addr' => array(
4625 'SERVER_ADDR',
4626 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4627 )
4628 )
4629 ),
4630 'user' => array(
4631 'title' => 'User',
4632 'rows' => array(
4633 'email' => array( 'Email', $current_user->user_email ),
4634 'first' => array( 'First', $current_user->user_firstname ),
4635 'last' => array( 'Last', $current_user->user_lastname )
4636 )
4637 ),
4638 'plugins' => array(
4639 'title' => 'Plugins',
4640 'rows' => array(
4641 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4642 )
4643 ),
4644 'php_info' => array(
4645 'title' => 'PHP Info',
4646 'rows' => array(
4647 'info' => array( $php_info )
4648 ),
4649 )
4650 );
4651
4652 // Allow the sections to be modified by other code.
4653 $sections = $this->apply_filters( 'email_template_sections', $sections );
4654
4655 return $sections;
4656 }
4657
4658 #endregion
4659
4660 #----------------------------------------------------------------------------------
4661 #region Initialization
4662 #----------------------------------------------------------------------------------
4663
4664 /**
4665 * Init plugin's Freemius instance.
4666 *
4667 * @author Vova Feldman (@svovaf)
4668 * @since 1.0.1
4669 *
4670 * @param number $id
4671 * @param string $public_key
4672 * @param bool $is_live
4673 * @param bool $is_premium
4674 */
4675 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4676 $this->_logger->entrance();
4677
4678 $this->dynamic_init( array(
4679 'id' => $id,
4680 'public_key' => $public_key,
4681 'is_live' => $is_live,
4682 'is_premium' => $is_premium,
4683 ) );
4684 }
4685
4686 /**
4687 * Dynamic initiator, originally created to support initiation
4688 * with parent_id for add-ons.
4689 *
4690 * @author Vova Feldman (@svovaf)
4691 * @since 1.0.6
4692 *
4693 * @param array $plugin_info
4694 *
4695 * @throws Freemius_Exception
4696 */
4697 function dynamic_init( array $plugin_info ) {
4698 $this->_logger->entrance();
4699
4700 $this->parse_settings( $plugin_info );
4701
4702 $this->register_after_settings_parse_hooks();
4703
4704 /**
4705 * 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.
4706 *
4707 * @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.
4708 */
4709 if ( $this->is_anonymous() ) {
4710 $is_network_level = ( $this->_is_network_active && fs_is_network_admin() );
4711
4712 if (
4713 ! $is_network_level ||
4714 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
4715 ) {
4716 if ( $this->is_paying_or_trial() ) {
4717 $this->reset_anonymous_mode( $is_network_level );
4718 }
4719 } else {
4720 $network = get_network();
4721
4722 if ( is_object( $network ) ) {
4723 $main_blog_id = $network->site_id;
4724 $first_install = $this->get_install_by_blog_id( $main_blog_id );
4725
4726 if ( is_object( $first_install ) ) {
4727 $this->_storage->network_install_blog_id = $main_blog_id;
4728 $this->_storage->network_user_id = $first_install->user_id;
4729 }
4730 }
4731 }
4732 }
4733
4734 if ( $this->should_stop_execution() ) {
4735 return;
4736 }
4737
4738 if ( ! $this->is_registered() ) {
4739 if ( $this->is_anonymous() ) {
4740 // If user skipped, no need to test connectivity.
4741 $this->_has_api_connection = true;
4742 $this->_is_on = true;
4743 } else {
4744 if ( false === $this->has_api_connectivity() ) {
4745 return;
4746 } else {
4747 if ( $this->_anonymous_mode ) {
4748 // Simulate anonymous mode.
4749 $this->_is_anonymous = true;
4750 }
4751 }
4752 }
4753 }
4754
4755 /**
4756 * This should be executed even if Freemius is off for the core module,
4757 * otherwise, the add-ons dialog box won't work properly. This is especially
4758 * relevant when the developer decided to turn FS off for existing users.
4759 *
4760 * @author Vova Feldman (@svovaf)
4761 */
4762 if ( $this->is_user_in_admin() &&
4763 'plugin-information' === fs_request_get( 'tab', false ) &&
4764 $this->should_use_freemius_updater_and_dialog() &&
4765 (
4766 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4767 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4768 )
4769 ) {
4770 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4771
4772 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4773 }
4774
4775 // Check if Freemius is on for the current plugin.
4776 // This MUST be executed after all the plugin variables has been loaded.
4777 if ( ! $this->is_registered() && ! $this->is_on() ) {
4778 return;
4779 }
4780
4781 if ( $this->has_api_connectivity() ) {
4782 if ( self::is_cron() ) {
4783 $this->hook_callback_to_sync_cron();
4784 } else if ( $this->is_user_in_admin() ) {
4785 /**
4786 * Schedule daily data sync cron if:
4787 *
4788 * 1. User opted-in (for tracking).
4789 * 2. If skipped, but later upgraded (opted-in via upgrade).
4790 *
4791 * @author Vova Feldman (@svovaf)
4792 * @since 1.1.7.3
4793 *
4794 */
4795 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
4796 $this->maybe_schedule_sync_cron();
4797 }
4798
4799 /**
4800 * Check if requested for manual blocking background sync.
4801 */
4802 if ( fs_request_has( 'background_sync' ) ) {
4803 self::require_pluggable_essentials();
4804 self::wp_cookie_constants();
4805
4806 $this->run_manual_sync();
4807 }
4808 }
4809 }
4810
4811 if ( $this->is_registered() ) {
4812 FS_Clone_Manager::instance()->maybe_resolve_new_subsite_install_automatically( $this );
4813
4814 $this->hook_callback_to_install_sync();
4815 }
4816
4817 if ( $this->is_addon() ) {
4818 if ( $this->is_parent_plugin_installed() ) {
4819 // Link to parent FS.
4820 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4821
4822 // Get parent plugin reference.
4823 $this->_parent_plugin = $this->_parent->get_plugin();
4824 }
4825 }
4826
4827 if ( $this->is_user_in_admin() ) {
4828 if ( $this->is_registered() && fs_request_has( 'purchase_completed' ) ) {
4829 $this->_admin_notices->add_sticky(
4830 sprintf(
4831 /* translators: %s: License type (e.g. you have a professional license) */
4832 $this->get_text_inline( 'You have purchased a %s license.', 'you-have-x-license' ),
4833 fs_request_get( 'purchased_plan' )
4834 ) .
4835 sprintf(
4836 $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' ),
4837 $this->get_module_label( true ),
4838 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? "products' " : '' ),
4839 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? 's' : '' ),
4840 sprintf(
4841 '<strong>%s</strong>',
4842 fs_request_get( 'purchase_email' )
4843 )
4844 ),
4845 'plan_purchased',
4846 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
4847 );
4848 }
4849
4850 if ( $this->is_addon() ) {
4851 if ( ! $this->is_parent_plugin_installed() ) {
4852 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4853
4854 if ( isset( $plugin_info['parent'] ) ) {
4855 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4856 }
4857
4858 $this->_admin_notices->add(
4859 ( ! empty( $parent_name ) ?
4860 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 ) :
4861 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() )
4862 ),
4863 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4864 'error'
4865 );
4866
4867 return;
4868 } else {
4869 $is_network_admin = fs_is_network_admin();
4870
4871 if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4872 // If add-on activated and parent not, automatically install parent for the user.
4873 $this->activate_parent_account( $this->_parent );
4874 } else if (
4875 $this->_parent->is_registered() &&
4876 ! $this->is_registered() &&
4877 /**
4878 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
4879 * * Network active and in network admin - network activate add-on account.
4880 * * Network active and not in network admin - activate add-on account for the current blog.
4881 * * Not network active and not in network admin - activate add-on account for the current blog.
4882 *
4883 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
4884 *
4885 * @author Leo Fajardo (@leorw)
4886 * @since 2.3.0
4887 */
4888 ( $this->is_network_active() || ! $is_network_admin )
4889 ) {
4890 $premium_license = null;
4891
4892 if (
4893 ! $this->has_free_plan() &&
4894 $this->is_bundle_license_auto_activation_enabled() &&
4895 $this->_parent->is_activated_with_bundle_license()
4896 ) {
4897 /**
4898 * If the add-on has no free plan, try to activate the account only when there's a bundle license.
4899 *
4900 * @author Leo Fajardo (@leorw)
4901 * @since 2.4.0
4902 */
4903 $bundle_license = $this->get_active_parent_license( $this->_parent->_get_license()->secret_key, false );
4904
4905 if (
4906 is_object( $bundle_license ) &&
4907 ! empty( $bundle_license->products ) &&
4908 in_array( $this->get_id(), $bundle_license->products )
4909 ) {
4910 $premium_license = $bundle_license;
4911 }
4912 }
4913
4914 if ( $this->has_free_plan() || is_object( $premium_license) ) {
4915 // If parent plugin activated, automatically install add-on for the user.
4916 $this->_activate_addon_account(
4917 $this->_parent,
4918 ( $this->is_network_active() && $is_network_admin ) ?
4919 true :
4920 get_current_blog_id(),
4921 $premium_license
4922 );
4923 }
4924 }
4925
4926 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4927 if ( $this->is_premium() ) {
4928 // Remove add-on download admin-notice.
4929 $this->_parent->_admin_notices->remove_sticky( array(
4930 'addon_plan_upgraded_' . $this->_slug,
4931 'no_addon_license_' . $this->_slug,
4932 ) );
4933 }
4934
4935 // $this->deactivate_premium_only_addon_without_license();
4936 }
4937 }
4938
4939 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
4940
4941 // if ( $this->is_registered() ||
4942 // $this->is_anonymous() ||
4943 // $this->is_pending_activation()
4944 // ) {
4945 // $this->_init_admin();
4946 // }
4947 }
4948
4949 /**
4950 * Should be called outside `$this->is_user_in_admin()` scope
4951 * because the updater has some logic that needs to be executed
4952 * during AJAX calls.
4953 *
4954 * Currently, we need to hook to the `http_request_host_is_external` filter.
4955 * In the future, there might be additional logic added.
4956 *
4957 * @author Vova Feldman
4958 * @since 1.2.1.6
4959 */
4960 if (
4961 $this->should_use_freemius_updater_and_dialog() &&
4962 (
4963 $this->is_premium() ||
4964 /**
4965 * If not premium but the premium version is installed, also instantiate the updater so that the
4966 * plugin information dialog of the premium version will have the information from the server.
4967 *
4968 * @author Leo Fajardo (@leorw)
4969 * @since 2.2.3
4970 */
4971 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
4972 ) &&
4973 $this->has_release_on_freemius() &&
4974 ( ! $this->is_unresolved_clone( true ) )
4975 ) {
4976 FS_Plugin_Updater::instance( $this );
4977 }
4978
4979 $this->do_action( 'initiated' );
4980
4981 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
4982 if ( isset( $this->_storage->prev_is_premium ) ) {
4983 $this->apply_filters(
4984 'after_code_type_change',
4985 // New code type.
4986 $this->_plugin->is_premium
4987 );
4988 } else {
4989 // Set for code type for the first time.
4990 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
4991 }
4992 }
4993
4994 if ( ! $this->is_addon() ) {
4995 if ( $this->is_registered() ) {
4996 // Fix for upgrade from versions < 1.0.9.
4997 if ( ! isset( $this->_storage->activation_timestamp ) ) {
4998 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
4999 }
5000
5001 $this->do_action( 'after_init_plugin_registered' );
5002 } else if ( $this->is_anonymous() ) {
5003 $this->do_action( 'after_init_plugin_anonymous' );
5004 } else if ( $this->is_pending_activation() ) {
5005 $this->do_action( 'after_init_plugin_pending_activations' );
5006 }
5007 } else {
5008 if ( $this->is_registered() ) {
5009 $this->do_action( 'after_init_addon_registered' );
5010 } else if ( $this->is_anonymous() ) {
5011 $this->do_action( 'after_init_addon_anonymous' );
5012 } else if ( $this->is_pending_activation() ) {
5013 $this->do_action( 'after_init_addon_pending_activations' );
5014 }
5015 }
5016 }
5017
5018 /**
5019 * @author Leo Fajardo (@leorw)
5020 * @since 2.2.3
5021 *
5022 * @return bool
5023 */
5024 private function should_use_freemius_updater_and_dialog() {
5025 return (
5026 /**
5027 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
5028 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
5029 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
5030 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
5031 * plugin details from .org).
5032 */
5033 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
5034 (
5035 ! self::is_plugin_install_page() &&
5036 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
5037 ( 'install-plugin' !== fs_request_get( 'action' ) )
5038 )
5039 );
5040 }
5041
5042 /**
5043 * @param string[] $permissions
5044 * @param bool $is_enabled
5045 * @param int|null $blog_id
5046 *
5047 * @return true|object `true` on success, API error object on failure.
5048 */
5049 private function update_site_permissions( array $permissions, $is_enabled, $blog_id = null ) {
5050 $this->_logger->entrance();
5051
5052 $params = array(
5053 'permissions' => implode( ',', $permissions ),
5054 'is_enabled' => $is_enabled,
5055 );
5056
5057 $current_blog_id = get_current_blog_id();
5058 $is_blog_switched = false;
5059 if ( is_numeric( $blog_id ) && $current_blog_id != $blog_id ) {
5060 $is_blog_switched = $this->switch_to_blog( $blog_id );
5061 }
5062
5063 $result = $this->api_site_call( '/permissions.json', 'put', $params );
5064
5065 if ( $is_blog_switched ) {
5066 $this->switch_to_blog( $current_blog_id );
5067 }
5068
5069 if (
5070 ! $this->is_api_result_object( $result ) ||
5071 ! isset( $result->install_id )
5072 ) {
5073 $this->_logger->api_error( $result );
5074
5075 return $result;
5076 }
5077
5078 return true;
5079 }
5080
5081 /**
5082 * @param string[] $permissions
5083 * @param bool $is_enabled
5084 * @param bool $has_site_delegated_connection
5085 *
5086 * @return true|object `true` on success, API error object on failure.
5087 */
5088 private function update_network_permissions(
5089 array $permissions,
5090 $is_enabled,
5091 &$has_site_delegated_connection
5092 ) {
5093 $this->_logger->entrance();
5094
5095 $install_id_2_blog_id = array();
5096 $install_by_blog_id = $this->get_blog_install_map();
5097
5098 $has_site_delegated_connection = false;
5099
5100 foreach ( $install_by_blog_id as $blog_id => $install ) {
5101 if ( $this->is_site_delegated_connection( $blog_id ) ) {
5102 // Only update permissions of non-delegated installs.
5103 $has_site_delegated_connection = true;
5104 continue;
5105 }
5106
5107 $install_id_2_blog_id[ $install->id ] = $blog_id;
5108 }
5109
5110 if ( empty( $install_id_2_blog_id ) ) {
5111 return true;
5112 }
5113
5114 $params = array(
5115 'permissions' => implode( ',', $permissions ),
5116 'is_enabled' => $is_enabled,
5117 'install_ids' => implode( ',', array_keys( $install_id_2_blog_id ) ),
5118 );
5119
5120 // Send update to FS.
5121 $result = $this->get_current_or_network_user_api_scope()->call(
5122 "/plugins/{$this->_module_id}/installs/permissions.json",
5123 'put',
5124 $params
5125 );
5126
5127 if ( ! $this->is_api_result_object( $result, 'installs_metadata' ) ) {
5128 $this->_logger->api_error( $result );
5129
5130 return $result;
5131 }
5132
5133 return true;
5134 }
5135
5136 /**
5137 * @param mixed $result
5138 *
5139 * @return string
5140 */
5141 private function get_api_error_message( $result ) {
5142 $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:',
5143 'unexpected-api-error' ), $this->_module_type ) . ' ';
5144
5145 if (
5146 $this->is_api_error( $result ) &&
5147 isset( $result->error )
5148 ) {
5149 $code = empty( $result->error->code ) ? '' : " Code: {$result->error->code}";
5150
5151 $error_message .= "<b>{$result->error->message}{$code}</b>";
5152 } else {
5153 $error_message .= var_export( $result, true );
5154 }
5155
5156 return $error_message;
5157 }
5158
5159 /**
5160 * @author Vova Feldman (@svovaf)
5161 * @since 2.5.1
5162 */
5163 function _toggle_permission_tracking_callback() {
5164 $this->_logger->entrance();
5165
5166 $this->check_ajax_referer( 'toggle_permission_tracking' );
5167
5168 if ( ! $this->is_registered( true ) ) {
5169 self::shoot_ajax_failure( 'User never opted-in.' );
5170 }
5171
5172 $is_enabled = fs_request_get_bool( 'is_enabled' );
5173 $permissions = fs_request_get( 'permissions' );
5174
5175 if ( ! is_string( $permissions ) ) {
5176 self::shoot_ajax_failure( 'The permissions param must be a string.' );
5177 }
5178
5179 $permissions = explode( ',', $permissions );
5180
5181 $result = $this->toggle_permission_tracking( $permissions, $is_enabled );
5182
5183 if ( true !== $result ) {
5184 self::shoot_ajax_failure( $this->get_api_error_message( $result ) );
5185 }
5186
5187 self::shoot_ajax_success();
5188 }
5189
5190 /**
5191 * @param string[] $permissions
5192 * @param bool $is_enabled
5193 * @param int|null $blog_id
5194 *
5195 * @return bool|mixed `true` if updated successfully or no update is needed.
5196 */
5197 private function toggle_permission_tracking( $permissions, $is_enabled, $blog_id = null ) {
5198 if ( ! $this->is_registered( true ) ) {
5199 // User never opted-in.
5200 return false;
5201 }
5202
5203 // Check if permissions are already set as needed.
5204 if ( FS_Permission_Manager::instance( $this )->are_permissions( $permissions, $is_enabled, $blog_id ) ) {
5205 /**
5206 * Note:
5207 * 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.
5208 */
5209 return true;
5210 }
5211
5212 $api_managed_permissions = array_intersect(
5213 $permissions,
5214 FS_Permission_Manager::get_api_managed_permission_ids()
5215 );
5216
5217 if (
5218 in_array( FS_Permission_Manager::PERMISSION_ESSENTIALS, $permissions ) &&
5219 ! in_array( FS_Permission_Manager::PERMISSION_SITE, $permissions )
5220 ) {
5221 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_SITE;
5222 }
5223
5224 if ( ! empty( $api_managed_permissions ) ) {
5225 $has_site_delegated_connection = false;
5226
5227 if (
5228 ! $is_enabled &&
5229 ! in_array( FS_Permission_Manager::PERMISSION_EXTENSIONS, $api_managed_permissions ) &&
5230 false === FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed( $blog_id )
5231 ) {
5232 /**
5233 * 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.
5234 *
5235 * @todo Remove this entire `if` after implementing granular opt-in that also sends the permissions to the API when opting in.
5236 */
5237 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_EXTENSIONS;
5238 }
5239
5240 if ( is_null( $blog_id ) && fs_is_network_admin() ) {
5241 $result = $this->update_network_permissions(
5242 $api_managed_permissions,
5243 $is_enabled,
5244 $has_site_delegated_connection
5245 );
5246 } else {
5247 $result = $this->update_site_permissions(
5248 $api_managed_permissions,
5249 $is_enabled,
5250 $blog_id
5251 );
5252 }
5253
5254 if ( true !== $result ) {
5255 return $result;
5256 }
5257
5258 if ( in_array( FS_Permission_Manager::PERMISSION_SITE, $api_managed_permissions ) ) {
5259 if ( $is_enabled ) {
5260 $this->schedule_sync_cron();
5261 } else {
5262 $this->clear_sync_cron( ! $has_site_delegated_connection );
5263 }
5264 }
5265
5266 if ( in_array( FS_Permission_Manager::PERMISSION_USER, $api_managed_permissions ) ) {
5267 $this->toggle_user_permission( $is_enabled, $blog_id );
5268 }
5269 }
5270
5271 $this->update_tracking_permissions(
5272 $permissions,
5273 $is_enabled,
5274 $blog_id
5275 );
5276
5277 return true;
5278 }
5279
5280 /**
5281 * @param bool $is_enabled
5282 * @param int|null $blog_id
5283 */
5284 private function toggle_user_permission( $is_enabled, $blog_id = null ) {
5285 $network_or_blog_ids = is_numeric( $blog_id ) ?
5286 $blog_id :
5287 fs_is_network_admin();
5288
5289 if ( $is_enabled ) {
5290 $this->reset_anonymous_mode( $network_or_blog_ids );
5291 } else {
5292 $this->skip_connection( $network_or_blog_ids );
5293 }
5294 }
5295
5296 /**
5297 * Opt-in back into usage tracking.
5298 *
5299 * Note: This will only work if the user opted-in previously.
5300 *
5301 * Returns:
5302 * 1. FALSE - If the user never opted-in.
5303 * 2. TRUE - If successfully opted-in back to usage tracking.
5304 * 3. object - API result on failure.
5305 *
5306 * @author Leo Fajardo (@leorw)
5307 * @since 1.2.1.5
5308 *
5309 * @bool $is_enabled
5310 *
5311 * @return bool|object
5312 */
5313 private function toggle_site_tracking( $is_enabled, $blog_id = null ) {
5314 $this->_logger->entrance();
5315
5316 return $this->toggle_permission_tracking(
5317 FS_Permission_Manager::instance( $this )->get_site_tracking_permission_names(),
5318 $is_enabled,
5319 $blog_id
5320 );
5321 }
5322
5323 /**
5324 * If user opted-in and later disabled usage-tracking,
5325 * re-allow tracking for licensing and updates.
5326 *
5327 * @author Leo Fajardo (@leorw)
5328 * @since 1.2.1.5
5329 *
5330 * @param bool $is_context_single_site
5331 */
5332 private function reconnect_locally( $is_context_single_site = false ) {
5333 $this->_logger->entrance();
5334
5335 if ( ! $this->is_registered() ) {
5336 return;
5337 }
5338
5339 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5340 if ( $this->is_tracking_prohibited() ) {
5341 FS_Permission_Manager::instance( $this )->update_site_tracking( true );
5342 }
5343 } else {
5344 $installs_map = $this->get_blog_install_map();
5345 foreach ( $installs_map as $blog_id => $install ) {
5346 /**
5347 * @var FS_Site $install
5348 */
5349 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
5350 FS_Permission_Manager::instance( $this )->update_site_tracking( true, $blog_id );
5351 }
5352 }
5353 }
5354 }
5355
5356 /**
5357 * 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.
5358 *
5359 * @param string[] $permissions
5360 * @param bool $is_enabled
5361 * @param int|null $blog_id
5362 *
5363 * @return array
5364 */
5365 private function update_tracking_permissions( $permissions, $is_enabled, $blog_id = null ) {
5366 // Alias.
5367 $permission_manager = FS_Permission_Manager::instance( $this );
5368
5369 $network_or_blog_ids = is_numeric( $blog_id ) ?
5370 $blog_id :
5371 fs_is_network_admin();
5372
5373 if ( true === $network_or_blog_ids ) {
5374 // Update the permission for all non-delegated sub-sites.
5375 $blog_ids = $this->get_non_delegated_blog_ids();
5376
5377 // Add the network-level to the array, to update the permission on the network-level storage.
5378 array_unshift( $blog_ids, null );
5379 }
5380 else
5381 {
5382 if ( false === $network_or_blog_ids ) {
5383 $network_or_blog_ids = null;
5384 }
5385
5386 $blog_ids = is_array( $network_or_blog_ids ) ?
5387 $network_or_blog_ids :
5388 array( $network_or_blog_ids );
5389 }
5390
5391 $result = array();
5392 foreach ( $permissions as $permission ) {
5393 $permission = trim( $permission );
5394 $is_permission_supported = true;
5395
5396 foreach ( $blog_ids as $id ) {
5397 $is_permission_supported = $permission_manager->update_permission_tracking_flag(
5398 $permission,
5399 $is_enabled,
5400 $id
5401 );
5402 }
5403
5404 if ( ! $is_permission_supported ) {
5405 $permission = 'no_match';
5406 }
5407
5408 $result[ $permission ] = $is_enabled;
5409 }
5410
5411 return $result;
5412 }
5413
5414 /**
5415 * Parse plugin's settings (as defined by the plugin dev).
5416 *
5417 * @author Vova Feldman (@svovaf)
5418 * @since 1.1.7.3
5419 *
5420 * @param array $plugin_info
5421 *
5422 * @throws \Freemius_Exception
5423 */
5424 private function parse_settings( &$plugin_info ) {
5425 $this->_logger->entrance();
5426
5427 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5428 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5429 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5430 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5431 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5432
5433 /**
5434 * @author Vova Feldman (@svovaf)
5435 * @since 1.1.9 Try to pull secret key from external config.
5436 */
5437 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5438 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5439 }
5440
5441 if ( isset( $plugin_info['parent'] ) ) {
5442 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5443 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5444 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5445 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5446 }
5447
5448 if ( false === $id ) {
5449 throw new Freemius_Exception( array(
5450 'error' => array(
5451 'type' => 'ParameterNotSet',
5452 'message' => 'Plugin id parameter is not set.',
5453 'code' => 'plugin_id_not_set',
5454 'http' => 500,
5455 )
5456 ) );
5457 }
5458 if ( false === $public_key ) {
5459 throw new Freemius_Exception( array(
5460 'error' => array(
5461 'type' => 'ParameterNotSet',
5462 'message' => 'Plugin public_key parameter is not set.',
5463 'code' => 'plugin_public_key_not_set',
5464 'http' => 500,
5465 )
5466 ) );
5467 }
5468
5469 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5470 $this->_plugin :
5471 new FS_Plugin();
5472
5473 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5474
5475 $plugin->update( array(
5476 'id' => $id,
5477 'type' => $this->get_option( $plugin_info, 'type', $this->_module_type ),
5478 'public_key' => $public_key,
5479 'slug' => $this->_slug,
5480 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5481 'parent_plugin_id' => $parent_id,
5482 'version' => $this->get_plugin_version(),
5483 'title' => $this->get_plugin_name( $premium_suffix ),
5484 'file' => $this->_plugin_basename,
5485 'is_premium' => $this->get_bool_option( $plugin_info, 'is_premium', true ),
5486 'premium_suffix' => $premium_suffix,
5487 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5488 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5489 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5490 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ),
5491 'opt_in_moderation' => $this->get_option(
5492 $plugin_info,
5493 'opt_in',
5494 // For backward compatibility, we support both parameter names: opt_in and opt_in_moderation.
5495 $this->get_option( $plugin_info, 'opt_in_moderation', null )
5496 ),
5497 ) );
5498
5499 if ( $plugin->is_updated() ) {
5500 // Update plugin details.
5501 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5502 }
5503 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5504 $this->_plugin->secret_key = $secret_key;
5505
5506 /**
5507 * 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).
5508 *
5509 * @author Vova Feldman
5510 * @since 2.4.5
5511 */
5512 if ( $this->is_network_active() && fs_is_network_admin() ) {
5513 if ( isset( $plugin_info['menu_network'] ) &&
5514 is_array( $plugin_info['menu_network'] ) &&
5515 ! empty( $plugin_info['menu_network'] )
5516 ) {
5517 $plugin_info['menu'] = $plugin_info['menu_network'];
5518 }
5519 }
5520
5521 if ( ! isset( $plugin_info['menu'] ) ) {
5522 $plugin_info['menu'] = array();
5523
5524 if ( ! empty( $this->_storage->sdk_last_version ) &&
5525 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5526 ) {
5527 // Backward compatibility to 1.1.2
5528 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5529 $plugin_info['menu_slug'] :
5530 $this->_slug;
5531 }
5532 }
5533
5534 $this->_menu = FS_Admin_Menu_Manager::instance(
5535 $this->_module_id,
5536 $this->_module_type,
5537 $this->get_unique_affix()
5538 );
5539
5540 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5541
5542 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5543 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5544 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5545 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5546 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5547 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5548 if ( $this->_is_premium_only ) {
5549 // If premium only plugin, disable anonymous mode.
5550 $this->_enable_anonymous = false;
5551 $this->_anonymous_mode = false;
5552 } else {
5553 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5554 $this->_anonymous_mode = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
5555 }
5556 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5557 $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
5558
5559 if ( ! empty( $plugin_info['trial'] ) ) {
5560 $this->_trial_days = $this->get_numeric_option(
5561 $plugin_info['trial'],
5562 'days',
5563 // Default to 0 - trial without days specification.
5564 0
5565 );
5566
5567 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5568 }
5569
5570 $this->_navigation = $this->get_option(
5571 $plugin_info,
5572 'navigation',
5573 $this->is_free_wp_org_theme() ?
5574 self::NAVIGATION_TABS :
5575 self::NAVIGATION_MENU
5576 );
5577 }
5578
5579 /**
5580 * @param string[] $options
5581 * @param string $key
5582 * @param mixed $default
5583 *
5584 * @return bool
5585 */
5586 private function get_option( &$options, $key, $default = false ) {
5587 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5588 }
5589
5590 private function get_bool_option( &$options, $key, $default = false ) {
5591 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5592 }
5593
5594 private function get_numeric_option( &$options, $key, $default = false ) {
5595 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5596 }
5597
5598 /**
5599 * Gate keeper.
5600 *
5601 * @author Vova Feldman (@svovaf)
5602 * @since 1.1.7.3
5603 *
5604 * @return bool
5605 */
5606 private function should_stop_execution() {
5607 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5608 /**
5609 * Don't execute Freemius until plugin was fully loaded at least once,
5610 * to give the opportunity for the activation hook to run before pinging
5611 * the API for connectivity test. This logic is relevant for the
5612 * identification of new plugin install vs. plugin update.
5613 *
5614 * @author Vova Feldman (@svovaf)
5615 * @since 1.1.9
5616 */
5617 return true;
5618 }
5619
5620 if ( $this->is_activation_mode() ) {
5621 if ( ! is_admin() ) {
5622 /**
5623 * If in activation mode, don't execute Freemius outside the admin dashboard.
5624 *
5625 * @author Vova Feldman (@svovaf)
5626 * @since 1.1.7.3
5627 */
5628 return true;
5629 }
5630
5631 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5632 /**
5633 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5634 * then don't start Freemius.
5635 *
5636 * @author Vova Feldman (@svovaf)
5637 * @since 1.1.6.3
5638 *
5639 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5640 */
5641 return true;
5642 }
5643
5644 if ( self::is_cron() ) {
5645 /**
5646 * If in activation mode, don't execute Freemius during wp crons
5647 * (wp crons have HTTP context - called as HTTP request).
5648 *
5649 * @author Vova Feldman (@svovaf)
5650 * @since 1.1.7.3
5651 */
5652 return true;
5653 }
5654
5655 if ( self::is_ajax() ) {
5656 /**
5657 * During activation, if running in AJAX mode, unless there's a sticky
5658 * connectivity issue notice, don't run Freemius.
5659 *
5660 * @author Vova Feldman (@svovaf)
5661 * @since 1.1.7.3
5662 */
5663 return true;
5664 }
5665 }
5666
5667 return false;
5668 }
5669
5670 /**
5671 * Triggered after code type has changed.
5672 *
5673 * @author Vova Feldman (@svovaf)
5674 * @since 1.1.9.1
5675 */
5676 function _after_code_type_change() {
5677 $this->_logger->entrance();
5678
5679 if ( $this->is_theme() ) {
5680 // Expire the cache of the previous tabs since the theme may
5681 // have setting updates after code type has changed.
5682 $this->_cache->expire( 'tabs' );
5683 $this->_cache->expire( 'tabs_stylesheets' );
5684 }
5685
5686 if ( ! $this->is_addon() ) {
5687 add_action(
5688 is_admin() ? 'admin_init' : 'init',
5689 array( &$this, '_plugin_code_type_changed' )
5690 );
5691 }
5692
5693 if ( $this->is_registered() && $this->is_premium() ) {
5694 // Purge cached payments after switching to the premium version.
5695 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5696 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5697 }
5698 }
5699
5700 /**
5701 * Handles plugin's code type change (free <--> premium).
5702 *
5703 * @author Vova Feldman (@svovaf)
5704 * @since 1.0.9
5705 */
5706 function _plugin_code_type_changed() {
5707 $this->_logger->entrance();
5708
5709 if ( $this->is_premium() ) {
5710 $this->reconnect_locally();
5711
5712 // Activated premium code.
5713 $this->do_action( 'after_premium_version_activation' );
5714
5715 // Remove all sticky messages related to download of the premium version.
5716 $this->_admin_notices->remove_sticky( array(
5717 'trial_started',
5718 'plan_upgraded',
5719 'plan_changed',
5720 'license_activated',
5721 ) );
5722
5723 $notice = '';
5724 if ( ! $this->is_only_premium() ) {
5725 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5726 }
5727
5728 $license_notice = $this->get_license_network_activation_notice();
5729 if ( ! empty( $license_notice ) ) {
5730 $notice .= ' ' . $license_notice;
5731 }
5732
5733 if ( ! empty( $notice ) ) {
5734 $this->_admin_notices->add_sticky(
5735 trim( $notice ),
5736 'premium_activated',
5737 $this->get_text_x_inline( 'W00t',
5738 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5739 );
5740 }
5741 } else {
5742 // Remove sticky message related to premium code activation.
5743 $this->_admin_notices->remove_sticky( 'premium_activated' );
5744
5745 // Activated free code (after had the premium before).
5746 $this->do_action( 'after_free_version_reactivation' );
5747
5748 if ( $this->is_paying() && ! $this->is_premium() ) {
5749 $this->add_complete_upgrade_instructions_notice(
5750 sprintf(
5751 /* translators: %s: License type (e.g. you have a professional license) */
5752 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5753 $this->get_plan_title()
5754 ),
5755 'plan_upgraded'
5756 );
5757 }
5758 }
5759
5760 if ( $this->is_registered() ) {
5761 // Schedule code type changes event.
5762 $this->schedule_install_sync();
5763 }
5764
5765 /**
5766 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5767 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5768 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5769 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5770 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5771 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5772 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5773 */
5774 $this->unregister_uninstall_hook();
5775
5776 $this->clear_module_main_file_cache();
5777
5778 // Update is_premium of latest version.
5779 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5780 }
5781
5782 #endregion
5783
5784 #----------------------------------------------------------------------------------
5785 #region Add-ons
5786 #----------------------------------------------------------------------------------
5787
5788 /**
5789 * Check if add-on installed and activated on site.
5790 *
5791 * @author Vova Feldman (@svovaf)
5792 * @since 1.0.6
5793 *
5794 * @param string|number $id_or_slug
5795 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5796 *
5797 * @return bool
5798 */
5799 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5800 $this->_logger->entrance();
5801
5802 $addon_id = self::get_module_id( $id_or_slug );
5803 $is_activated = self::has_instance( $addon_id );
5804
5805 if ( ! $is_activated ) {
5806 return false;
5807 }
5808
5809 if ( is_bool( $is_premium ) ) {
5810 // Check if the specified code version is activate.
5811 $addon = $this->get_addon_instance( $addon_id );
5812 $is_activated = ( $is_premium === $addon->is_premium() );
5813 }
5814
5815 return $is_activated;
5816 }
5817
5818 /**
5819 * Check if add-on was connected to install
5820 *
5821 * @author Vova Feldman (@svovaf)
5822 * @since 1.1.7
5823 *
5824 * @param string|number $id_or_slug
5825 *
5826 * @return bool
5827 */
5828 function is_addon_connected( $id_or_slug ) {
5829 $this->_logger->entrance();
5830
5831 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5832
5833 $addon_id = self::get_module_id( $id_or_slug );
5834 $addon = $this->get_addon( $addon_id );
5835 $slug = $addon->slug;
5836 if ( ! isset( $sites[ $slug ] ) ) {
5837 return false;
5838 }
5839
5840 $site = $sites[ $slug ];
5841
5842 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5843
5844 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5845 // The given slug do NOT belong to any of the plugin's add-ons.
5846 return false;
5847 }
5848
5849 return ( is_object( $site ) &&
5850 is_numeric( $site->id ) &&
5851 is_numeric( $site->user_id ) &&
5852 FS_Plugin_Plan::is_valid_id( $site->plan_id )
5853 );
5854 }
5855
5856 /**
5857 * Determines if add-on installed.
5858 *
5859 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
5860 *
5861 * @author Vova Feldman (@svovaf)
5862 * @since 1.0.6
5863 *
5864 * @param string|number $id_or_slug
5865 *
5866 * @return bool
5867 */
5868 function is_addon_installed( $id_or_slug ) {
5869 $this->_logger->entrance();
5870
5871 $addon_id = self::get_module_id( $id_or_slug );
5872
5873 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
5874 }
5875
5876 /**
5877 * Get add-on basename.
5878 *
5879 * @author Vova Feldman (@svovaf)
5880 * @since 1.0.6
5881 *
5882 * @param string|number $id_or_slug
5883 *
5884 * @return string
5885 */
5886 function get_addon_basename( $id_or_slug ) {
5887 $addon_id = self::get_module_id( $id_or_slug );
5888
5889 if ( $this->is_addon_activated( $addon_id ) ) {
5890 return self::instance( $addon_id )->get_plugin_basename();
5891 }
5892
5893 $addon = $this->get_addon( $addon_id );
5894 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
5895
5896 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
5897 return $premium_basename;
5898 }
5899
5900 $all_plugins = $this->get_all_plugins();
5901
5902 foreach ( $all_plugins as $basename => $data ) {
5903 if ( $addon->slug === $data['slug'] ||
5904 $addon->premium_slug === $data['slug']
5905 ) {
5906 return $basename;
5907 }
5908 }
5909
5910 $free_basename = "{$addon->slug}/{$addon->slug}.php";
5911
5912 return $free_basename;
5913 }
5914
5915 /**
5916 * Get installed add-ons instances.
5917 *
5918 * @author Vova Feldman (@svovaf)
5919 * @since 1.0.6
5920 *
5921 * @return Freemius[]
5922 */
5923 function get_installed_addons() {
5924 if ( $this->is_addon() ) {
5925 // Add-on cannot have add-ons.
5926 return array();
5927 }
5928
5929 $installed_addons = array();
5930
5931 foreach ( self::$_instances as $instance ) {
5932 if ( $instance->is_addon_of( $this->_plugin->id ) ) {
5933 $installed_addons[] = $instance;
5934 }
5935 }
5936
5937 return $installed_addons;
5938 }
5939
5940 /**
5941 * Check if any add-ons of the plugin are installed.
5942 *
5943 * @author Leo Fajardo (@leorw)
5944 * @since 1.1.1
5945 *
5946 * @return bool
5947 */
5948 function has_installed_addons() {
5949 if ( ! $this->has_addons() ) {
5950 return false;
5951 }
5952
5953 foreach ( self::$_instances as $instance ) {
5954 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
5955 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
5956 return true;
5957 }
5958 }
5959 }
5960
5961 return false;
5962 }
5963
5964 /**
5965 * Tell Freemius that the current plugin is an add-on.
5966 *
5967 * @author Vova Feldman (@svovaf)
5968 * @since 1.0.6
5969 *
5970 * @param number $parent_plugin_id The parent plugin ID
5971 */
5972 function init_addon( $parent_plugin_id ) {
5973 $this->_plugin->parent_plugin_id = $parent_plugin_id;
5974 }
5975
5976 /**
5977 * @author Vova Feldman (@svovaf)
5978 * @since 1.0.6
5979 *
5980 * @return bool
5981 */
5982 function is_addon() {
5983 return (
5984 isset( $this->_plugin->parent_plugin_id ) &&
5985 is_numeric( $this->_plugin->parent_plugin_id )
5986 );
5987 }
5988
5989 /**
5990 * @author Vova Feldman (@svovaf)
5991 * @since 2.3.2
5992 *
5993 * @param number $parent_product_id
5994 *
5995 * @return bool
5996 */
5997 function is_addon_of( $parent_product_id ) {
5998 return (
5999 $this->is_addon() &&
6000 $parent_product_id == $this->_plugin->parent_plugin_id
6001 );
6002 }
6003
6004 /**
6005 * Deactivate add-on if it's premium only and the user does't have a valid license.
6006 *
6007 * @param bool $is_after_trial_cancel
6008 *
6009 * @return bool If add-on was deactivated.
6010 */
6011 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
6012 if ( ! $this->has_free_plan() &&
6013 ! $this->has_features_enabled_license() &&
6014 ! $this->_has_premium_license()
6015 ) {
6016 if ( $this->is_registered() ) {
6017 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
6018 // if (empty($this->_storage->activation_timestamp) ||
6019 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
6020 // ) {
6021 /**
6022 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
6023 *
6024 * Retry syncing the user add-on licenses.
6025 */
6026 // Sync licenses.
6027 $this->_sync_licenses();
6028 // }
6029
6030 // Try to activate premium license.
6031 $this->_activate_license( true );
6032 }
6033
6034 if ( ! $this->has_free_plan() &&
6035 ! $this->has_features_enabled_license() &&
6036 ! $this->_has_premium_license()
6037 ) {
6038 // @todo Check if deactivate plugins also call the deactivation hook.
6039
6040 $this->_parent->_admin_notices->add_sticky(
6041 sprintf(
6042 ( $is_after_trial_cancel ?
6043 $this->_parent->get_text_inline(
6044 '%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.',
6045 'addon-trial-cancelled-message'
6046 ) :
6047 $this->_parent->get_text_inline(
6048 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
6049 'addon-no-license-message'
6050 )
6051 ),
6052 '<b>' . $this->_plugin->title . '</b>'
6053 ) . ' ' . sprintf(
6054 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
6055 $this->_parent->addon_url( $this->_slug ),
6056 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
6057 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
6058 ),
6059 'no_addon_license_' . $this->_slug,
6060 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
6061 ( $is_after_trial_cancel ? 'success' : 'error' )
6062 );
6063
6064 deactivate_plugins( array( $this->_plugin_basename ), true );
6065
6066 return true;
6067 }
6068 }
6069
6070 return false;
6071 }
6072
6073 #endregion
6074
6075 #----------------------------------------------------------------------------------
6076 #region Sandbox
6077 #----------------------------------------------------------------------------------
6078
6079 /**
6080 * Set Freemius into sandbox mode for debugging.
6081 *
6082 * @author Vova Feldman (@svovaf)
6083 * @since 1.0.4
6084 *
6085 * @param string $secret_key
6086 */
6087 function init_sandbox( $secret_key ) {
6088 $this->_plugin->secret_key = $secret_key;
6089
6090 // Update plugin details.
6091 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
6092 }
6093
6094 /**
6095 * Check if running payments in sandbox mode.
6096 *
6097 * @author Vova Feldman (@svovaf)
6098 * @since 1.0.4
6099 *
6100 * @return bool
6101 */
6102 function is_payments_sandbox() {
6103 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
6104 }
6105
6106 #endregion
6107
6108 /**
6109 * Check if running test vs. live plugin.
6110 *
6111 * @author Vova Feldman (@svovaf)
6112 * @since 1.0.5
6113 *
6114 * @return bool
6115 */
6116 function is_live() {
6117 return $this->_plugin->is_live;
6118 }
6119
6120 /**
6121 * Check if super-admin skipped connection for all sites in the network.
6122 *
6123 * @author Vova Feldman (@svovaf)
6124 * @since 2.0.0
6125 */
6126 function is_network_anonymous() {
6127 if ( ! $this->_is_network_active ) {
6128 return false;
6129 }
6130
6131 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
6132
6133 if ( empty( $is_anonymous_ms ) ) {
6134 return false;
6135 }
6136
6137 return $is_anonymous_ms['is'];
6138 }
6139
6140 /**
6141 * Check if super-admin opted-in for all sites in the network.
6142 *
6143 * @author Vova Feldman (@svovaf)
6144 * @since 2.0.0
6145 */
6146 function is_network_connected() {
6147 if ( ! $this->_is_network_active ) {
6148 return false;
6149 }
6150
6151 return $this->_storage->get( 'is_network_connected' );
6152 }
6153
6154 /**
6155 * Check if the user skipped connecting the account with Freemius.
6156 *
6157 * @author Vova Feldman (@svovaf)
6158 * @since 1.0.7
6159 *
6160 * @return bool
6161 */
6162 function is_anonymous() {
6163 if ( ! isset( $this->_is_anonymous ) ) {
6164 if ( $this->is_network_anonymous() ) {
6165 $this->_is_anonymous = true;
6166 } else if ( fs_is_network_admin() ) {
6167 /**
6168 * When not-network-anonymous, yet, running in the network admin, consider as anonymous only when ALL non-delegated sites are set to anonymous.
6169 */
6170 $non_delegated_sites = $this->get_non_delegated_blog_ids();
6171
6172 foreach ( $non_delegated_sites as $blog_id ) {
6173 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6174
6175 if ( empty( $is_anonymous ) || false === $is_anonymous[ 'is' ] ) {
6176 $this->_is_anonymous = false;
6177 break;
6178 }
6179 }
6180
6181 if ( false !== $this->_is_anonymous ) {
6182 $this->_is_anonymous = true;
6183 }
6184 } else {
6185 if ( ! isset( $this->_storage->is_anonymous ) ) {
6186 // Not skipped.
6187 $this->_is_anonymous = false;
6188 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
6189 // For back compatibility, since the variable was boolean before.
6190 $this->_is_anonymous = $this->_storage->is_anonymous;
6191
6192 // Upgrade stored data format to 1.1.3 format.
6193 $this->set_anonymous_mode( $this->_storage->is_anonymous );
6194 } else {
6195 // Version 1.1.3 and later.
6196 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
6197 }
6198 }
6199 }
6200
6201 return $this->_is_anonymous;
6202 }
6203
6204 /**
6205 * Check if the user skipped the connection of a specified site.
6206 *
6207 * @author Vova Feldman (@svovaf)
6208 * @since 2.0.0
6209 *
6210 * @param int $blog_id
6211 *
6212 * @return bool
6213 */
6214 function is_anonymous_site( $blog_id = 0 ) {
6215 if ( $this->is_network_anonymous() ) {
6216 return true;
6217 }
6218
6219 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6220
6221 if ( empty( $is_anonymous ) ) {
6222 return false;
6223 }
6224
6225 return $is_anonymous['is'];
6226 }
6227
6228 /**
6229 * Check if user connected his account and install pending email activation.
6230 *
6231 * @author Vova Feldman (@svovaf)
6232 * @since 1.0.7
6233 *
6234 * @return bool
6235 */
6236 function is_pending_activation() {
6237 return $this->_storage->get( 'is_pending_activation', false );
6238 }
6239
6240 /**
6241 * @author Leo Fajardo (@leorw)
6242 * @since 2.5.0
6243 */
6244 private function clear_pending_activation_mode() {
6245 // Remove the pending activation sticky notice (if it still exists).
6246 $this->_admin_notices->remove_sticky( 'activation_pending' );
6247
6248 // Clear the plugin's pending activation mode.
6249 unset( $this->_storage->is_pending_activation );
6250 }
6251
6252 /**
6253 * Check if plugin must be WordPress.org compliant.
6254 *
6255 * @since 1.0.7
6256 *
6257 * @return bool
6258 */
6259 function is_org_repo_compliant() {
6260 return $this->_is_org_compliant;
6261 }
6262
6263 #--------------------------------------------------------------------------------
6264 #region WP Cron Common
6265 #--------------------------------------------------------------------------------
6266
6267 /**
6268 * @author Vova Feldman (@svovaf)
6269 * @since 2.0.0
6270 *
6271 * @param string $name Cron name.
6272 *
6273 * @return object
6274 */
6275 private function get_cron_data( $name ) {
6276 $this->_logger->entrance( $name );
6277
6278 /**
6279 * @var object $cron_data
6280 */
6281 return $this->_storage->get( "{$name}_cron", null );
6282 }
6283
6284 /**
6285 * @author Vova Feldman (@svovaf)
6286 * @since 2.0.0
6287 *
6288 * @param string $name Cron name.
6289 */
6290 private function clear_cron_data( $name ) {
6291 $this->_logger->entrance( $name );
6292
6293 $this->_storage->remove( "{$name}_cron" );
6294 }
6295
6296 /**
6297 * @author Vova Feldman (@svovaf)
6298 * @since 2.0.0
6299 *
6300 * @param string $name Cron name.
6301 * @param int $cron_blog_id The cron executing blog ID.
6302 */
6303 private function set_cron_data( $name, $cron_blog_id = 0 ) {
6304 $this->_logger->entrance( $name );
6305
6306 $this->_storage->store( "{$name}_cron", (object) array(
6307 'version' => $this->get_plugin_version(),
6308 'blog_id' => $cron_blog_id,
6309 'sdk_version' => $this->version,
6310 'timestamp' => WP_FS__SCRIPT_START_TIME,
6311 'on' => true,
6312 ) );
6313 }
6314
6315 /**
6316 * Get the cron's executing blog ID.
6317 *
6318 * @author Vova Feldman (@svovaf)
6319 * @since 2.0.0
6320 *
6321 * @param string $name Cron name.
6322 *
6323 * @return int
6324 */
6325 private function get_cron_blog_id( $name ) {
6326 $this->_logger->entrance( $name );
6327
6328 if ( ! is_multisite() ) {
6329 // Not a multisite.
6330 return 0;
6331 }
6332
6333 $cron_data = $this->get_cron_data( $name );
6334
6335 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6336 $cron_data->blog_id :
6337 0;
6338 }
6339
6340 /**
6341 * @author Vova Feldman (@svovaf)
6342 * @since 2.0.0
6343 *
6344 * @param string $name Cron name.
6345 *
6346 * @return bool
6347 */
6348 private function is_cron_on( $name ) {
6349 $this->_logger->entrance( $name );
6350
6351 /**
6352 * @var object $cron_data
6353 */
6354 $cron_data = $this->get_cron_data( $name );
6355
6356 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6357 }
6358
6359 /**
6360 * Unix timestamp for previous cron execution or false if never executed.
6361 *
6362 * @author Vova Feldman (@svovaf)
6363 * @since 2.0.0
6364 *
6365 * @param string $name Cron name.
6366 *
6367 * @return int|false
6368 */
6369 private function cron_last_execution( $name ) {
6370 $this->_logger->entrance( $name );
6371
6372 return $this->_storage->get( "{$name}_timestamp" );
6373 }
6374
6375 /**
6376 * Set cron execution time to now.
6377 *
6378 * @author Vova Feldman (@svovaf)
6379 * @since 2.0.0
6380 *
6381 * @param string $name Cron name.
6382 */
6383 private function set_cron_execution_timestamp( $name ) {
6384 $this->_logger->entrance( $name );
6385
6386 $this->_storage->store( "{$name}_timestamp", time() );
6387 }
6388
6389 /**
6390 * Sets the keepalive time to now.
6391 *
6392 * @author Leo Fajardo (@leorw)
6393 * @since 2.2.3
6394 *
6395 * @param bool|null $use_network_level_storage
6396 */
6397 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6398 $this->_logger->entrance();
6399
6400 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6401 }
6402
6403 /**
6404 * Check if cron was executed in the last $period of seconds.
6405 *
6406 * @author Vova Feldman (@svovaf)
6407 * @since 2.0.0
6408 *
6409 * @param string $name Cron name.
6410 * @param int $period In seconds
6411 *
6412 * @return bool
6413 */
6414 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6415 $this->_logger->entrance( $name );
6416
6417 $last_execution = $this->cron_last_execution( $name );
6418
6419 if ( ! is_numeric( $last_execution ) ) {
6420 return false;
6421 }
6422
6423 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6424 }
6425
6426 /**
6427 * WP Cron is executed on a site level. When running in a multisite network environment
6428 * with the network integration activated, for optimization reasons, we are consolidating
6429 * the installs data sync cron to be executed only from a single site.
6430 *
6431 * @author Vova Feldman (@svovaf)
6432 * @since 2.0.0
6433 *
6434 * @param int $except_blog_id Target any except the excluded blog ID.
6435 *
6436 * @return int
6437 */
6438 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6439 if ( ! is_multisite() ) {
6440 return 0;
6441 }
6442
6443 if ( $this->_is_network_active ) {
6444 $network_install_blog_id = $this->_storage->network_install_blog_id;
6445
6446 if (
6447 is_numeric( $network_install_blog_id ) &&
6448 $except_blog_id != $network_install_blog_id &&
6449 self::is_site_active( $network_install_blog_id )
6450 ) {
6451 // Try to run cron from the main network blog.
6452 $install = $this->get_install_by_blog_id( $network_install_blog_id );
6453
6454 if (
6455 is_object( $install ) &&
6456 $this->is_tracking_allowed( $network_install_blog_id, $install )
6457 ) {
6458 return $network_install_blog_id;
6459 }
6460 }
6461 }
6462
6463 // Get first opted-in blog ID with active tracking.
6464 $installs = $this->get_blog_install_map();
6465 foreach ( $installs as $blog_id => $install ) {
6466 if ( $except_blog_id != $blog_id &&
6467 self::is_site_active( $blog_id ) &&
6468 $this->is_tracking_allowed( $blog_id, $install )
6469 ) {
6470 return $blog_id;
6471 }
6472 }
6473
6474 return 0;
6475 }
6476
6477 /**
6478 * @author Vova Feldman (@svovaf)
6479 * @since 2.0.0
6480 *
6481 * @param string $name Cron name.
6482 * @param string $action_tag Callback action tag.
6483 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6484 */
6485 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6486 $this->_logger->entrance( $name );
6487
6488 if ( ! $this->is_cron_on( $name ) ) {
6489 return;
6490 }
6491
6492 $clear_cron = true;
6493 if ( ! $is_network_clear && $this->_is_network_active ) {
6494 $installs = $this->get_blog_install_map();
6495
6496 foreach ( $installs as $blog_id => $install ) {
6497 /**
6498 * @var FS_Site $install
6499 */
6500 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6501 $clear_cron = false;
6502 break;
6503 }
6504 }
6505 }
6506
6507 if ( ! $clear_cron ) {
6508 return;
6509 }
6510
6511 $cron_blog_id = $this->get_cron_blog_id( $name );
6512
6513 $this->clear_cron_data( $name );
6514
6515 if ( 0 < $cron_blog_id ) {
6516 switch_to_blog( $cron_blog_id );
6517 }
6518
6519 if ( empty( $action_tag ) ) {
6520 $action_tag = $name;
6521 }
6522
6523 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6524
6525 if ( 0 < $cron_blog_id ) {
6526 restore_current_blog();
6527 }
6528 }
6529
6530 /**
6531 * Unix timestamp for next cron execution or false if not scheduled.
6532 *
6533 * @author Vova Feldman (@svovaf)
6534 * @since 2.0.0
6535 *
6536 * @param string $name Cron name.
6537 * @param string $action_tag Callback action tag.
6538 *
6539 * @return int|false
6540 */
6541 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6542 $this->_logger->entrance( $name );
6543
6544 if ( ! $this->is_cron_on( $name ) ) {
6545 return false;
6546 }
6547
6548 $cron_blog_id = $this->get_cron_blog_id( $name );
6549
6550 if ( 0 < $cron_blog_id ) {
6551 switch_to_blog( $cron_blog_id );
6552 }
6553
6554 if ( empty( $action_tag ) ) {
6555 $action_tag = $name;
6556 }
6557
6558 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6559
6560 if ( 0 < $cron_blog_id ) {
6561 restore_current_blog();
6562 }
6563
6564 return $next_scheduled;
6565 }
6566
6567 /**
6568 * @author Vova Feldman (@svovaf)
6569 * @since 2.0.0
6570 *
6571 * @param string $name Cron name.
6572 * @param string $action_tag Callback action tag.
6573 * @param string $recurrence 'single' or 'daily'.
6574 * @param int $start_at Defaults to now.
6575 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6576 * @param int $except_blog_id Target any except the excluded blog ID.
6577 */
6578 private function schedule_cron(
6579 $name,
6580 $action_tag = '',
6581 $recurrence = 'single',
6582 $start_at = WP_FS__SCRIPT_START_TIME,
6583 $randomize_start = true,
6584 $except_blog_id = 0
6585 ) {
6586 $this->_logger->entrance( $name );
6587
6588 $this->clear_cron( $name, $action_tag, true );
6589
6590 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6591
6592 if ( is_multisite() && 0 == $cron_blog_id ) {
6593 // Don't schedule cron since couldn't find a target blog.
6594 return;
6595 }
6596
6597 if ( 0 < $cron_blog_id ) {
6598 switch_to_blog( $cron_blog_id );
6599 }
6600
6601 if ( 'daily' === $recurrence ) {
6602 if ( $randomize_start ) {
6603 // Schedule first sync with a random 12 hour time range from now.
6604 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6605 }
6606
6607 // Schedule daily WP cron.
6608 wp_schedule_event(
6609 $start_at,
6610 'daily',
6611 $this->get_action_tag( $action_tag )
6612 );
6613 } else if ( 'single' === $recurrence ) {
6614 // Schedule single cron.
6615 wp_schedule_single_event(
6616 $start_at,
6617 $this->get_action_tag( $action_tag )
6618 );
6619 }
6620
6621 $this->set_cron_data( $name, $cron_blog_id );
6622
6623 if ( 0 < $cron_blog_id ) {
6624 restore_current_blog();
6625 }
6626 }
6627
6628 /**
6629 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6630 * that doesn't halt page loading.
6631 *
6632 * @author Vova Feldman (@svovaf)
6633 * @since 2.0.0
6634 *
6635 * @param string $name Cron name.
6636 * @param callable $callable The function that should be executed.
6637 */
6638 private function execute_cron( $name, $callable ) {
6639 $this->_logger->entrance( $name );
6640
6641 // Store the last time data sync was executed.
6642 $this->set_cron_execution_timestamp( $name );
6643
6644 // Check if API is temporary down.
6645 if ( FS_Api::is_temporary_down() ) {
6646 return;
6647 }
6648
6649 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6650
6651 $users_2_blog_ids = array();
6652
6653 if ( ! is_multisite() ) {
6654 // Add dummy blog.
6655 $users_2_blog_ids[0] = array( 0 );
6656 } else {
6657 $installs = $this->get_blog_install_map();
6658 foreach ( $installs as $blog_id => $install ) {
6659 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6660 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6661 $users_2_blog_ids[ $install->user_id ] = array();
6662 }
6663
6664 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6665 }
6666 }
6667 }
6668
6669 $current_blog_id = get_current_blog_id();
6670
6671 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6672 if ( 0 < $blog_ids[0] ) {
6673 $this->switch_to_blog( $blog_ids[0] );
6674 }
6675
6676 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6677
6678 foreach ( $blog_ids as $blog_id ) {
6679 $this->do_action( "after_{$name}_cron", $blog_id );
6680 }
6681 }
6682
6683 if ( is_multisite() ) {
6684 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6685
6686 $this->do_action( "after_{$name}_cron_multisite" );
6687 }
6688 }
6689
6690 #endregion
6691
6692 #----------------------------------------------------------------------------------
6693 #region Daily Sync Cron
6694 #----------------------------------------------------------------------------------
6695
6696
6697 /**
6698 * @author Vova Feldman (@svovaf)
6699 * @since 2.0.0
6700 *
6701 * @return bool
6702 */
6703 private function is_sync_cron_scheduled() {
6704 return $this->is_cron_on( 'sync' );
6705 }
6706
6707 /**
6708 * Get the sync cron's executing blog ID.
6709 *
6710 * @author Vova Feldman (@svovaf)
6711 * @since 2.0.0
6712 *
6713 * @return int
6714 */
6715 private function get_sync_cron_blog_id() {
6716 return $this->get_cron_blog_id( 'sync' );
6717 }
6718
6719 /**
6720 * @author Vova Feldman (@svovaf)
6721 * @since 1.1.7.3
6722 */
6723 private function run_manual_sync() {
6724 if ( ! $this->is_user_admin() ) {
6725 return;
6726 }
6727
6728 // Run manual sync.
6729 $this->_sync_cron();
6730
6731 // Reschedule next cron to run 24 hours from now (performance optimization).
6732 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6733 }
6734
6735 /**
6736 * Data sync cron job. Replaces the background sync non blocking HTTP request
6737 * that doesn't halt page loading.
6738 *
6739 * @author Vova Feldman (@svovaf)
6740 * @since 1.1.7.3
6741 * @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.
6742 */
6743 function _sync_cron() {
6744 $this->_logger->entrance();
6745
6746 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6747 }
6748
6749 /**
6750 * The actual data sync cron logic.
6751 *
6752 * @author Vova Feldman (@svovaf)
6753 * @since 2.0.0
6754 *
6755 * @param int[] $blog_ids
6756 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6757 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6758 * updates for a single site in case `execute_cron` has switched to a different blog.
6759 */
6760 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6761 if ( $this->is_registered() ) {
6762 if ( $this->has_paid_plan() ) {
6763 // Initiate background plan sync.
6764 $this->_sync_license( true, false, $current_blog_id );
6765
6766 if ( $this->is_paying() ) {
6767 // Check for premium plugin updates.
6768 $this->check_updates( true );
6769 }
6770 } else {
6771 // Sync install(s) (only if something changed locally).
6772 if ( 1 < count( $blog_ids ) ) {
6773 $this->sync_installs();
6774 } else {
6775 $this->sync_install();
6776 }
6777
6778 $this->maybe_sync_install_user();
6779 }
6780 }
6781 }
6782
6783 /**
6784 * Check if sync was executed in the last $period of seconds.
6785 *
6786 * @author Vova Feldman (@svovaf)
6787 * @since 1.1.7.3
6788 *
6789 * @param int $period In seconds
6790 *
6791 * @return bool
6792 */
6793 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6794 return $this->is_cron_executed( 'sync', $period );
6795 }
6796
6797 /**
6798 * @author Vova Feldman (@svovaf)
6799 * @since 1.1.7.3
6800 *
6801 * @return bool
6802 */
6803 private function is_sync_cron_on() {
6804 return $this->is_cron_on( 'sync' );
6805 }
6806
6807 /**
6808 * @author Leo Fajardo (@leorw)
6809 * @since 2.5.0
6810 */
6811 private function maybe_schedule_sync_cron() {
6812 $next_schedule = $this->next_sync_cron();
6813
6814 // The event is properly scheduled, so no need to reschedule it.
6815 if (
6816 is_numeric( $next_schedule ) &&
6817 $next_schedule > time()
6818 ) {
6819 return;
6820 }
6821
6822 $this->schedule_sync_cron();
6823 }
6824
6825 /**
6826 * @author Vova Feldman (@svovaf)
6827 * @since 1.1.7.3
6828 *
6829 * @param int $start_at Defaults to now.
6830 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6831 * @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.
6832 */
6833 private function schedule_sync_cron(
6834 $start_at = WP_FS__SCRIPT_START_TIME,
6835 $randomize_start = true,
6836 $except_blog_id = 0
6837 ) {
6838 $this->schedule_cron(
6839 'sync',
6840 'data_sync',
6841 'daily',
6842 $start_at,
6843 $randomize_start,
6844 $except_blog_id
6845 );
6846 }
6847
6848 /**
6849 * Add the actual sync function to the cron job hook.
6850 *
6851 * @author Vova Feldman (@svovaf)
6852 * @since 1.1.7.3
6853 */
6854 private function hook_callback_to_sync_cron() {
6855 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6856 }
6857
6858 /**
6859 * @author Vova Feldman (@svovaf)
6860 * @since 1.1.7.3
6861 *
6862 * @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.
6863 */
6864 private function clear_sync_cron( $is_network_clear = false ) {
6865 $this->_logger->entrance();
6866
6867 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6868 }
6869
6870 /**
6871 * Unix timestamp for next sync cron execution or false if not scheduled.
6872 *
6873 * @author Vova Feldman (@svovaf)
6874 * @since 1.1.7.3
6875 *
6876 * @return int|false
6877 */
6878 function next_sync_cron() {
6879 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6880 }
6881
6882 /**
6883 * Unix timestamp for previous sync cron execution or false if never executed.
6884 *
6885 * @author Vova Feldman (@svovaf)
6886 * @since 1.1.7.3
6887 *
6888 * @return int|false
6889 */
6890 function last_sync_cron() {
6891 return $this->cron_last_execution( 'sync' );
6892 }
6893
6894 #endregion Daily Sync Cron ------------------------------------------------------------------
6895
6896 #----------------------------------------------------------------------------------
6897 #region Async Install Sync
6898 #----------------------------------------------------------------------------------
6899
6900 /**
6901 * @author Vova Feldman (@svovaf)
6902 * @since 1.1.7.3
6903 *
6904 * @return bool
6905 */
6906 private function is_install_sync_scheduled() {
6907 return $this->is_cron_on( 'install_sync' );
6908 }
6909
6910 /**
6911 * Get the sync cron's executing blog ID.
6912 *
6913 * @author Vova Feldman (@svovaf)
6914 * @since 2.0.0
6915 *
6916 * @return int
6917 */
6918 private function get_install_sync_cron_blog_id() {
6919 return $this->get_cron_blog_id( 'install_sync' );
6920 }
6921
6922 /**
6923 * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
6924 *
6925 * @author Vova Feldman (@svovaf)
6926 * @since 1.1.7.3
6927 *
6928 * @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.
6929 */
6930 private function schedule_install_sync( $except_blog_id = 0 ) {
6931 if ( $this->is_clone() ) {
6932 return;
6933 }
6934
6935 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
6936 }
6937
6938 /**
6939 * Unix timestamp for previous install sync cron execution or false if never executed.
6940 *
6941 * @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.
6942 *
6943 * @author Vova Feldman (@svovaf)
6944 * @since 1.1.7.3
6945 *
6946 * @return int|false
6947 */
6948 function last_install_sync() {
6949 return $this->cron_last_execution( 'install_sync' );
6950 }
6951
6952 /**
6953 * Unix timestamp for next install sync cron execution or false if not scheduled.
6954 *
6955 * @author Vova Feldman (@svovaf)
6956 * @since 1.1.7.3
6957 *
6958 * @return int|false
6959 */
6960 function next_install_sync() {
6961 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
6962 }
6963
6964 /**
6965 * Add the actual install sync function to the cron job hook.
6966 *
6967 * @author Vova Feldman (@svovaf)
6968 * @since 1.1.7.3
6969 */
6970 private function hook_callback_to_install_sync() {
6971 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
6972 }
6973
6974 /**
6975 * @author Vova Feldman (@svovaf)
6976 * @since 1.1.7.3
6977 *
6978 * @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.
6979 */
6980 private function clear_install_sync_cron( $is_network_clear = false ) {
6981 $this->_logger->entrance();
6982
6983 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
6984 }
6985
6986 /**
6987 * @author Vova Feldman (@svovaf)
6988 * @since 1.1.7.3
6989 * @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.
6990 */
6991 public function _run_sync_install() {
6992 $this->_logger->entrance();
6993
6994 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
6995 }
6996
6997 /**
6998 * The actual install(s) sync cron logic.
6999 *
7000 * @author Vova Feldman (@svovaf)
7001 * @since 2.0.0
7002 *
7003 * @param int[] $blog_ids
7004 * @param int|null $current_blog_id
7005 */
7006 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
7007 if ( $this->is_registered() ) {
7008 if ( 1 < count( $blog_ids ) ) {
7009 $this->sync_installs( array(), true );
7010 } else {
7011 $this->sync_install( array(), true );
7012 }
7013
7014 $this->maybe_sync_install_user();
7015 }
7016 }
7017
7018 #endregion Async Install Sync ------------------------------------------------------------------
7019
7020 /**
7021 * Show a notice that activation is currently pending.
7022 *
7023 * @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.
7024 *
7025 * @author Vova Feldman (@svovaf)
7026 * @since 1.0.7
7027 *
7028 * @param bool|string $email_address
7029 * @param bool $is_pending_trial Since 1.2.1.5
7030 * @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.
7031 * @param bool $has_upgrade_context Since 2.5.3
7032 * @param bool $support_email_address Since 2.5.3
7033 */
7034 function _add_pending_activation_notice(
7035 $email_address = false,
7036 $is_pending_trial = false,
7037 $is_suspicious_email = false,
7038 $has_upgrade_context = false,
7039 $support_email_address = false
7040 ) {
7041 if ( ! is_string( $email_address ) ) {
7042 $current_user = self::_get_current_wp_user();
7043 $email_address = $current_user->user_email;
7044 }
7045
7046 $formatted_message_args = array(
7047 "<b>{$this->get_plugin_name()}</b>",
7048 "<b>{$email_address}</b>",
7049 );
7050
7051 if ( ! $has_upgrade_context || ! fs_is_network_admin() ) {
7052 /* translators: %3$s: action (e.g.: "start the trial" or "complete the opt-in") */
7053 $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' );
7054
7055 $formatted_message_args[] = $is_pending_trial ?
7056 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
7057 $this->get_text_inline( 'complete the opt-in', 'complete-the-opt-in' );
7058
7059 $notice_title = $this->get_text_inline( 'Thanks!', 'thanks' );
7060 } else {
7061 /* translators: %3$s: What the user is expected to receive via email (e.g.: "the installation instructions" or "a license key") */
7062 $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.' );
7063
7064 if ( $this->has_release_on_freemius() ) {
7065 $formatted_message_args[] = $this->get_text_x_inline(
7066 'the installation instructions',
7067 'Part of the message telling the user what they should receive via email.',
7068 'the-installation-instructions-phrase'
7069 );
7070 } else {
7071 $formatted_message_args[] = $this->get_text_x_inline(
7072 'a license key',
7073 'Part of the message telling the user what they should receive via email.',
7074 'a-license-key-phrase'
7075 );
7076
7077 $formatted_message .= ( ' ' . sprintf(
7078 /* translators: %s: activation link (e.g.: <a>Click here</a>) */
7079 $this->get_text_inline( '%s to activate the license once you get it.', 'license-activation-link-message' ),
7080 sprintf(
7081 '<b><a href="%s">%s</a></b>',
7082 $this->get_activation_url( array(
7083 'fs_action' => 'reset_pending_activation_mode',
7084 'require_license' => 'true',
7085 'fs_unique_affix' => $this->get_unique_affix(),
7086 ) ),
7087 $this->get_text_x_inline( 'Click here', 'Part of an activation link message.', 'click-here' )
7088 )
7089 ) );
7090 }
7091
7092 $formatted_message_args[] = ( ! empty( $support_email_address ) ) ?
7093 ( "<b>{$support_email_address}</b>" ) :
7094 $this->get_text_x_inline(
7095 "the product's support email address",
7096 'Part of the message that tells the user to check their spam folder for a specific email.',
7097 'product-support-email-address-phrase'
7098 );
7099
7100 $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' ) );
7101
7102 $notice_title = $this->get_text_inline( 'Thanks for upgrading.', 'after-upgrade-thank-you-message' );
7103 }
7104
7105 $this->_admin_notices->add_sticky(
7106 vsprintf( $formatted_message, $formatted_message_args ),
7107 'activation_pending',
7108 $notice_title
7109 );
7110 }
7111
7112 /**
7113 * Check if currently in plugin activation.
7114 *
7115 * @author Vova Feldman (@svovaf)
7116 * @since 1.1.4
7117 *
7118 * @return bool
7119 */
7120 function is_plugin_activation() {
7121 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7122
7123 return !empty($result);
7124 }
7125
7126 /**
7127 *
7128 * NOTE: admin_menu action executed before admin_init.
7129 *
7130 * @author Vova Feldman (@svovaf)
7131 * @since 1.0.7
7132 */
7133 function _admin_init_action() {
7134 $is_migration = $this->is_migration();
7135
7136 /**
7137 * Automatically redirect to connect/activation page after plugin activation.
7138 *
7139 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
7140 */
7141 if ( $this->is_plugin_activation() ) {
7142 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7143
7144 if ( isset( $_GET['activate-multi'] ) ) {
7145 /**
7146 * Don't redirect if activating multiple plugins at once (bulk activation).
7147 */
7148 } else if (
7149 self::is_deactivation_snoozed() &&
7150 (
7151 // Either running the free code base.
7152 ! $this->is_premium() ||
7153 // Or if has a free version.
7154 ! $this->is_only_premium() ||
7155 // If premium only, don't redirect if license is activated.
7156 ( $this->is_registered() && ! $this->can_use_premium_code() )
7157 )
7158 ) {
7159 /**
7160 * 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.
7161 */
7162 } else if ( ! $is_migration ) {
7163 $this->_redirect_on_activation_hook();
7164 return;
7165 }
7166 }
7167
7168 if ( $is_migration ) {
7169 return;
7170 }
7171
7172 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
7173 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
7174
7175 $this->skip_connection( fs_is_network_admin() );
7176
7177 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
7178 }
7179
7180 if ( $this->is_network_activation_mode() &&
7181 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
7182 ) {
7183 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
7184
7185 $this->delegate_connection();
7186
7187 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
7188 }
7189
7190 $this->_add_upgrade_action_link();
7191
7192 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
7193 (
7194 ( true === $this->_storage->require_license_activation ) ||
7195 // Not registered nor anonymous.
7196 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
7197 // OR, network level and in network upgrade mode.
7198 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
7199 )
7200 ) {
7201 if ( ! $this->is_pending_activation() ) {
7202 if ( ! $this->is_activation_page() ) {
7203 /**
7204 * If a user visits any other admin page before activating the premium-only theme with a valid
7205 * license, reactivate the previous theme.
7206 *
7207 * @author Leo Fajardo (@leorw)
7208 * @since 1.2.2
7209 */
7210 if ( $this->is_theme() &&
7211 ! $this->has_settings_menu() &&
7212 ! isset( $_REQUEST['fs_action'] ) &&
7213 $this->can_activate_previous_theme()
7214 ) {
7215 if ( $this->is_only_premium() ) {
7216 $this->activate_previous_theme();
7217 return;
7218 }
7219
7220 if ( true === $this->_storage->require_license_activation ) {
7221 $this->_storage->require_license_activation = false;
7222 }
7223 }
7224
7225 if ( ! fs_is_network_admin() &&
7226 $this->is_network_activation_mode() &&
7227 ! $this->is_delegated_connection()
7228 ) {
7229 return;
7230 }
7231
7232 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
7233 if ( ! $this->_anonymous_mode &&
7234 ( ! $this->is_addon() || ! $this->_parent->is_anonymous() ) ) {
7235 // Show notice for new plugin installations.
7236 $this->_admin_notices->add(
7237 sprintf(
7238 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
7239 sprintf( '<b><a href="%s">%s</a></b>',
7240 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
7241 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
7242 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
7243 )
7244 ),
7245 '',
7246 'update-nag'
7247 );
7248 }
7249 } else {
7250 if ( $this->should_add_sticky_optin_notice() ) {
7251 $this->add_sticky_optin_admin_notice();
7252 }
7253
7254 if ( $this->has_filter( 'optin_pointer_element' ) ) {
7255 // Don't show admin nag if plugin update.
7256 wp_enqueue_script( 'wp-pointer' );
7257 wp_enqueue_style( 'wp-pointer' );
7258
7259 $this->_enqueue_connect_essentials();
7260
7261 add_action( 'admin_print_footer_scripts', array(
7262 $this,
7263 '_add_connect_pointer_script'
7264 ) );
7265 }
7266 }
7267 }
7268 }
7269
7270 if ( $this->show_opt_in_on_themes_page() &&
7271 $this->is_activation_page()
7272 ) {
7273 $this->_show_theme_activation_optin_dialog();
7274 }
7275 }
7276 }
7277
7278 /**
7279 * @author Vova Feldman (@svovaf)
7280 * @since 2.0.0
7281 *
7282 * @return bool
7283 */
7284 private function should_add_sticky_optin_notice() {
7285 if ( $this->is_addon() && $this->_parent->is_anonymous() ) {
7286 return false;
7287 }
7288
7289 if ( fs_is_network_admin() ) {
7290 if ( ! $this->_is_network_active ) {
7291 return false;
7292 }
7293
7294 if ( ! $this->is_network_activation_mode() ) {
7295 return false;
7296 }
7297
7298 return ! isset( $this->_storage->sticky_optin_added_ms );
7299 }
7300
7301 if ( ! $this->is_activation_mode() ) {
7302 return false;
7303 }
7304
7305 // If running from a blog admin and delegated the connection.
7306 return ! isset( $this->_storage->sticky_optin_added );
7307 }
7308
7309 /**
7310 * @author Leo Fajardo (@leorw)
7311 * @since 2.0.0
7312 */
7313 private function add_sticky_optin_admin_notice() {
7314 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7315 $this->_storage->sticky_optin_added = true;
7316 } else {
7317 $this->_storage->sticky_optin_added_ms = true;
7318 }
7319
7320 // Show notice for new plugin installations.
7321 $this->_admin_notices->add_sticky(
7322 sprintf(
7323 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7324 $this->_module_type,
7325 sprintf( '<b><a href="%s">%s</a></b>',
7326 $this->get_activation_url(),
7327 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7328 )
7329 ),
7330 'connect_account',
7331 '',
7332 'update-nag'
7333 );
7334 }
7335
7336 /**
7337 * Enqueue connect requires scripts and styles.
7338 *
7339 * @author Vova Feldman (@svovaf)
7340 * @since 1.1.4
7341 */
7342 function _enqueue_connect_essentials() {
7343 wp_enqueue_script( 'jquery' );
7344 wp_enqueue_script( 'json2' );
7345
7346 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.js' );
7347 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7348 }
7349
7350 /**
7351 * Add connect / opt-in pointer.
7352 *
7353 * @author Vova Feldman (@svovaf)
7354 * @since 1.1.4
7355 */
7356 function _add_connect_pointer_script() {
7357 $vars = array( 'id' => $this->_module_id );
7358 $pointer_content = fs_get_template( 'connect.php', $vars );
7359 ?>
7360 <script type="text/javascript">// <![CDATA[
7361 jQuery(document).ready(function ($) {
7362 if ('undefined' !== typeof(jQuery().pointer)) {
7363
7364 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7365
7366 if (element.length > 0) {
7367 var optin = $(element).pointer($.extend(true, {}, {
7368 content : <?php echo json_encode( $pointer_content ) ?>,
7369 position : {
7370 edge : 'left',
7371 align: 'center'
7372 },
7373 buttons : function () {
7374 // Don't show pointer buttons.
7375 return '';
7376 },
7377 pointerWidth: 482
7378 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7379
7380 <?php
7381 echo $this->apply_filters( 'optin_pointer_execute', "
7382
7383 optin.pointer('open');
7384
7385 // Tag the opt-in pointer with custom class.
7386 $('.wp-pointer #fs_connect')
7387 .parents('.wp-pointer.wp-pointer-top')
7388 .addClass('fs-opt-in-pointer');
7389
7390 ", 'element', 'optin' ) ?>
7391 }
7392 }
7393 });
7394 // ]]></script>
7395 <?php
7396 }
7397
7398 /**
7399 * Return current page's URL.
7400 *
7401 * @author Vova Feldman (@svovaf)
7402 * @since 1.0.7
7403 *
7404 * @return string
7405 */
7406 static function current_page_url() {
7407 $url = 'http';
7408
7409 if ( isset( $_SERVER["HTTPS"] ) ) {
7410 if ( $_SERVER["HTTPS"] == "on" ) {
7411 $url .= "s";
7412 }
7413 }
7414 $url .= "://";
7415 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7416 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7417 } else {
7418 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7419 }
7420
7421 return esc_url( $url );
7422 }
7423
7424 /**
7425 * Check if the current page is the plugin's main admin settings page.
7426 *
7427 * @author Vova Feldman (@svovaf)
7428 * @since 1.0.7
7429 *
7430 * @return bool
7431 */
7432 function _is_plugin_page() {
7433 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7434 fs_is_plugin_page( $this->_slug );
7435 }
7436
7437 /* Events
7438 ------------------------------------------------------------------------------------------------------------------*/
7439 /**
7440 * Delete site install from Database.
7441 *
7442 * @author Vova Feldman (@svovaf)
7443 * @since 1.0.1
7444 *
7445 * @param bool $store
7446 * @param int|null $blog_id Since 2.0.0
7447 *
7448 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7449 */
7450 function _delete_site( $store = true, $blog_id = null ) {
7451 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7452 }
7453
7454 /**
7455 * Delete site install from Database.
7456 *
7457 * @author Vova Feldman (@svovaf)
7458 * @since 1.2.2.7
7459 *
7460 * @param string $slug
7461 * @param string $module_type
7462 * @param bool $store
7463 * @param int|null $blog_id Since 2.0.0
7464 *
7465 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7466 */
7467 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7468 $sites = self::get_all_sites( $module_type, $blog_id );
7469
7470 $install_id = false;
7471
7472 if ( isset( $sites[ $slug ] ) ) {
7473 if ( is_object( $sites[ $slug ] ) ) {
7474 $install_id = $sites[ $slug ]->id;
7475 }
7476
7477 unset( $sites[ $slug ] );
7478
7479 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7480 }
7481
7482 return $install_id;
7483 }
7484
7485 /**
7486 * Delete user.
7487 *
7488 * @author Vova Feldman (@svovaf)
7489 * @since 2.0.0
7490 *
7491 * @param number $user_id
7492 * @param bool $store
7493 *
7494 * @return false|int The user ID if deleted. Otherwise, FALSE (when install not exist).
7495 */
7496 private static function delete_user( $user_id, $store = true ) {
7497 $users = self::get_all_users();
7498
7499 if ( ! is_array( $users ) || ! isset( $users[ $user_id ] ) ) {
7500 return false;
7501 }
7502
7503 unset( $users[ $user_id ] );
7504
7505 self::$_accounts->set_option( 'users', $users, $store );
7506
7507 return $user_id;
7508 }
7509
7510 /**
7511 * Delete plugin's plans information.
7512 *
7513 * @param bool $store Flush to Database if true.
7514 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7515 *
7516 * @author Vova Feldman (@svovaf)
7517 * @since 1.0.9
7518 */
7519 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7520 $this->_logger->entrance();
7521
7522 $plans = self::get_all_plans( $this->_module_type );
7523
7524 $plans_to_keep = array();
7525
7526 if ( $keep_associated_plans ) {
7527 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7528 foreach ( $plans_ids_to_keep as $plan_id ) {
7529 $plan = self::_get_plan_by_id( $plan_id );
7530 if ( is_object( $plan ) ) {
7531 $plans_to_keep[] = self::_encrypt_entity( $plan );
7532 }
7533 }
7534 }
7535
7536 if ( ! empty( $plans_to_keep ) ) {
7537 $plans[ $this->_slug ] = $plans_to_keep;
7538 } else {
7539 unset( $plans[ $this->_slug ] );
7540 }
7541
7542 $this->set_account_option( 'plans', $plans, $store );
7543 }
7544
7545 /**
7546 * Delete all plugin licenses.
7547 *
7548 * @author Vova Feldman (@svovaf)
7549 * @since 1.0.9
7550 *
7551 * @param bool $store
7552 */
7553 private function _delete_licenses( $store = true ) {
7554 $this->_logger->entrance();
7555
7556 $all_licenses = self::get_all_licenses();
7557
7558 unset( $all_licenses[ $this->_module_id ] );
7559
7560 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7561 }
7562
7563 /**
7564 * Check if Freemius was added on new plugin installation.
7565 *
7566 * @author Vova Feldman (@svovaf)
7567 * @since 1.1.5
7568 *
7569 * @return bool
7570 */
7571 function is_plugin_new_install() {
7572 return isset( $this->_storage->is_plugin_new_install ) &&
7573 $this->_storage->is_plugin_new_install;
7574 }
7575
7576 /**
7577 * Check if it's the first plugin release that is running Freemius.
7578 *
7579 * @author Vova Feldman (@svovaf)
7580 * @since 1.2.1.5
7581 *
7582 * @return bool
7583 */
7584 function is_first_freemius_powered_version() {
7585 return empty( $this->_storage->plugin_last_version );
7586 }
7587
7588 /**
7589 * @author Leo Fajardo (@leorw)
7590 * @since 1.2.2
7591 *
7592 * @return bool|string
7593 */
7594 private function get_previous_theme_slug() {
7595 return isset( $this->_storage->previous_theme ) ?
7596 $this->_storage->previous_theme :
7597 false;
7598 }
7599
7600 /**
7601 * @author Leo Fajardo (@leorw)
7602 * @since 1.2.2
7603 *
7604 * @return bool
7605 */
7606 private function can_activate_previous_theme() {
7607 return $this->can_activate_theme( $this->get_previous_theme_slug() );
7608 }
7609
7610 /**
7611 * @author Leo Fajardo (@leorw)
7612 * @since 2.5.0
7613 *
7614 * @return bool
7615 */
7616 private function can_activate_theme( $slug ) {
7617 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7618 $theme_instance = wp_get_theme( $slug );
7619
7620 return $theme_instance->exists();
7621 }
7622
7623 return false;
7624 }
7625
7626 /**
7627 * @author Leo Fajardo (@leorw)
7628 * @since 1.2.2
7629 */
7630 private function activate_previous_theme() {
7631 switch_theme( $this->get_previous_theme_slug() );
7632 unset( $this->_storage->previous_theme );
7633
7634 global $pagenow;
7635 if ( 'themes.php' === $pagenow ) {
7636 /**
7637 * Refresh the active theme information.
7638 *
7639 * @author Leo Fajardo (@leorw)
7640 * @since 1.2.2
7641 */
7642 fs_redirect( $this->admin_url( $pagenow ) );
7643 }
7644 }
7645
7646 /**
7647 * @author Leo Fajardo (@leorw)
7648 * @since 1.2.2
7649 *
7650 * @return string
7651 */
7652 function get_previous_theme_activation_url() {
7653 if ( ! $this->can_activate_previous_theme() ) {
7654 return '';
7655 }
7656
7657 /**
7658 * Activation URL
7659 *
7660 * @author Leo Fajardo (@leorw)
7661 * @since 1.2.2
7662 */
7663 return wp_nonce_url(
7664 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7665 'switch-theme_' . $this->get_previous_theme_slug()
7666 );
7667 }
7668
7669 /**
7670 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7671 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7672 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7673 * theme doesn't exist, then there will be no close button.
7674 *
7675 * @author Leo Fajardo (@leorw)
7676 * @since 1.2.2
7677 *
7678 * @param string $slug_or_name Old theme's slug or name.
7679 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7680 */
7681 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7682 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7683 $old_theme->get_stylesheet() :
7684 $slug_or_name;
7685
7686 $this->_activate_plugin_event_hook();
7687 }
7688
7689 /**
7690 * Plugin activated hook.
7691 *
7692 * @author Vova Feldman (@svovaf)
7693 * @since 1.0.1
7694 *
7695 * @uses FS_Api
7696 */
7697 function _activate_plugin_event_hook() {
7698 $this->_logger->entrance( 'slug = ' . $this->_slug );
7699
7700 if ( ! $this->is_user_admin() ) {
7701 return;
7702 }
7703
7704 $this->unregister_uninstall_hook();
7705
7706 // Clear API cache on activation.
7707 FS_Api::clear_cache();
7708
7709 $is_premium_version_activation = $this->is_plugin() ?
7710 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7711 $this->is_premium();
7712
7713 if ( $is_premium_version_activation && $this->is_pending_activation() ) {
7714 $this->clear_pending_activation_mode();
7715 }
7716
7717 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7718
7719 if ( $this->is_plugin() ) {
7720 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7721 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7722 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7723 $other_version_basename = $is_premium_version_activation ?
7724 $this->_free_plugin_basename :
7725 $this->premium_plugin_basename();
7726
7727 if ( ! $this->_is_network_active ) {
7728 /**
7729 * Themes are always network activated, but the ACTUAL activation is per site.
7730 *
7731 * During the activation, the plugin isn't yet active, therefore,
7732 * _is_network_active will be set to false even if it's a network level
7733 * activation. So we need to fix that by looking at the is_network_admin() value.
7734 *
7735 * @author Vova Feldman
7736 */
7737 $this->_is_network_active = (
7738 $this->_is_multisite_integrated &&
7739 fs_is_network_admin()
7740 );
7741 }
7742
7743 /**
7744 * If the other module version is active, deactivate it.
7745 *
7746 * is_plugin_active() checks if the plugin is active on the site or the network level and
7747 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7748 *
7749 * @author Leo Fajardo (@leorw)
7750 * @since 1.2.2
7751 */
7752 if (
7753 is_plugin_active( $other_version_basename ) &&
7754 $this->apply_filters( 'deactivate_on_activation', true )
7755 ) {
7756 deactivate_plugins( $other_version_basename );
7757 }
7758 }
7759
7760 if ( $this->is_registered() ) {
7761 if ( $is_premium_version_activation ) {
7762 $this->reconnect_locally();
7763 }
7764
7765
7766 // Schedule re-activation event and sync.
7767 // $this->sync_install( array(), true );
7768 $this->schedule_install_sync();
7769
7770 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7771 if ( $is_premium_version_activation ) {
7772 $this->_admin_notices->add(
7773 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7774 $this->get_text_x_inline( 'W00t',
7775 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7776 );
7777 }
7778 } else if ( $this->is_anonymous() ) {
7779 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7780 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7781 $network = true;
7782 } else {
7783 $plugin_version = isset( $this->_storage->is_anonymous ) ?
7784 $this->_storage->is_anonymous['version'] :
7785 null;
7786 $network = false;
7787 }
7788
7789 /**
7790 * Reset "skipped" click cache on the following:
7791 * 1. Freemius DEV mode.
7792 * 2. WordPress DEBUG mode.
7793 * 3. If a plugin and the user skipped the exact same version before.
7794 *
7795 * @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).
7796 *
7797 * @todo 4. If explicitly asked to retry after every activation.
7798 */
7799 if ( WP_FS__DEV_MODE ||
7800 (
7801 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7802 $this->get_plugin_version() == $plugin_version
7803 )
7804 ) {
7805 $this->reset_anonymous_mode( $network );
7806 }
7807 }
7808
7809 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7810
7811 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7812 /**
7813 * When activating an add-on, try to also activate a license.
7814 *
7815 * @author Leo Fajardo (@leorw)
7816 * @since 2.3.0
7817 */
7818 if ( ! $this->_is_network_active ) {
7819 $this->maybe_activate_addon_license();
7820 } else {
7821 $this->maybe_network_activate_addon_license();
7822 }
7823
7824 /**
7825 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7826 *
7827 * @author Leo Fajardo (@leorw)
7828 * @since 2.3.0
7829 */
7830 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7831
7832 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7833 $this->_storage->require_license_activation = false;
7834 }
7835 }
7836
7837 if (
7838 $is_premium_version_activation &&
7839 (
7840 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7841 (
7842 $this->is_registered() &&
7843 ! $is_trial_or_has_features_enabled_license
7844 )
7845 )
7846 ) {
7847 $this->_storage->require_license_activation = true;
7848 }
7849
7850 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7851 /**
7852 * If no previous version of plugin's version exist, it means that it's either
7853 * the first time that the plugin installed on the site, or the plugin was installed
7854 * before but didn't have Freemius integrated.
7855 *
7856 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7857 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7858 * only after immediate activation.
7859 *
7860 * @since 1.1.4
7861 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7862 */
7863 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7864 }
7865
7866 /**
7867 * Also flush when activating the premium version so that even if Freemius was off before, the API
7868 * connectivity test can be run again.
7869 *
7870 * @author Leo Fajardo (@leorw)
7871 * @since 2.2.3.1
7872 */
7873 $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation );
7874
7875 if ( ! $this->_anonymous_mode &&
7876 ( false !== $has_api_connectivity ) &&
7877 ! $this->_isAutoInstall
7878 ) {
7879 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7880 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7881 }
7882
7883 /**
7884 * Activation hook is executed after the plugin's main file is loaded, therefore,
7885 * after the plugin was loaded. The logic is located at activate_plugin()
7886 * ./wp-admin/includes/plugin.php.
7887 *
7888 * @author Vova Feldman (@svovaf)
7889 * @since 1.1.9
7890 */
7891 $this->_storage->was_plugin_loaded = true;
7892 }
7893
7894 /**
7895 * @author Leo Fajardo (@leorw)
7896 * @since 2.3.0
7897 */
7898 private function maybe_activate_addon_license() {
7899 $parent_fs = $this->get_parent_instance();
7900
7901 if (
7902 ! is_object( $parent_fs ) ||
7903 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7904 ) {
7905 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7906 return;
7907 }
7908
7909 $license = $this->get_active_parent_license();
7910 if ( ! is_object( $license ) ) {
7911 return;
7912 }
7913
7914 if (
7915 $this->is_bundle_license_auto_activation_enabled() &&
7916 ! empty( $license->products )
7917 ) {
7918 $this->activate_bundle_license( $license );
7919
7920 return;
7921 }
7922
7923 if ( ! $this->is_registered() ) {
7924 // Opt in with a license key.
7925 $this->opt_in(
7926 $parent_fs->get_current_or_network_user()->email,
7927 false,
7928 false,
7929 $license->secret_key
7930 );
7931 } else {
7932 // Activate the license.
7933 $install = $this->api_site_call(
7934 '/',
7935 'put',
7936 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7937 );
7938
7939 if ( ! FS_Api::is_api_error( $install ) ) {
7940 $this->_sync_addon_license( $this->get_id(), true );
7941 }
7942 }
7943 }
7944
7945 /**
7946 * @author Leo Fajardo (@leorw)
7947 * @since 2.3.0
7948 *
7949 * @param FS_Plugin_License $license
7950 */
7951 private function maybe_network_activate_addon_license( $license = null ) {
7952 $parent_fs = $this->get_parent_instance();
7953 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7954 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7955 return;
7956 }
7957
7958 $license = ( ! is_null( $license ) ) ?
7959 $license :
7960 $this->get_active_parent_license();
7961
7962 if ( ! is_object( $license ) ) {
7963 return;
7964 }
7965
7966 if (
7967 $this->is_bundle_license_auto_activation_enabled() &&
7968 ! empty( $license->products )
7969 ) {
7970 $this->activate_bundle_license( $license );
7971
7972 return;
7973 }
7974
7975 if ( ! $this->is_network_registered() ) {
7976 $sites = $this->get_sites_for_network_level_optin();
7977
7978 if ( count( $sites ) > $license->left() ) {
7979 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
7980 return;
7981 }
7982
7983 // Opt in with a license key.
7984 $this->opt_in(
7985 $parent_fs->get_user()->email,
7986 false,
7987 false,
7988 $license->secret_key,
7989 false,
7990 false,
7991 false,
7992 null,
7993 $sites
7994 );
7995 } else {
7996 $blog_2_install_map = array();
7997 $site_ids = array();
7998
7999 $all_sites = Freemius::get_sites();
8000
8001 foreach ( $all_sites as $site ) {
8002 $blog_id = Freemius::get_site_blog_id( $site );
8003 $install = $this->get_install_by_blog_id( $blog_id );
8004
8005 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
8006 // Skip license activation for installs that are already associated with a license.
8007 continue;
8008 }
8009
8010 if ( is_object( $install ) ) {
8011 $blog_2_install_map[ $blog_id ] = $install;
8012 } else {
8013 $site_ids[] = $blog_id;
8014 }
8015 }
8016
8017 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
8018 return;
8019 }
8020
8021 $user = $this->get_current_or_network_user();
8022
8023 if ( ! empty( $blog_2_install_map ) ) {
8024 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
8025
8026 if ( true !== $result ) {
8027 return;
8028 }
8029 }
8030
8031 if ( ! empty( $site_ids ) ) {
8032 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
8033 }
8034 }
8035 }
8036
8037 /**
8038 * 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.
8039 *
8040 * @author Leo Fajardo (@leorw)
8041 * @since 2.4.0
8042 *
8043 * @param FS_Plugin_License $license
8044 * @param array $sites
8045 * @param int $blog_id
8046 */
8047 private function maybe_activate_bundle_license( FS_Plugin_License $license = null, $sites = array(), $blog_id = 0 ) {
8048 if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
8049 $license = $this->_license;
8050 }
8051
8052 if ( ! is_object( $license ) ) {
8053 return;
8054 }
8055
8056 $parent_license = ( ! empty( $license->products ) ) ?
8057 $license :
8058 $this->get_active_parent_license( $license->secret_key );
8059
8060 if ( is_object( $parent_license ) ) {
8061 $this->activate_bundle_license( $parent_license, $sites, $blog_id );
8062 }
8063 }
8064
8065 /**
8066 * Try to activate a bundle license for all the bundle products installed on the site.
8067 * (1) If a child product install already has a license, the bundle license won't be activated.
8068 * (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.
8069 * (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.
8070 *
8071 * @author Leo Fajardo (@leorw)
8072 * @since 2.4.0
8073 *
8074 * @param FS_Plugin_License $license
8075 * @param array $sites
8076 * @param int $current_blog_id
8077 */
8078 private function activate_bundle_license( $license, $sites = array(), $current_blog_id = 0 ) {
8079 $is_network_admin = fs_is_network_admin();
8080
8081 $installs_by_blog_map = array();
8082 $site_info_by_blog_map = array();
8083
8084 /**
8085 * Try to activate the license for all supported products.
8086 *
8087 * @author Leo Fajardo
8088 */
8089 foreach ( $license->products as $product_id ) {
8090 $fs = self::get_instance_by_id( $product_id );
8091
8092 if ( ! is_object( $fs ) ) {
8093 continue;
8094 }
8095
8096 if ( ! $fs->has_paid_plan() ) {
8097 continue;
8098 }
8099
8100 if (
8101 ! $fs->is_addon() &&
8102 ! FS_Plan_Manager::instance()->has_paid_plan( $fs->_plans )
8103 ) {
8104 /**
8105 * The parent product can be free-only but can have its `has_paid_plan` flag set to `true` when
8106 * there is a context bundle.
8107 */
8108 continue;
8109 }
8110
8111 if ( $current_blog_id > 0 ) {
8112 $fs->switch_to_blog( $current_blog_id );
8113 }
8114
8115 if ( $fs->has_active_valid_license() ) {
8116 continue;
8117 }
8118
8119 if ( ! $is_network_admin || $current_blog_id > 0 ) {
8120 if ( $fs->is_network_active() && ! $fs->is_delegated_connection( $current_blog_id ) ) {
8121 // Do not try to activate the license in the site level if the product is network active and the connection was not delegated.
8122 continue;
8123 }
8124 } else {
8125 if ( ! $fs->is_network_active() ) {
8126 // Do not try to activate the license in the network level if the product is not network active.
8127 continue;
8128 }
8129
8130 if ( $fs->is_network_delegated_connection() ) {
8131 // Do not try to activate the license in the network level if the activation has been delegated to site admins.
8132 continue;
8133 }
8134
8135 $has_install_with_license = false;
8136
8137 // 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.
8138 $filtered_sites = array();
8139
8140 if ( empty( $sites ) ) {
8141 $all_sites = self::get_sites();
8142
8143 foreach ( $all_sites as $site ) {
8144 $sites[] = array( 'blog_id' => self::get_site_blog_id( $site ) );
8145 }
8146 } else {
8147 // Populate the map here to avoid calling `$fs->get_site_info( $site );` in the other `for` loop below.
8148 foreach ( $sites as $site ) {
8149 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8150 continue;
8151 }
8152
8153 $site_info_by_blog_map[ $site['blog_id'] ] = $site;
8154 }
8155 }
8156
8157 foreach ( $sites as $site ) {
8158 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8159 continue;
8160 }
8161
8162 $blog_id = $site['blog_id'];
8163
8164 if ( ! isset( $installs_by_blog_map[ $blog_id ] ) ) {
8165 $installs_by_blog_map[ $blog_id ] = self::get_all_sites( $fs->get_module_type(), $blog_id );
8166 }
8167
8168 $installs = $installs_by_blog_map[ $blog_id ];
8169 $install = null;
8170
8171 if ( isset( $installs[ $fs->get_slug() ] ) ) {
8172 $install = $installs[ $fs->get_slug() ];
8173
8174 if (
8175 is_object( $install ) &&
8176 (
8177 ! FS_Site::is_valid_id( $install->id ) ||
8178 ! FS_User::is_valid_id( $install->user_id ) ||
8179 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
8180 )
8181 ) {
8182 $install = null;
8183 }
8184 }
8185
8186 if (
8187 is_object( $install ) &&
8188 FS_Plugin_License::is_valid_id( $install->license_id )
8189 ) {
8190 $has_install_with_license = true;
8191 break;
8192 }
8193
8194 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
8195 // Site activation delegated, don't activate bundle license on the site in the network admin.
8196 continue;
8197 }
8198
8199 if ( ! isset( $site_info_by_blog_map[ $blog_id ] ) ) {
8200 $site_info_by_blog_map[ $blog_id ] = $fs->get_site_info( $site );
8201 }
8202
8203 $filtered_sites[] = $site_info_by_blog_map[ $blog_id ];
8204 }
8205
8206 if ( $has_install_with_license || empty( $filtered_sites ) ) {
8207 // 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.
8208 continue;
8209 }
8210
8211 $sites = $filtered_sites;
8212 }
8213
8214 $fs->activate_migrated_license(
8215 $license->secret_key,
8216 null,
8217 null,
8218 $sites,
8219 ( $current_blog_id > 0 ? $current_blog_id : null )
8220 );
8221 }
8222 }
8223
8224 /**
8225 * Returns a parent license that can be activated for the context product.
8226 *
8227 * @author Leo Fajardo (@leorw)
8228 * @since 2.3.0
8229 *
8230 * @param string|null $license_key
8231 * @param bool $flush
8232 *
8233 * @return FS_Plugin_License
8234 */
8235 function get_active_parent_license( $license_key = null, $flush = true ) {
8236 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
8237
8238 $fs = $this;
8239
8240 if ( $this->is_addon() ) {
8241 $parent_instance = $this->get_parent_instance();
8242
8243 if ( is_object( $parent_instance ) && $parent_instance->is_registered() ) {
8244 $fs = $parent_instance;
8245 }
8246 }
8247
8248 $foreign_licenses = $fs->get_foreign_licenses_info(
8249 self::get_all_licenses( $this->get_parent_id() )
8250 );
8251
8252 if ( ! empty ( $foreign_licenses ) ) {
8253 $foreign_licenses = array(
8254 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
8255 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
8256 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
8257 );
8258
8259 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
8260 }
8261
8262 $result = $fs->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, $flush );
8263
8264 if (
8265 ! $this->is_api_result_object( $result, 'licenses' ) ||
8266 ! is_array( $result->licenses ) ||
8267 empty( $result->licenses )
8268 ) {
8269 return null;
8270 }
8271
8272 $parent_license = null;
8273
8274 if ( empty( $license_key ) ) {
8275 $parent_license = $result->licenses[0];
8276 } else {
8277 foreach ( $result->licenses as $license ) {
8278 if ( $license_key === $license->secret_key ) {
8279 $parent_license = $license;
8280 break;
8281 }
8282 }
8283 }
8284
8285 if ( ! is_null( $parent_license ) ) {
8286 $parent_license = new FS_Plugin_License( $parent_license );
8287 }
8288
8289 return $parent_license;
8290 }
8291
8292 /**
8293 * @author Leo Fajardo (@leorw)
8294 * @since 2.3.0
8295 *
8296 * @return array
8297 */
8298 function get_sites_for_network_level_optin() {
8299 $sites = array();
8300 $all_sites = self::get_sites();
8301
8302 foreach ( $all_sites as $site ) {
8303 $blog_id = self::get_site_blog_id( $site );
8304
8305 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
8306 ! $this->is_installed_on_site( $blog_id )
8307 ) {
8308 $sites[] = $this->get_site_info( $site );
8309 }
8310 }
8311
8312 return $sites;
8313 }
8314
8315 /**
8316 * Delete account.
8317 *
8318 * @author Vova Feldman (@svovaf)
8319 * @since 1.0.3
8320 *
8321 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8322 */
8323 function delete_account_event( $check_user = true ) {
8324 $this->_logger->entrance( 'slug = ' . $this->_slug );
8325
8326 if ( $check_user && ! $this->is_user_admin() ) {
8327 return;
8328 }
8329
8330 $this->do_action( 'before_account_delete' );
8331
8332 // Clear all admin notices.
8333 $this->_admin_notices->clear_all_sticky( false );
8334
8335 $this->_delete_site( false );
8336
8337 $delete_network_common_data = true;
8338
8339 if ( $this->_is_network_active ) {
8340 $installs = $this->get_blog_install_map();
8341
8342 // Don't delete common network data unless no other installs left.
8343 $delete_network_common_data = empty( $installs );
8344 }
8345
8346 if ( $delete_network_common_data ) {
8347 $this->_delete_plans( false );
8348
8349 $this->_delete_licenses( false );
8350
8351 // Delete add-ons related to plugin's account.
8352 $this->_delete_account_addons( false );
8353 }
8354
8355 // @todo Delete plans and licenses of add-ons.
8356
8357 self::$_accounts->store();
8358
8359 /**
8360 * IMPORTANT:
8361 * Clear crons must be executed before clearing all storage.
8362 * Otherwise, the cron will not be cleared.
8363 */
8364 if ( $delete_network_common_data ) {
8365 $this->clear_sync_cron();
8366 }
8367
8368 $this->clear_install_sync_cron();
8369
8370 // Clear all storage data.
8371 $this->_storage->clear_all( true, array(
8372 'is_delegated_connection',
8373 'connectivity_test',
8374 'is_on',
8375 ), false );
8376
8377 // Send delete event.
8378 $this->get_api_site_scope()->call( '/', 'delete' );
8379
8380 $this->do_action( 'after_account_delete' );
8381 }
8382
8383 /**
8384 * Delete network level account.
8385 *
8386 * @author Vova Feldman (@svovaf)
8387 * @since 2.0.0
8388 *
8389 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8390 */
8391 function delete_network_account_event( $check_user = true ) {
8392 $this->_logger->entrance( 'slug = ' . $this->_slug );
8393
8394 if ( $check_user && ! $this->is_user_admin() ) {
8395 return;
8396 }
8397
8398 $this->do_action( 'before_network_account_delete' );
8399
8400 // Clear all admin notices.
8401 $this->_admin_notices->clear_all_sticky();
8402
8403 $this->_delete_plans( false, false );
8404
8405 $this->_delete_licenses( false );
8406
8407 // Delete add-ons related to plugin's account.
8408 $this->_delete_account_addons( false );
8409
8410 // @todo Delete plans and licenses of add-ons.
8411
8412 self::$_accounts->store( true );
8413
8414 /**
8415 * IMPORTANT:
8416 * Clear crons must be executed before clearing all storage.
8417 * Otherwise, the cron will not be cleared.
8418 */
8419 $this->clear_sync_cron( true );
8420 $this->clear_install_sync_cron( true );
8421
8422 $sites = self::get_sites();
8423
8424 $install_ids = array();
8425 foreach ( $sites as $site ) {
8426 $blog_id = self::get_site_blog_id( $site );
8427
8428 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8429 continue;
8430 }
8431
8432 $install_id = $this->_delete_site( true, $blog_id );
8433
8434 // Clear all storage data.
8435 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8436
8437 if ( FS_Site::is_valid_id( $install_id ) ) {
8438 $install_ids[] = $install_id;
8439 }
8440
8441 switch_to_blog( $blog_id );
8442
8443 $this->do_action( 'after_account_delete' );
8444
8445 restore_current_blog();
8446 }
8447
8448 $this->_storage->clear_all( true, array(
8449 'connectivity_test',
8450 'is_on',
8451 ), true );
8452
8453 // Send delete event.
8454 if ( ! empty( $install_ids ) ) {
8455 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8456 }
8457
8458 $this->do_action( 'after_network_account_delete' );
8459 }
8460
8461 /**
8462 * Plugin deactivation hook.
8463 *
8464 * @author Vova Feldman (@svovaf)
8465 * @since 1.0.1
8466 */
8467 function _deactivate_plugin_hook() {
8468 $this->_logger->entrance( 'slug = ' . $this->_slug );
8469
8470 if ( ! $this->is_user_admin() ) {
8471 return;
8472 }
8473
8474 $is_network_deactivation = fs_is_network_admin();
8475 $storage_keys_for_removal = array();
8476
8477 $this->_admin_notices->clear_all_sticky();
8478
8479 $storage_keys_for_removal[] = 'sticky_optin_added';
8480 if ( isset( $this->_storage->sticky_optin_added ) ) {
8481 unset( $this->_storage->sticky_optin_added );
8482 }
8483
8484 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8485 // Remember that plugin was already installed.
8486 $this->_storage->is_plugin_new_install = false;
8487 }
8488
8489 // Hook to plugin uninstall.
8490 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8491
8492 $this->clear_module_main_file_cache();
8493 $this->clear_sync_cron( $this->_is_network_active );
8494 $this->clear_install_sync_cron();
8495
8496 if ( $this->is_registered() ) {
8497 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8498 FS_Plugin_Updater::instance( $this )->delete_update_data();
8499 }
8500
8501 if ( $is_network_deactivation ) {
8502 // Send deactivation event.
8503 $this->sync_installs( array(
8504 'is_active' => false,
8505 ) );
8506 } else {
8507 // Send deactivation event.
8508 $this->sync_install( array(
8509 'is_active' => false,
8510 ) );
8511 }
8512 } else {
8513 if ( false === $this->has_api_connectivity() && ! $this->is_premium() ) {
8514 // Reset connectivity test cache.
8515 $this->clear_connectivity_info();
8516
8517 $storage_keys_for_removal[] = 'connectivity_test';
8518 }
8519 }
8520
8521 if ( $is_network_deactivation ) {
8522 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8523 unset( $this->_storage->sticky_optin_added_ms );
8524 }
8525
8526 if ( ! empty( $storage_keys_for_removal ) ) {
8527 $sites = self::get_sites();
8528
8529 foreach ( $sites as $site ) {
8530 $blog_id = self::get_site_blog_id( $site );
8531
8532 foreach ( $storage_keys_for_removal as $key ) {
8533 $this->_storage->remove( $key, false, $blog_id );
8534 }
8535
8536 $this->_storage->save( $blog_id );
8537 }
8538 }
8539 }
8540
8541 // Clear API cache on deactivation.
8542 FS_Api::clear_cache();
8543
8544 $this->remove_sdk_reference();
8545 }
8546
8547 /**
8548 * @author Vova Feldman (@svovaf)
8549 * @since 1.1.6
8550 */
8551 private function remove_sdk_reference() {
8552 global $fs_active_plugins;
8553
8554 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8555 if ( $this->_plugin_basename == $data->plugin_path ) {
8556 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8557 break;
8558 }
8559 }
8560
8561 fs_fallback_to_newest_active_sdk();
8562 }
8563
8564 /**
8565 * @author Vova Feldman (@svovaf)
8566 * @since 1.1.3
8567 *
8568 * @param bool $is_anonymous
8569 * @param bool|int $network_or_blog_id Since 2.0.0
8570 */
8571 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8572 // Store information regarding skip to try and opt-in the user
8573 // again in the future.
8574 $skip_info = array(
8575 'is' => $is_anonymous,
8576 'timestamp' => WP_FS__SCRIPT_START_TIME,
8577 'version' => $this->get_plugin_version(),
8578 );
8579
8580 if ( true === $network_or_blog_id ) {
8581 $this->_storage->is_anonymous_ms = $skip_info;
8582 } else {
8583 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8584 }
8585
8586 $this->network_upgrade_mode_completed();
8587
8588 // Update anonymous mode cache.
8589 $this->_is_anonymous = $is_anonymous;
8590 }
8591
8592 /**
8593 * @author Vova Feldman (@svovaf)
8594 * @since 2.5.1
8595 *
8596 * @param bool|int $network_or_blog_id
8597 */
8598 private function unset_anonymous_mode( $network_or_blog_id = 0 ) {
8599 if ( true === $network_or_blog_id ) {
8600 unset( $this->_storage->is_anonymous_ms );
8601 } else {
8602 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8603 }
8604 }
8605
8606 /**
8607 * @author Vova Feldman (@svovaf)
8608 * @since 2.0.0
8609 *
8610 * @param int $blog_id Site ID.
8611 * @param int $user_id User ID.
8612 * @param string $domain Site domain.
8613 * @param string $path Site path.
8614 * @param int $network_id Network ID. Only relevant on multi-network installations.
8615 * @param array $meta Metadata. Used to set initial site options.
8616 *
8617 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8618 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8619 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8620 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8621 */
8622 public function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8623 $this->_logger->entrance();
8624
8625 if ( ! $this->_is_network_active ) {
8626 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
8627 return;
8628 }
8629
8630 $site = null;
8631 $new_blog_id = $blog_id;
8632
8633 if ( $this->is_premium() &&
8634 $this->is_network_connected() &&
8635 is_object( $this->_license ) &&
8636 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8637 $this->is_license_network_active( $blog_id )
8638 ) {
8639 /**
8640 * 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.
8641 */
8642 $current_blog_id = get_current_blog_id();
8643 $license = clone $this->_license;
8644
8645 $this->switch_to_blog( $blog_id );
8646
8647 // Opt-in with network user.
8648 $this->install_with_user(
8649 $this->get_network_user(),
8650 $license->secret_key,
8651 false,
8652 false,
8653 false
8654 );
8655
8656 if ( is_object( $this->_site ) ) {
8657 if ( $this->_site->license_id == $license->id ) {
8658 /**
8659 * If the license was activated successfully, sync the license data from the remote server.
8660 */
8661 $this->_license = $license;
8662 $this->sync_site_license();
8663 }
8664 }
8665
8666 $site = $this->_site;
8667
8668 $this->switch_to_blog( $current_blog_id );
8669
8670 if ( is_object( $site ) ) {
8671 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id, $site );
8672
8673 // Already connected (with or without a license), so no need to continue.
8674 return;
8675 }
8676 }
8677
8678 if ( $this->is_network_anonymous() ) {
8679 /**
8680 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8681 */
8682 $this->skip_site_connection( $blog_id );
8683 } else if ( $this->is_network_delegated_connection() ) {
8684 /**
8685 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8686 */
8687 $this->delegate_site_connection( $blog_id );
8688 } else if ( $this->is_network_connected() ) {
8689 /**
8690 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8691 */
8692 $current_blog_id = get_current_blog_id();
8693
8694 $this->switch_to_blog( $blog_id );
8695
8696 // Opt-in with network user.
8697 $this->install_with_user(
8698 $this->get_network_user(),
8699 false,
8700 false,
8701 false,
8702 false
8703 );
8704
8705 $site = $this->_site;
8706
8707 $this->switch_to_blog( $current_blog_id );
8708 } else {
8709 /**
8710 * If the super-admin mixed different options (connect, skip, delegated):
8711 * a) If at least one site connection was delegated, then automatically delegate connection.
8712 * 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.
8713 */
8714 $has_delegated_site = false;
8715
8716 $sites = self::get_sites();
8717 foreach ( $sites as $wp_site ) {
8718 $blog_id = self::get_site_blog_id( $wp_site );
8719
8720 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8721 $has_delegated_site = true;
8722 break;
8723 }
8724 }
8725
8726 if ( $has_delegated_site ) {
8727 $this->delegate_site_connection( $blog_id );
8728 } else {
8729 $this->skip_site_connection( $blog_id );
8730 }
8731 }
8732
8733 /**
8734 * 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.
8735 *
8736 * @author Leo Fajardo (@leorw)
8737 * @since 2.5.0
8738 */
8739 FS_Clone_Manager::instance()->store_blog_install_info( $new_blog_id, $site );
8740 }
8741
8742 /**
8743 * @author Vova Feldman (@svovaf)
8744 * @since 2.5.0
8745 *
8746 * @param \WP_Site $new_site
8747 * @param array $args
8748 */
8749 public function _after_wp_initialize_site_callback( WP_Site $new_site, $args ) {
8750 $this->_logger->entrance();
8751
8752 $this->_after_new_blog_callback(
8753 $new_site->id,
8754 // Dummy user ID (not in use).
8755 0,
8756 $new_site->domain,
8757 $new_site->path,
8758 $new_site->network_id,
8759 // Dummy meta, not in use.
8760 array()
8761 );
8762 }
8763
8764 /**
8765 * @author Vova Feldman (@svovaf)
8766 * @since 1.1.3
8767 *
8768 * @param bool|int|int[] $network_or_blog_ids Since 2.0.0.
8769 */
8770 private function reset_anonymous_mode( $network_or_blog_ids = false ) {
8771 if ( true === $network_or_blog_ids ) {
8772 $this->unset_anonymous_mode( true );
8773
8774 if ( fs_is_network_admin() ) {
8775 $this->_is_anonymous = null;
8776 }
8777
8778 // Rest anonymous mode for all non-delegated sub-sites.
8779 $blog_ids = $this->get_non_delegated_blog_ids();
8780 }
8781 else
8782 {
8783 if ( false === $network_or_blog_ids ) {
8784 $network_or_blog_ids = 0;
8785 }
8786
8787 $blog_ids = is_array( $network_or_blog_ids ) ?
8788 $network_or_blog_ids :
8789 array( $network_or_blog_ids );
8790
8791 foreach ( $blog_ids as $blog_id ) {
8792 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8793 $this->_is_anonymous = null;
8794 }
8795 }
8796 }
8797
8798 foreach ( $blog_ids as $blog_id ) {
8799 $this->unset_anonymous_mode( $blog_id );
8800 }
8801
8802 /**
8803 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8804 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8805 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8806 *
8807 * @author Leo Fajardo (@leorw)
8808 * @since 1.2.2
8809 */
8810 if ( ! $this->_is_network_active ) {
8811 $this->_is_anonymous = null;
8812 }
8813 }
8814
8815 /**
8816 * @author Leo Fajardo (@leorw)
8817 * @since 2.5.3
8818 */
8819 private function update_license_required_permissions_if_anonymous() {
8820 if ( ! $this->is_anonymous() ) {
8821 return;
8822 }
8823
8824 $this->reset_anonymous_mode( fs_is_network_admin() );
8825
8826 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
8827 'essentials' => true,
8828 'events' => true,
8829 'diagnostic' => false,
8830 'extensions' => false,
8831 'site' => false,
8832 ) );
8833 }
8834
8835 /**
8836 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8837 * deleting the account in the network level, the URL of the page to redirect to is correct.
8838 *
8839 * @author Leo Fajardo (@leorw)
8840 *
8841 * @since 2.1.3
8842 */
8843 private function maybe_set_slug_and_network_menu_exists_flag() {
8844 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8845 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8846 $this->_menu->get_slug() :
8847 $this->_slug
8848 );
8849 }
8850 }
8851
8852 /**
8853 * Clears the anonymous mode and redirects to the opt-in screen.
8854 *
8855 * @author Vova Feldman (@svovaf)
8856 * @since 1.1.7
8857 */
8858 function connect_again() {
8859 if ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) {
8860 return;
8861 }
8862
8863 if ( $this->is_anonymous() ) {
8864 $this->reset_anonymous_mode( fs_is_network_admin() );
8865 }
8866
8867 $activation_url_params = array();
8868
8869 if ( $this->is_pending_activation() ) {
8870 $this->clear_pending_activation_mode();
8871
8872 if ( fs_request_get_bool( 'require_license' ) ) {
8873 $activation_url_params['require_license'] = true;
8874 }
8875 }
8876
8877 $this->maybe_set_slug_and_network_menu_exists_flag();
8878
8879 fs_redirect( $this->get_activation_url( $activation_url_params ) );
8880 }
8881
8882 /**
8883 * Skip account connect, and set anonymous mode.
8884 *
8885 * @author Vova Feldman (@svovaf)
8886 * @since 1.1.1
8887 *
8888 * @param bool|int|int[] $network_or_blog_ids Since 2.5.1
8889 */
8890 function skip_connection( $network_or_blog_ids = false ) {
8891 $this->_logger->entrance();
8892
8893 $this->_admin_notices->remove_sticky( 'connect_account' );
8894
8895 if ( true === $network_or_blog_ids ) {
8896 $this->set_anonymous_mode( true, true );
8897
8898 if ( fs_is_network_admin() ) {
8899 $this->_is_anonymous = null;
8900 }
8901
8902 // Rest anonymous mode for all non-delegated sub-sites.
8903 $blog_ids = $this->get_non_delegated_blog_ids();
8904 }
8905 else
8906 {
8907 if ( false === $network_or_blog_ids ) {
8908 $network_or_blog_ids = 0;
8909 }
8910
8911 $blog_ids = is_array( $network_or_blog_ids ) ?
8912 $network_or_blog_ids :
8913 array( $network_or_blog_ids );
8914
8915 foreach ( $blog_ids as $blog_id ) {
8916 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8917 $this->_is_anonymous = null;
8918 }
8919 }
8920 }
8921
8922 foreach ( $blog_ids as $blog_id ) {
8923 $this->skip_site_connection( $blog_id );
8924 }
8925
8926 $this->network_upgrade_mode_completed();
8927 }
8928
8929 /**
8930 * Skip connection for specific site in the network.
8931 *
8932 * @author Vova Feldman (@svovaf)
8933 * @since 2.0.0
8934 *
8935 * @param int|null $blog_id
8936 * @param bool $send_skip
8937 */
8938 private function skip_site_connection( $blog_id = null ) {
8939 $this->_logger->entrance();
8940
8941 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8942
8943 $this->set_anonymous_mode( true, $blog_id );
8944 }
8945
8946 /**
8947 * Plugin version update hook.
8948 *
8949 * @author Vova Feldman (@svovaf)
8950 * @since 1.0.4
8951 */
8952 private function update_plugin_version_event() {
8953 $this->_logger->entrance();
8954
8955 if ( ! $this->is_registered() ) {
8956 return;
8957 }
8958
8959 $this->schedule_install_sync();
8960 // $this->sync_install( array(), true );
8961 }
8962
8963 /**
8964 * Generate an MD5 signature of a plugins collection.
8965 * This helper methods used to identify changes in a plugins collection.
8966 *
8967 * @author Vova Feldman (@svovaf)
8968 * @since 2.0.0
8969 *
8970 * @param array [string]array $plugins
8971 *
8972 * @return string
8973 */
8974 private function get_plugins_thumbprint( $plugins ) {
8975 ksort( $plugins );
8976
8977 $thumbprint = '';
8978 foreach ( $plugins as $basename => $data ) {
8979 $thumbprint .= $data['slug'] . ',' .
8980 $data['Version'] . ',' .
8981 ( $data['is_active'] ? '1' : '0' ) . ';';
8982 }
8983
8984 return md5( $thumbprint );
8985 }
8986
8987 /**
8988 * Return a list of modified plugins since the last sync.
8989 *
8990 * Note:
8991 * There's no point to store a plugins counter since even if the number of
8992 * plugins didn't change, we still need to check if the versions are all the
8993 * same and the activity state is similar.
8994 *
8995 * @author Vova Feldman (@svovaf)
8996 * @since 1.1.8
8997 *
8998 * @return array|false
8999 */
9000 private function get_plugins_data_for_api() {
9001 // Alias.
9002 $site_active_plugins_option_name = 'active_plugins';
9003 $network_plugins_option_name = 'all_plugins';
9004
9005 /**
9006 * Collection of all site level active plugins.
9007 */
9008 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
9009
9010 if ( ! is_object( $site_active_plugins_cache ) ) {
9011 $site_active_plugins_cache = (object) array(
9012 'timestamp' => '',
9013 'md5' => '',
9014 'plugins' => array(),
9015 );
9016 }
9017
9018 $time = time();
9019
9020 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
9021 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
9022 ) {
9023 // Don't send plugin updates if last update was in the past 5 min.
9024 return false;
9025 }
9026
9027 // Write timestamp to lock the logic.
9028 $site_active_plugins_cache->timestamp = $time;
9029 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
9030
9031 // Reload options from DB.
9032 self::$_accounts->load( true );
9033 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
9034
9035 if ( $time != $site_active_plugins_cache->timestamp ) {
9036 // If timestamp is different, then another thread captured the lock.
9037 return false;
9038 }
9039
9040 /**
9041 * Collection of all plugins (network level).
9042 */
9043 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
9044
9045 if ( ! is_object( $network_plugins_cache ) ) {
9046 $network_plugins_cache = (object) array(
9047 'timestamp' => '',
9048 'md5' => '',
9049 'plugins' => array(),
9050 );
9051 }
9052
9053 // Check if there's a change in plugins.
9054 $network_plugins = self::get_network_plugins();
9055 $site_active_plugins = self::get_site_active_plugins();
9056
9057 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
9058 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
9059
9060 // Check if plugins status changed (version or active/inactive).
9061 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
9062 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
9063
9064 if ( ! $network_plugins_changed &&
9065 ! $site_active_plugins_changed
9066 ) {
9067 // No changes.
9068 return array();
9069 }
9070
9071 $plugins_update_data = array();
9072
9073 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
9074 if ( ! isset( $network_plugins[ $basename ] ) ) {
9075 // Plugin uninstalled.
9076 $uninstalled_plugin_data = $data;
9077 $uninstalled_plugin_data['is_active'] = false;
9078 $uninstalled_plugin_data['is_uninstalled'] = true;
9079 $plugins_update_data[] = $uninstalled_plugin_data;
9080
9081 unset( $network_plugins[ $basename ] );
9082
9083 unset( $network_plugins_cache->plugins[ $basename ] );
9084 unset( $site_active_plugins_cache->plugins[ $basename ] );
9085
9086 continue;
9087 }
9088
9089 $was_active = $data['is_active'] ||
9090 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9091 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
9092 $is_active = $network_plugins[ $basename ]['is_active'] ||
9093 ( isset( $site_active_plugins[ $basename ] ) &&
9094 $site_active_plugins[ $basename ]['is_active'] );
9095
9096 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9097 isset( $site_active_plugins[ $basename ] )
9098 ) {
9099 // Plugin was site level activated.
9100 $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
9101 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
9102 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9103 ! isset( $site_active_plugins[ $basename ] )
9104 ) {
9105 // Plugin was site level deactivated.
9106 unset( $site_active_plugins_cache->plugins[ $basename ] );
9107 }
9108
9109 $prev_version = $data['version'];
9110 $current_version = $network_plugins[ $basename ]['Version'];
9111
9112 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
9113 // Plugin activated or deactivated, or version changed.
9114
9115 if ( $was_active !== $is_active ) {
9116 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
9117 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
9118 }
9119 }
9120
9121 if ( $prev_version !== $current_version ) {
9122 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
9123 }
9124
9125 $updated_plugin_data = $data;
9126 $updated_plugin_data['is_active'] = $is_active;
9127 $updated_plugin_data['version'] = $current_version;
9128 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
9129 $plugins_update_data[] = $updated_plugin_data;
9130 }
9131 }
9132
9133 // Find new plugins that weren't yet seen before.
9134 foreach ( $network_plugins as $basename => $data ) {
9135 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
9136 // New plugin.
9137 $new_plugin = array(
9138 'slug' => $data['slug'],
9139 'version' => $data['Version'],
9140 'title' => $data['Name'],
9141 'is_active' => $data['is_active'],
9142 'is_uninstalled' => false,
9143 );
9144
9145 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
9146
9147 $is_site_level_active = (
9148 isset( $site_active_plugins[ $basename ] ) &&
9149 $site_active_plugins[ $basename ]['is_active']
9150 );
9151
9152 /**
9153 * If not network active, set the activity status based on the site-level plugin status.
9154 */
9155 if ( ! $new_plugin['is_active'] ) {
9156 $new_plugin['is_active'] = $is_site_level_active;
9157 }
9158
9159 $plugins_update_data[] = $new_plugin;
9160
9161 if ( isset( $site_active_plugins[ $basename ] ) ) {
9162 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
9163 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
9164 }
9165 }
9166 }
9167
9168 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
9169 $site_active_plugins_cache->timestamp = $time;
9170 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
9171
9172 $network_plugins_cache->md5 = $network_plugins_thumbprint;
9173 $network_plugins_cache->timestamp = $time;
9174 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
9175
9176 return $plugins_update_data;
9177 }
9178
9179 /**
9180 * Return a list of modified themes since the last sync.
9181 *
9182 * Note:
9183 * There's no point to store a themes counter since even if the number of
9184 * themes didn't change, we still need to check if the versions are all the
9185 * same and the activity state is similar.
9186 *
9187 * @author Vova Feldman (@svovaf)
9188 * @since 1.1.8
9189 *
9190 * @return array|false
9191 */
9192 private function get_themes_data_for_api() {
9193 // Alias.
9194 $option_name = 'all_themes';
9195
9196 $all_cached_themes = self::$_accounts->get_option( $option_name );
9197
9198 if ( ! is_object( $all_cached_themes ) ) {
9199 $all_cached_themes = (object) array(
9200 'timestamp' => '',
9201 'md5' => '',
9202 'themes' => array(),
9203 );
9204 }
9205
9206 $time = time();
9207
9208 if ( ! empty( $all_cached_themes->timestamp ) &&
9209 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
9210 ) {
9211 // Don't send theme updates if last update was in the past 5 min.
9212 return false;
9213 }
9214
9215 // Write timestamp to lock the logic.
9216 $all_cached_themes->timestamp = $time;
9217 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9218
9219 // Reload options from DB.
9220 self::$_accounts->load( true );
9221 $all_cached_themes = self::$_accounts->get_option( $option_name );
9222
9223 if ( $time != $all_cached_themes->timestamp ) {
9224 // If timestamp is different, then another thread captured the lock.
9225 return false;
9226 }
9227
9228 // Get active theme.
9229 $active_theme = wp_get_theme();
9230 $active_theme_stylesheet = $active_theme->get_stylesheet();
9231
9232 // Check if there's a change in themes.
9233 $all_themes = wp_get_themes();
9234
9235 // Check if themes changed.
9236 ksort( $all_themes );
9237
9238 $themes_signature = '';
9239 foreach ( $all_themes as $slug => $data ) {
9240 $is_active = ( $slug === $active_theme_stylesheet );
9241 $themes_signature .= $slug . ',' .
9242 $data->version . ',' .
9243 ( $is_active ? '1' : '0' ) . ';';
9244 }
9245
9246 // Check if themes status changed (version or active/inactive).
9247 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
9248
9249 $themes_update_data = array();
9250
9251 if ( $themes_changed ) {
9252 // Change in themes, report changes.
9253
9254 // Update existing themes info.
9255 foreach ( $all_cached_themes->themes as $slug => $data ) {
9256 $is_active = ( $slug === $active_theme_stylesheet );
9257
9258 if ( ! isset( $all_themes[ $slug ] ) ) {
9259 // Plugin uninstalled.
9260 $uninstalled_theme_data = $data;
9261 $uninstalled_theme_data['is_active'] = false;
9262 $uninstalled_theme_data['is_uninstalled'] = true;
9263 $themes_update_data[] = $uninstalled_theme_data;
9264
9265 unset( $all_themes[ $slug ] );
9266 unset( $all_cached_themes->themes[ $slug ] );
9267 } else if ( $data['is_active'] !== $is_active ||
9268 $data['version'] !== $all_themes[ $slug ]->version
9269 ) {
9270 // Plugin activated or deactivated, or version changed.
9271
9272 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
9273 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
9274
9275 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
9276 }
9277 }
9278
9279 // Find new themes that weren't yet seen before.
9280 foreach ( $all_themes as $slug => $data ) {
9281 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
9282 $is_active = ( $slug === $active_theme_stylesheet );
9283
9284 // New plugin.
9285 $new_plugin = array(
9286 'slug' => $slug,
9287 'version' => $data->version,
9288 'title' => $data->name,
9289 'is_active' => $is_active,
9290 'is_uninstalled' => false,
9291 );
9292
9293 $themes_update_data[] = $new_plugin;
9294 $all_cached_themes->themes[ $slug ] = $new_plugin;
9295 }
9296 }
9297
9298 $all_cached_themes->md5 = md5( $themes_signature );
9299 $all_cached_themes->timestamp = time();
9300 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9301 }
9302
9303 return $themes_update_data;
9304 }
9305
9306 /**
9307 * Get site data for API install request.
9308 *
9309 * @author Vova Feldman (@svovaf)
9310 * @since 1.1.2
9311 *
9312 * @param string[] $override
9313 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9314 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9315 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, title, and URL).
9316 *
9317 * @return array
9318 */
9319 private function get_install_data_for_api(
9320 array $override,
9321 $include_plugins = true,
9322 $include_themes = true,
9323 $include_blog_data = true
9324 ) {
9325 // Alias.
9326 $permissions = FS_Permission_Manager::instance( $this );
9327
9328 if ( $permissions->is_extensions_tracking_allowed() ) {
9329 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
9330 /**
9331 * @since 1.1.8 Also send plugin updates.
9332 */
9333 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9334 $plugins = $this->get_plugins_data_for_api();
9335 if ( ! empty( $plugins ) ) {
9336 $override['plugins'] = $plugins;
9337 }
9338 }
9339 }
9340
9341 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
9342 /**
9343 * @since 1.1.8 Also send themes updates.
9344 */
9345 if ( $include_themes && ! isset( $override['themes'] ) ) {
9346 $themes = $this->get_themes_data_for_api();
9347 if ( ! empty( $themes ) ) {
9348 $override['themes'] = $themes;
9349 }
9350 }
9351 }
9352 }
9353
9354 $versions = $this->get_versions();
9355
9356 $blog_data = array();
9357 if ( $include_blog_data ) {
9358 $blog_data['url'] = self::get_unfiltered_site_url();
9359
9360 if ( $permissions->is_diagnostic_tracking_allowed() ) {
9361 $blog_data = array_merge( $blog_data, array(
9362 'language' => self::get_sanitized_language(),
9363 'title' => get_bloginfo( 'name' ),
9364 ) );
9365 }
9366 }
9367
9368 return array_merge( $versions, $blog_data, array(
9369 'version' => $this->get_plugin_version(),
9370 'is_premium' => $this->is_premium(),
9371 // Special params.
9372 'is_active' => true,
9373 'is_uninstalled' => false,
9374 ), $override );
9375 }
9376
9377 /**
9378 * Update installs details.
9379 *
9380 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
9381 *
9382 * @author Vova Feldman (@svovaf)
9383 * @since 2.0.0
9384 *
9385 * @param string[] string $override
9386 * @param bool $only_diff
9387 * @param bool $is_keepalive
9388 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9389 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9390 *
9391 * @return array
9392 */
9393 private function get_installs_data_for_api(
9394 array $override,
9395 $only_diff = false,
9396 $is_keepalive = false,
9397 $include_plugins = true,
9398 $include_themes = true
9399 ) {
9400 /**
9401 * @since 1.1.8 Also send plugin updates.
9402 */
9403 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9404 // $plugins = $this->get_plugins_data_for_api();
9405 // if ( ! empty( $plugins ) ) {
9406 // $override['plugins'] = $plugins;
9407 // }
9408 // }
9409 /**
9410 * @since 1.1.8 Also send themes updates.
9411 */
9412 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9413 // $themes = $this->get_themes_data_for_api();
9414 // if ( ! empty( $themes ) ) {
9415 // $override['themes'] = $themes;
9416 // }
9417 // }
9418
9419 // Common properties.
9420 $versions = $this->get_versions();
9421 $common = array_merge( $versions, array(
9422 'version' => $this->get_plugin_version(),
9423 'is_premium' => $this->is_premium(),
9424 ), $override );
9425
9426
9427 $is_common_diff_for_any_site = false;
9428 $common_diff_union = array();
9429
9430 $installs_data = array();
9431
9432 $sites = self::get_sites();
9433
9434 $subsite_data_for_api_by_install_id = array();
9435 $install_url_by_install_id = array();
9436 $subsite_registration_date_by_install_id = array();
9437
9438 foreach ( $sites as $site ) {
9439 $blog_id = self::get_site_blog_id( $site );
9440
9441 $install = $this->get_install_by_blog_id( $blog_id );
9442
9443 if ( is_object( $install ) ) {
9444 if ( $install->user_id != $this->_user->id ) {
9445 // Install belongs to a different owner.
9446 continue;
9447 }
9448
9449 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
9450 // Don't send updates regarding opted-out installs.
9451 continue;
9452 }
9453
9454 $install_data = $this->get_site_info( $site, true );
9455
9456 if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $install_data['blog_id'] ) ) {
9457 continue;
9458 }
9459
9460 $uid = $install_data['uid'];
9461 $url = $install_data['url'];
9462 $registration_date = $install_data['registration_date'];
9463
9464 if ( isset( $subsite_data_for_api_by_install_id[ $install->id ] ) ) {
9465 $clone_subsite_data = $subsite_data_for_api_by_install_id[ $install->id ];
9466 $clone_install_url = $install_url_by_install_id[ $install->id ];
9467 $clone_subsite_registration_date = $subsite_registration_date_by_install_id[ $install->id ];
9468
9469 $skip = false;
9470
9471 if (
9472 ! empty( $install_data['registration_date'] ) &&
9473 ! empty( $clone_subsite_registration_date )
9474 ) {
9475 /**
9476 * 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.
9477 *
9478 * @author Leo Fajardo (@leorw)
9479 * @since 2.5.1
9480 */
9481 $skip = ( strtotime( $install_data['registration_date'] ) > strtotime( $clone_subsite_registration_date ) );
9482 } else if (
9483 /**
9484 * 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.
9485 *
9486 * @author Leo Fajardo (@leorw)
9487 * @since 2.5.0
9488 */
9489 fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) ||
9490 fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $url ) )
9491 ) {
9492 $skip = true;
9493 }
9494
9495 if ( $skip ) {
9496 // 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.
9497 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
9498 continue;
9499 }
9500 }
9501
9502 unset( $install_data['blog_id'] );
9503 unset( $install_data['uid'] );
9504 unset( $install_data['url'] );
9505 unset( $install_data['registration_date'] );
9506
9507 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9508 $install_data['is_uninstalled'] = $install->is_uninstalled;
9509
9510 $common_diff = null;
9511 $is_common_diff = false;
9512 if ( $only_diff ) {
9513 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9514 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9515
9516 $is_common_diff = ! empty( $common_diff );
9517
9518 if ( $is_common_diff ) {
9519 foreach ( $common_diff as $k => $v ) {
9520 if ( ! isset( $common_diff_union[ $k ] ) ) {
9521 $common_diff_union[ $k ] = $v;
9522 }
9523 }
9524 }
9525
9526 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9527 }
9528
9529 if ( ! empty( $install_data ) || $is_common_diff || $is_keepalive ) {
9530 // Add install ID and site unique ID.
9531 $install_data['id'] = $install->id;
9532 $install_data['uid'] = $uid;
9533 $install_data['url'] = $url;
9534
9535 $subsite_data_for_api_by_install_id[ $install->id ] = $install_data;
9536 $install_url_by_install_id[ $install->id ] = $install->url;
9537 $subsite_registration_date_by_install_id[ $install->id ] = $registration_date;
9538 }
9539 }
9540 }
9541
9542 restore_current_blog();
9543
9544 $installs_data = array_merge(
9545 $installs_data,
9546 array_values( $subsite_data_for_api_by_install_id )
9547 );
9548
9549 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9550 if ( ! $only_diff ) {
9551 $installs_data[] = $common;
9552 } else if ( ! empty( $common_diff_union ) ) {
9553 $installs_data[] = $common_diff_union;
9554 }
9555 }
9556
9557 foreach ( $installs_data as &$data ) {
9558 $data = (object) $data;
9559 }
9560
9561 return $installs_data;
9562 }
9563
9564 /**
9565 * Compare site actual data to the stored install data and return the differences for an API data sync.
9566 *
9567 * @author Vova Feldman (@svovaf)
9568 * @since 2.0.0
9569 *
9570 * @param array $site
9571 * @param FS_Site $install
9572 * @param string[] string $override
9573 *
9574 * @return array
9575 */
9576 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9577 $diff = array();
9578 $special = array();
9579 $special_override = false;
9580
9581 foreach ( $site as $p => $v ) {
9582 if ( property_exists( $install, $p ) ) {
9583 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9584 $install->{$p} != $v
9585 ) {
9586 $val = self::get_api_sanitized_property( $p, $v );
9587
9588 if ( $install->{$p} != $val ) {
9589 $install->{$p} = $val;
9590 $diff[ $p ] = $val;
9591 }
9592 }
9593 } else {
9594 $special[ $p ] = $v;
9595
9596 if ( isset( $override[ $p ] ) ||
9597 'plugins' === $p ||
9598 'themes' === $p
9599 ) {
9600 $special_override = true;
9601 }
9602 }
9603 }
9604
9605 if ( $special_override || 0 < count( $diff ) ) {
9606 // Add special params only if has at least one
9607 // standard param, or if explicitly requested to
9608 // override a special param or a param which is not exist
9609 // in the install object.
9610 $diff = array_merge( $diff, $special );
9611 }
9612
9613 return $diff;
9614 }
9615
9616 /**
9617 * @author Leo Fajardo (@leorw)
9618 * @since 2.5.1
9619 */
9620 private function send_pending_clone_update_once() {
9621 $this->_logger->entrance();
9622
9623 if ( ! empty( $this->_storage->clone_id ) ) {
9624 return;
9625 }
9626
9627 $install_clone = $this->get_api_site_scope()->call(
9628 '/clones',
9629 'post',
9630 array( 'site_url' => self::get_unfiltered_site_url() )
9631 );
9632
9633 if ( $this->is_api_result_entity( $install_clone ) ) {
9634 $this->_storage->clone_id = $install_clone->id;
9635 }
9636 }
9637
9638 /**
9639 * @author Leo Fajardo (@leorw)
9640 * @since 2.5.1
9641 *
9642 * @param string $resolution_type
9643 * @param FS_Site $clone_context_install
9644 */
9645 function send_clone_resolution_update( $resolution_type, $clone_context_install ) {
9646 $this->_logger->entrance();
9647
9648 if ( empty( $this->_storage->clone_id ) ) {
9649 return;
9650 }
9651
9652 $new_install_id = null;
9653 $current_site = null;
9654
9655 $flush = false;
9656
9657 /**
9658 * 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.
9659 */
9660 if ( $clone_context_install->id != $this->_site->id ) {
9661 $new_install_id = $this->_site->id;
9662 $current_site = $this->_site;
9663 $this->_site = $clone_context_install;
9664
9665 $flush = true;
9666 }
9667
9668 $this->get_api_site_scope( $flush )->call(
9669 "/clones/{$this->_storage->clone_id}",
9670 'put',
9671 array(
9672 'resolution' => $resolution_type,
9673 'new_install_id' => $new_install_id,
9674 )
9675 );
9676
9677 if ( is_object( $current_site ) ) {
9678 /**
9679 * Ensure that the install scope entity is updated back to the previous install entity.
9680 */
9681 $this->_site = $current_site;
9682
9683 // Restore the previous install scope entity of the API.
9684 $this->get_api_site_scope( true );
9685 }
9686 }
9687
9688 /**
9689 * Update install only if changed.
9690 *
9691 * @author Vova Feldman (@svovaf)
9692 * @since 1.0.9
9693 *
9694 * @param string[] string $override
9695 * @param bool $flush
9696 * @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.
9697 *
9698 * @return false|object|string
9699 */
9700 private function send_install_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9701 $this->_logger->entrance();
9702
9703 $check_properties = $this->get_install_data_for_api( $override );
9704
9705 if ( $flush ) {
9706 $params = $check_properties;
9707 } else {
9708 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9709 }
9710
9711 if ( empty( $params ) ) {
9712 $keepalive_only_update = $this->should_send_keepalive_update();
9713
9714 if ( ! $keepalive_only_update ) {
9715 /**
9716 * There are no updates to send including keepalive.
9717 *
9718 * @author Leo Fajardo (@leorw)
9719 * @since 2.2.3
9720 */
9721 return false;
9722 }
9723 }
9724
9725 if ( $is_two_way_sync ) {
9726 /**
9727 * Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9728 *
9729 * @author Leo Fajardo (@leorw)
9730 * @since 2.2.3
9731 */
9732 if ( ! is_multisite() ) {
9733 // Update last install sync timestamp.
9734 $this->set_cron_execution_timestamp( 'install_sync' );
9735 }
9736
9737 $params['uid'] = $this->get_anonymous_id();
9738 }
9739
9740 $this->set_keepalive_timestamp();
9741
9742 // Send updated values to FS.
9743 $site = $this->api_site_call( '/', 'put', $params, true );
9744
9745 if ( $is_two_way_sync && $this->is_api_result_entity( $site ) ) {
9746 /**
9747 * Clear scheduled install sync after a two-way sync call.
9748 *
9749 * @author Leo Fajardo (@leorw)
9750 * @since 2.2.3
9751 */
9752 if ( ! is_multisite() ) {
9753 // I successfully sent install update, clear scheduled sync if exist.
9754 $this->clear_install_sync_cron();
9755 }
9756 }
9757
9758 return $site;
9759 }
9760
9761 /**
9762 * Update installs only if changed.
9763 *
9764 * @author Vova Feldman (@svovaf)
9765 * @since 2.0.0
9766 *
9767 * @param string[] string $override
9768 * @param bool $flush
9769 * @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.
9770 *
9771 * @return false|object|string
9772 */
9773 private function send_installs_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9774 $this->_logger->entrance();
9775
9776 /**
9777 * Pass `true` to use the network level storage since the update is for many installs.
9778 *
9779 * @author Leo Fajardo (@leorw)
9780 * @since 2.2.3
9781 */
9782 $should_send_keepalive = $this->should_send_keepalive_update( true );
9783
9784 $installs_data = $this->get_installs_data_for_api( $override, ! $flush, $should_send_keepalive );
9785
9786 if ( empty( $installs_data ) ) {
9787 return false;
9788 }
9789
9790 if ( $is_two_way_sync ) {
9791 // Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9792 $this->set_cron_execution_timestamp( 'install_sync' );
9793 }
9794
9795 /**
9796 * Pass `true` to use the network level storage since the update is for many installs.
9797 *
9798 * @author Leo Fajardo (@leorw)
9799 * @since 2.2.3
9800 */
9801 $this->set_keepalive_timestamp( true );
9802
9803 // Send updated values to FS.
9804 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9805
9806 if ( $is_two_way_sync && $this->is_api_result_object( $result, 'installs' ) ) {
9807 // I successfully sent a two-way installs update, clear the scheduled install sync if it exists.
9808 $this->clear_install_sync_cron();
9809 }
9810
9811 return $result;
9812 }
9813
9814 /**
9815 * @author Leo Fajardo (@leorw)
9816 *
9817 * @param bool|null $use_network_level_storage
9818 *
9819 * @return bool
9820 */
9821 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9822 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9823
9824 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9825 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9826 return true;
9827 } else {
9828 // 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.
9829 return ( 7 == rand( 1, 7 ) );
9830 }
9831 }
9832
9833 /**
9834 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9835 *
9836 * @author Leo Fajardo (@leorw)
9837 * @since 2.3.2
9838 */
9839 private function maybe_sync_install_user() {
9840 if ( $this->_user->id == $this->_site->user_id ) {
9841 return;
9842 }
9843
9844 // Fetch user data and store if found.
9845 $this->sync_user_by_current_install();
9846 }
9847
9848 /**
9849 * Update install only if changed.
9850 *
9851 * @author Vova Feldman (@svovaf)
9852 * @since 1.0.9
9853 *
9854 * @param string[] string $override
9855 * @param bool $flush
9856 */
9857 function sync_install( $override = array(), $flush = false ) {
9858 $this->_logger->entrance();
9859
9860 $site = $this->send_install_update( $override, $flush, true );
9861
9862 if ( false === $site ) {
9863 // No sync required.
9864 return;
9865 }
9866
9867 if ( ! $this->is_api_result_entity( $site ) ) {
9868 // Failed to sync, don't update locally.
9869 return;
9870 }
9871
9872 $this->_site = new FS_Site( $site );
9873
9874 $this->_store_site( true );
9875 }
9876
9877 /**
9878 * Update install only if changed.
9879 *
9880 * @author Vova Feldman (@svovaf)
9881 * @since 1.0.9
9882 *
9883 * @param string[] string $override
9884 * @param bool $flush
9885 */
9886 private function sync_installs( $override = array(), $flush = false ) {
9887 $this->_logger->entrance();
9888
9889 $result = $this->send_installs_update( $override, $flush, true );
9890
9891 if ( false === $result ) {
9892 // No sync required.
9893 return;
9894 }
9895
9896 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9897 // Failed to sync, don't update locally.
9898 return;
9899 }
9900
9901 $address_to_blog_map = $this->get_address_to_blog_map();
9902
9903 foreach ( $result->installs as $install ) {
9904 $this->_site = new FS_Site( $install );
9905
9906 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9907 $blog_id = $address_to_blog_map[ $address ];
9908
9909 $this->_store_site( true, $blog_id );
9910 }
9911 }
9912
9913 /**
9914 * Track install's custom event.
9915 *
9916 * IMPORTANT:
9917 * Custom event tracking is currently only supported for specific clients.
9918 * If you are not one of them, please don't use this method. If you will,
9919 * the API will simply ignore your request based on the plugin ID.
9920 *
9921 * Need custom tracking for your plugin or theme?
9922 * If you are interested in custom event tracking please contact yo@freemius.com
9923 * for further details.
9924 *
9925 * @author Vova Feldman (@svovaf)
9926 * @since 1.2.1
9927 *
9928 * @param string $name Event name.
9929 * @param array $properties Associative key/value array with primitive values only
9930 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9931 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9932 *
9933 * @return object|false Event data or FALSE on failure.
9934 *
9935 * @throws \Freemius_InvalidArgumentException
9936 */
9937 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9938 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9939
9940 if ( ! $this->is_registered() ) {
9941 return false;
9942 }
9943
9944 $event = array( 'type' => $name );
9945
9946 if ( is_numeric( $process_at ) && $process_at > time() ) {
9947 $event['process_at'] = $process_at;
9948 }
9949
9950 if ( $once ) {
9951 $event['once'] = true;
9952 }
9953
9954 if ( ! empty( $properties ) ) {
9955 // Verify associative array values are primitive.
9956 foreach ( $properties as $k => $v ) {
9957 if ( ! is_scalar( $v ) ) {
9958 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9959 }
9960 }
9961
9962 $event['properties'] = $properties;
9963 }
9964
9965 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9966
9967 return $this->is_api_error( $result ) ?
9968 false :
9969 $result;
9970 }
9971
9972 /**
9973 * Track install's custom event only once, but it still triggers the API call.
9974 *
9975 * IMPORTANT:
9976 * Custom event tracking is currently only supported for specific clients.
9977 * If you are not one of them, please don't use this method. If you will,
9978 * the API will simply ignore your request based on the plugin ID.
9979 *
9980 * Need custom tracking for your plugin or theme?
9981 * If you are interested in custom event tracking please contact yo@freemius.com
9982 * for further details.
9983 *
9984 * @author Vova Feldman (@svovaf)
9985 * @since 1.2.1
9986 *
9987 * @param string $name Event name.
9988 * @param array $properties Associative key/value array with primitive values only
9989 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9990 *
9991 * @return object|false Event data or FALSE on failure.
9992 *
9993 * @throws \Freemius_InvalidArgumentException
9994 *
9995 * @user Freemius::track_event()
9996 */
9997 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9998 return $this->track_event( $name, $properties, $process_at, true );
9999 }
10000
10001 /**
10002 * Plugin uninstall hook.
10003 *
10004 * @author Vova Feldman (@svovaf)
10005 * @since 1.0.1
10006 *
10007 * @param bool $check_user Enforce checking if user have plugins activation privileges.
10008 */
10009 function _uninstall_plugin_event( $check_user = true ) {
10010 $this->_logger->entrance( 'slug = ' . $this->_slug );
10011
10012 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
10013 return;
10014 }
10015
10016 $params = array();
10017 $uninstall_reason = null;
10018 if ( isset( $this->_storage->uninstall_reason ) ) {
10019 $uninstall_reason = $this->_storage->uninstall_reason;
10020 $params['reason_id'] = $uninstall_reason->id;
10021 $params['reason_info'] = $uninstall_reason->info;
10022 }
10023
10024 if ( ! $this->is_registered() ) {
10025 // Send anonymous uninstall event only if user submitted a feedback.
10026 if ( isset( $uninstall_reason ) ) {
10027 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
10028 $this->opt_in( false, false, false, false, true );
10029 } else {
10030 $params['uid'] = $this->get_anonymous_id();
10031 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
10032 }
10033 }
10034 } else {
10035 $params = array_merge( $params, array(
10036 'is_active' => false,
10037 'is_uninstalled' => true,
10038 ) );
10039
10040 if ( $this->_is_network_active ) {
10041 // Send uninstall event.
10042 $this->send_installs_update( $params );
10043 } else {
10044 // Send uninstall event and handle the result.
10045 $this->sync_install( $params );
10046 }
10047 }
10048
10049 // @todo Decide if we want to delete plugin information from db.
10050 }
10051
10052 /**
10053 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
10054 *
10055 * @author Vova Feldman (@svovaf)
10056 * @since 2.2.1
10057 *
10058 * @param string $is_premium
10059 * @param string $caller
10060 *
10061 * @return void
10062 */
10063 function set_basename( $is_premium, $caller ) {
10064 $basename = plugin_basename( $caller );
10065
10066 $current_basename = $is_premium ?
10067 $this->_premium_plugin_basename :
10068 $this->_free_plugin_basename;
10069
10070 if ( $current_basename == $basename ) {
10071 // Basename value set correctly.
10072 return;
10073 }
10074
10075 if ( $is_premium ) {
10076 $this->_premium_plugin_basename = $basename;
10077 } else {
10078 $this->_free_plugin_basename = $basename;
10079 }
10080
10081 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
10082
10083 register_activation_hook(
10084 $plugin_dir . $basename,
10085 array( &$this, '_activate_plugin_event_hook' )
10086 );
10087 }
10088
10089 /**
10090 * @author Vova Feldman (@svovaf)
10091 * @since 1.1.1
10092 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
10093 *
10094 * @return string
10095 */
10096 function premium_plugin_basename() {
10097 if ( ! isset( $this->_premium_plugin_basename ) ) {
10098 $this->_premium_plugin_basename = $this->is_premium() ?
10099 // The product is premium, so use the current basename.
10100 $this->_plugin_basename :
10101 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
10102 }
10103
10104 return $this->_premium_plugin_basename;
10105 }
10106
10107 /**
10108 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
10109 *
10110 * @author Vova Feldman (@svovaf)
10111 * @since 1.0.2
10112 */
10113 public static function _uninstall_plugin_hook() {
10114 self::_load_required_static();
10115
10116 self::$_static_logger->entrance();
10117
10118 if ( ! current_user_can( 'activate_plugins' ) ) {
10119 return;
10120 }
10121
10122 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
10123
10124 self::$_static_logger->info( 'plugin = ' . $plugin_file );
10125
10126 define( 'WP_FS__UNINSTALL_MODE', true );
10127
10128 $fs = self::get_instance_by_file( $plugin_file );
10129
10130 if ( is_object( $fs ) ) {
10131 $fs->remove_sdk_reference();
10132
10133 self::require_plugin_essentials();
10134
10135 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
10136 is_plugin_active( $fs->premium_plugin_basename() )
10137 ) {
10138 // Deleting Free or Premium plugin version while the other version still installed.
10139 return;
10140 }
10141
10142 if (
10143 ! $fs->is_clone() &&
10144 /**
10145 * 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).
10146 *
10147 * @author Leo Fajardo
10148 */
10149 ( ! is_object( $fs->_site ) || $fs->is_registered() )
10150 ) {
10151 $fs->_uninstall_plugin_event();
10152 }
10153
10154 $fs->do_action( 'after_uninstall' );
10155 }
10156 }
10157
10158 #----------------------------------------------------------------------------------
10159 #region Plugin Information
10160 #----------------------------------------------------------------------------------
10161
10162 /**
10163 * Load WordPress core plugin.php essential module.
10164 *
10165 * @author Vova Feldman (@svovaf)
10166 * @since 1.1.1
10167 */
10168 private static function require_plugin_essentials() {
10169 if ( ! function_exists( 'get_plugins' ) ) {
10170 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
10171
10172 require_once ABSPATH . 'wp-admin/includes/plugin.php';
10173 }
10174 }
10175
10176 /**
10177 * Load WordPress core pluggable.php module.
10178 *
10179 * @author Vova Feldman (@svovaf)
10180 * @since 1.1.2
10181 */
10182 private static function require_pluggable_essentials() {
10183 if ( ! function_exists( 'wp_get_current_user' ) ) {
10184 require_once ABSPATH . 'wp-includes/pluggable.php';
10185 }
10186 }
10187
10188 /**
10189 * Return plugin data.
10190 *
10191 * @author Vova Feldman (@svovaf)
10192 * @since 1.0.1
10193 *
10194 * @param bool $reparse_plugin_metadata
10195 *
10196 * @return array
10197 */
10198 function get_plugin_data( $reparse_plugin_metadata = false ) {
10199 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
10200 self::require_plugin_essentials();
10201
10202 if ( $this->is_plugin() ) {
10203 /**
10204 * @author Vova Feldman (@svovaf)
10205 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
10206 *
10207 * @link https://github.com/Freemius/wordpress-sdk/issues/77
10208 */
10209 $plugin_data = get_plugin_data(
10210 $this->_plugin_main_file_path,
10211 false,
10212 false
10213 );
10214 } else {
10215 $theme_data = wp_get_theme();
10216
10217 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
10218 $parent_theme = $theme_data->parent();
10219
10220 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
10221 $theme_data = $parent_theme;
10222 }
10223 }
10224
10225 $plugin_data = array(
10226 'Name' => $theme_data->get( 'Name' ),
10227 'Version' => $theme_data->get( 'Version' ),
10228 'Author' => $theme_data->get( 'Author' ),
10229 'Description' => $theme_data->get( 'Description' ),
10230 'PluginURI' => $theme_data->get( 'ThemeURI' ),
10231 );
10232 }
10233
10234 $this->_plugin_data = $plugin_data;
10235 }
10236
10237 return $this->_plugin_data;
10238 }
10239
10240 /**
10241 * @author Vova Feldman (@svovaf)
10242 * @since 1.0.1
10243 * @since 1.2.2.5 If slug not set load slug by module ID.
10244 *
10245 * @return string Plugin slug.
10246 */
10247 function get_slug() {
10248 if ( ! isset( $this->_slug ) ) {
10249 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
10250 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
10251 }
10252
10253 return $this->_slug;
10254 }
10255
10256 /**
10257 * @author Leo Fajardo (@leorw)
10258 * @since 2.2.1
10259 *
10260 * @return string
10261 */
10262 function get_premium_slug() {
10263 return ( is_object( $this->_plugin ) && ! empty( $this->_plugin->premium_slug ) ) ?
10264 $this->_plugin->premium_slug :
10265 "{$this->_slug}-premium";
10266 }
10267
10268 /**
10269 * Retrieve the desired folder name for the product.
10270 *
10271 * @author Vova Feldman (@svovaf)
10272 * @since 1.2.1.7
10273 *
10274 * @return string Plugin slug.
10275 */
10276 function get_target_folder_name() {
10277 return $this->can_use_premium_code() ?
10278 $this->_plugin->premium_slug :
10279 $this->_slug;
10280 }
10281
10282 /**
10283 * @author Vova Feldman (@svovaf)
10284 * @since 1.0.1
10285 *
10286 * @return number Plugin ID.
10287 */
10288 function get_id() {
10289 return $this->_plugin->id;
10290 }
10291
10292 /**
10293 * @author Leo Fajardo (@leorw)
10294 * @since 2.2.4
10295 *
10296 * @return number|null Bundle ID.
10297 */
10298 function get_bundle_id() {
10299 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
10300 $this->_plugin->bundle_id :
10301 null;
10302 }
10303
10304 /**
10305 * @author Vova Feldman (@svovaf)
10306 * @since 2.3.1
10307 *
10308 * @return string|null Bundle public key.
10309 */
10310 function get_bundle_public_key() {
10311 return isset( $this->_plugin->bundle_public_key ) ?
10312 $this->_plugin->bundle_public_key :
10313 null;
10314 }
10315
10316 /**
10317 * Get whether the SDK has been initiated in the context of a Bundle.
10318 *
10319 * This will return true, if `bundle_id` is present in the SDK init parameters.
10320 *
10321 * ```php
10322 * $my_fs = fs_dynamic_init( array(
10323 * // ...
10324 * 'bundle_id' => 'XXXX', // Will return true since we have bundle id.
10325 * 'bundle_public_key' => 'pk_XXXX',
10326 * ) );
10327 * ```
10328 *
10329 * @author Swashata Ghosh (@swashata)
10330 * @since 2.5.0
10331 *
10332 * @return bool True if we are running in bundle context, false otherwise.
10333 */
10334 private function has_bundle_context() {
10335 return ! is_null( $this->get_bundle_id() );
10336 }
10337
10338 /**
10339 * @author Vova Feldman (@svovaf)
10340 * @since 1.2.1.5
10341 *
10342 * @return string Freemius SDK version
10343 */
10344 function get_sdk_version() {
10345 return $this->version;
10346 }
10347
10348 /**
10349 * @author Vova Feldman (@svovaf)
10350 * @since 1.2.1.5
10351 *
10352 * @return number Parent plugin ID (if parent exist).
10353 */
10354 function get_parent_id() {
10355 return $this->is_addon() ?
10356 $this->get_parent_instance()->get_id() :
10357 $this->_plugin->id;
10358 }
10359
10360 /**
10361 * @author Vova Feldman (@svovaf)
10362 * @since 2.3.1
10363 *
10364 * @return string
10365 */
10366 function get_usage_tracking_terms_url() {
10367 return $this->apply_filters(
10368 'usage_tracking_terms_url',
10369 "https://freemius.com/product/opt-in/{$this->_plugin->id}/{$this->_slug}/"
10370 );
10371 }
10372
10373 /**
10374 * @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.
10375 * @since 2.5.8
10376 *
10377 * @return string
10378 */
10379 function get_license_activation_terms_url() {
10380 return $this->apply_filters(
10381 'license_activation_terms_url',
10382 "https://freemius.com/product/license-activation/{$this->_plugin->id}/{$this->_slug}/"
10383 );
10384 }
10385
10386 /**
10387 * @author Vova Feldman (@svovaf)
10388 * @since 2.3.1
10389 *
10390 * @return string
10391 */
10392 function get_eula_url() {
10393 return $this->apply_filters(
10394 'eula_url',
10395 "https://freemius.com/product/{$this->_plugin->id}/{$this->_slug}/legal/eula/"
10396 );
10397 }
10398
10399 /**
10400 * @author Vova Feldman (@svovaf)
10401 * @since 1.0.1
10402 *
10403 * @return string Plugin public key.
10404 */
10405 function get_public_key() {
10406 return $this->_plugin->public_key;
10407 }
10408
10409 /**
10410 * Will be available only on sandbox mode.
10411 *
10412 * @author Vova Feldman (@svovaf)
10413 * @since 1.0.4
10414 *
10415 * @return mixed Plugin secret key.
10416 */
10417 function get_secret_key() {
10418 return $this->_plugin->secret_key;
10419 }
10420
10421 /**
10422 * @author Vova Feldman (@svovaf)
10423 * @since 1.1.1
10424 *
10425 * @return bool
10426 */
10427 function has_secret_key() {
10428 return ! empty( $this->_plugin->secret_key );
10429 }
10430
10431 /**
10432 * @author Vova Feldman (@svovaf)
10433 * @since 1.0.9
10434 *
10435 * @param string|bool $premium_suffix
10436 *
10437 * @return string
10438 */
10439 function get_plugin_name( $premium_suffix = false ) {
10440 $this->_logger->entrance();
10441
10442 /**
10443 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10444 *
10445 * @author Vova Feldman
10446 */
10447 if ( ! isset( $this->_plugin_name ) ) {
10448 // Name is not yet set.
10449 $this->set_name( $premium_suffix );
10450 } else if (
10451 ! empty( $premium_suffix ) &&
10452 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10453 ) {
10454 // Name is already set, but there's a change in the premium suffix.
10455 $this->set_name( $premium_suffix );
10456 }
10457
10458 return $this->_plugin_name;
10459 }
10460
10461 /**
10462 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10463 *
10464 * @author Vova Feldman (@svovaf)
10465 * @since 2.2.1
10466 *
10467 * @param string $premium_suffix
10468 */
10469 private function set_name( $premium_suffix = '' ) {
10470 $plugin_data = $this->get_plugin_data();
10471
10472 // Get name.
10473 $this->_plugin_name = $plugin_data['Name'];
10474
10475 if ( is_string( $premium_suffix ) ) {
10476 $premium_suffix = trim( $premium_suffix );
10477
10478 if ( ! empty( $premium_suffix ) ) {
10479 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10480 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10481 $suffix_len = strlen( $suffix );
10482
10483 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10484 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10485 ) {
10486 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10487 }
10488 }
10489 }
10490
10491 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10492 }
10493
10494 /**
10495 * @author Vova Feldman (@svovaf)
10496 * @since 1.0.0
10497 *
10498 * @param bool $reparse_plugin_metadata
10499 *
10500 * @return string
10501 */
10502 function get_plugin_version( $reparse_plugin_metadata = false ) {
10503 $this->_logger->entrance();
10504
10505 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10506
10507 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10508
10509 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10510 }
10511
10512 /**
10513 * @author Vova Feldman (@svovaf)
10514 * @since 1.2.1.7
10515 *
10516 * @return string
10517 */
10518 function get_plugin_title() {
10519 $this->_logger->entrance();
10520
10521 $title = $this->_plugin->title;
10522
10523 return $this->apply_filters( 'plugin_title', $title );
10524 }
10525
10526 /**
10527 * @author Vova Feldman (@svovaf)
10528 * @since 1.2.2.7
10529 *
10530 * @param bool $lowercase
10531 *
10532 * @return string
10533 */
10534 function get_module_label( $lowercase = false ) {
10535 $label = $this->is_addon() ?
10536 $this->get_text_inline( 'Add-On', 'addon' ) :
10537 ( $this->is_plugin() ?
10538 $this->get_text_inline( 'Plugin', 'plugin' ) :
10539 $this->get_text_inline( 'Theme', 'theme' ) );
10540
10541 if ( $lowercase ) {
10542 $label = strtolower( $label );
10543 }
10544
10545 return $label;
10546 }
10547
10548 /**
10549 * @author Vova Feldman (@svovaf)
10550 * @since 1.0.4
10551 *
10552 * @return string
10553 */
10554 function get_plugin_basename() {
10555 if ( ! isset( $this->_plugin_basename ) ) {
10556 if ( $this->is_plugin() ) {
10557 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10558 } else {
10559 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10560 }
10561 }
10562
10563 return $this->_plugin_basename;
10564 }
10565
10566 function get_plugin_folder_name() {
10567 $this->_logger->entrance();
10568
10569 $plugin_folder = $this->_plugin_basename;
10570
10571 while ( '.' !== dirname( $plugin_folder ) ) {
10572 $plugin_folder = dirname( $plugin_folder );
10573 }
10574
10575 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10576
10577 return $plugin_folder;
10578 }
10579
10580 #endregion ------------------------------------------------------------------
10581
10582 /* Account
10583 ------------------------------------------------------------------------------------------------------------------*/
10584
10585 /**
10586 * Find plugin's slug by plugin's basename.
10587 *
10588 * @author Vova Feldman (@svovaf)
10589 * @since 1.0.9
10590 *
10591 * @param string $plugin_base_name
10592 *
10593 * @return false|string
10594 */
10595 private static function find_slug_by_basename( $plugin_base_name ) {
10596 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10597
10598 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10599 return false;
10600 }
10601
10602 return $file_slug_map[ $plugin_base_name ];
10603 }
10604
10605 /**
10606 * Store the map between the plugin's basename to the slug.
10607 *
10608 * @author Vova Feldman (@svovaf)
10609 * @since 1.0.9
10610 */
10611 private function store_file_slug_map() {
10612 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10613
10614 if ( ! array( $file_slug_map ) ) {
10615 $file_slug_map = array();
10616 }
10617
10618 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10619 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10620 ) {
10621 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10622 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10623 }
10624 }
10625
10626 /**
10627 * @return array[number]FS_User
10628 */
10629 static function get_all_users() {
10630 $users = self::maybe_get_entities_account_option( 'users', array() );
10631
10632 if ( ! is_array( $users ) ) {
10633 $users = array();
10634 }
10635
10636 return $users;
10637 }
10638
10639 /**
10640 * @param string $module_type
10641 * @param null|int $blog_id Since 2.0.0
10642 *
10643 * @return array[string]FS_Site
10644 */
10645 private static function get_all_sites(
10646 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10647 $blog_id = null,
10648 $is_backup = false
10649 ) {
10650 $sites = self::get_account_option(
10651 ( $is_backup ? 'prev_' : '' ) . 'sites',
10652 $module_type,
10653 $blog_id
10654 );
10655
10656 if ( ! is_array( $sites ) ) {
10657 $sites = array();
10658 }
10659
10660 return $sites;
10661 }
10662
10663 /**
10664 * @author Leo Fajardo (@leorw)
10665 *
10666 * @since 1.2.2
10667 *
10668 * @param string $option_name
10669 * @param string $module_type
10670 * @param null|int $network_level_or_blog_id Since 2.0.0
10671 *
10672 * @return mixed
10673 */
10674 private static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10675 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10676 $option_name = $module_type . '_' . $option_name;
10677 }
10678
10679 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10680 }
10681
10682 /**
10683 * @author Leo Fajardo (@leorw)
10684 *
10685 * @since 1.2.2
10686 *
10687 * @param string $option_name
10688 * @param mixed $option_value
10689 * @param bool $store
10690 * @param null|int $network_level_or_blog_id Since 2.0.0
10691 */
10692 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10693 self::set_account_option_by_module(
10694 $this->_module_type,
10695 $option_name,
10696 $option_value,
10697 $store,
10698 $network_level_or_blog_id
10699 );
10700 }
10701
10702 /**
10703 * @author Vova Feldman (@svovaf)
10704 *
10705 * @since 1.2.2.7
10706 *
10707 * @param string $module_type
10708 * @param string $option_name
10709 * @param mixed $option_value
10710 * @param bool $store
10711 * @param null|int $network_level_or_blog_id Since 2.0.0
10712 */
10713 private static function set_account_option_by_module(
10714 $module_type,
10715 $option_name,
10716 $option_value,
10717 $store,
10718 $network_level_or_blog_id = null
10719 ) {
10720 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10721 $option_name = $module_type . '_' . $option_name;
10722 }
10723
10724 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10725 }
10726
10727 /**
10728 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10729 *
10730 * @author Leo Fajardo (@leorw)
10731 * @since 2.3.1
10732 *
10733 * @param string $option_name
10734 * @param mixed $default
10735 * @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).
10736 *
10737 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10738 */
10739 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10740 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10741
10742 $class_name = '';
10743
10744 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10745 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10746 }
10747
10748 switch ( $option_name ) {
10749 case 'plugins':
10750 case 'themes':
10751 case 'addons':
10752 $class_name = FS_Plugin::get_class_name();
10753 break;
10754 case 'users':
10755 $class_name = FS_User::get_class_name();
10756 break;
10757 case 'sites':
10758 $class_name = FS_Site::get_class_name();
10759 break;
10760 case 'licenses':
10761 case 'all_licenses':
10762 $class_name = FS_Plugin_License::get_class_name();
10763 break;
10764 case 'plans':
10765 $class_name = FS_Plugin_Plan::get_class_name();
10766 break;
10767 case 'updates':
10768 $class_name = FS_Plugin_Tag::get_class_name();
10769 break;
10770 }
10771
10772 if ( empty( $class_name ) ) {
10773 return $option;
10774 }
10775
10776 return fs_get_entities( $option, $class_name );
10777 }
10778
10779 /**
10780 * @author Vova Feldman (@svovaf)
10781 * @since 1.0.6
10782 *
10783 * @param number|null $module_id
10784 *
10785 * @return FS_Plugin_License[]
10786 */
10787 private static function get_all_licenses( $module_id = null ) {
10788 $licenses = self::get_account_option( 'all_licenses' );
10789
10790 if ( ! is_array( $licenses ) ) {
10791 $licenses = array();
10792 }
10793
10794 if ( is_null( $module_id ) ) {
10795 return $licenses;
10796 }
10797
10798 $licenses = isset( $licenses[ $module_id ] ) ?
10799 $licenses[ $module_id ] :
10800 array();
10801
10802 return $licenses;
10803 }
10804
10805 /**
10806 * @author Leo Fajardo (@leorw)
10807 * @since 2.0.0
10808 *
10809 * @return array
10810 */
10811 private static function get_all_licenses_by_module_type() {
10812 $licenses = self::get_account_option( 'all_licenses' );
10813
10814 $licenses_by_module_type = array(
10815 WP_FS__MODULE_TYPE_PLUGIN => array(),
10816 WP_FS__MODULE_TYPE_THEME => array()
10817 );
10818
10819 if ( ! is_array( $licenses ) ) {
10820 return $licenses_by_module_type;
10821 }
10822
10823 foreach ( $licenses as $module_id => $module_licenses ) {
10824 $fs = self::get_instance_by_id( $module_id );
10825 if ( false === $fs ) {
10826 continue;
10827 }
10828
10829 $licenses_by_module_type[ $fs->_module_type ] = array_merge( $licenses_by_module_type[ $fs->_module_type ], $module_licenses );
10830 }
10831
10832 return $licenses_by_module_type;
10833 }
10834
10835 /**
10836 * @author Leo Fajardo (@leorw)
10837 * @since 2.0.0
10838 *
10839 * @param number $module_id
10840 * @param number|null $user_id
10841 *
10842 * @return array
10843 */
10844 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10845 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10846
10847 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10848 $all_modules_user_id_license_ids_map = array();
10849 }
10850
10851 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10852 $all_modules_user_id_license_ids_map[ $module_id ] :
10853 array();
10854
10855 if ( FS_User::is_valid_id( $user_id ) ) {
10856 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10857 $user_id_license_ids_map[ $user_id ] :
10858 array();
10859 }
10860
10861 return $user_id_license_ids_map;
10862 }
10863
10864 /**
10865 * @author Leo Fajardo (@leorw)
10866 * @since 2.0.0
10867 *
10868 * @param array $new_user_id_license_ids_map
10869 * @param number $module_id
10870 * @param number|null $user_id
10871 */
10872 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10873 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10874 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10875 $all_modules_user_id_license_ids_map = array();
10876 }
10877
10878 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10879 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10880 }
10881
10882 if ( FS_User::is_valid_id( $user_id ) ) {
10883 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10884 } else {
10885 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10886 }
10887
10888 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10889 }
10890
10891 /**
10892 * Get a collection of the user's linked license IDs.
10893 *
10894 * @author Vova Feldman (@svovaf)
10895 * @since 2.0.0
10896 *
10897 * @param number $user_id
10898 *
10899 * @return number[]
10900 */
10901 private function get_user_linked_license_ids( $user_id ) {
10902 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10903 }
10904
10905 /**
10906 * Override the user's linked license IDs with a new IDs collection.
10907 *
10908 * @author Vova Feldman (@svovaf)
10909 * @since 2.0.0
10910 *
10911 * @param number $user_id
10912 * @param number[] $license_ids
10913 */
10914 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10915 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10916 }
10917
10918 /**
10919 * Link a specified license ID to a given user.
10920 *
10921 * @author Vova Feldman (@svovaf)
10922 * @since 2.0.0
10923 *
10924 * @param number $license_id
10925 * @param number $user_id
10926 */
10927 private function link_license_2_user( $license_id, $user_id ) {
10928 $license_ids = $this->get_user_linked_license_ids( $user_id );
10929
10930 if ( in_array( $license_id, $license_ids ) ) {
10931 // License already linked.
10932 return;
10933 }
10934
10935 $license_ids[] = $license_id;
10936
10937 $this->set_user_linked_license_ids( $user_id, $license_ids );
10938 }
10939
10940 /**
10941 * @param string|bool $module_type
10942 *
10943 * @return FS_Plugin_Plan[]
10944 */
10945 private static function get_all_plans( $module_type = false ) {
10946 $plans = self::get_account_option( 'plans', $module_type );
10947
10948 if ( ! is_array( $plans ) ) {
10949 $plans = array();
10950 }
10951
10952 return $plans;
10953 }
10954
10955 /**
10956 * @author Vova Feldman (@svovaf)
10957 * @since 1.0.4
10958 *
10959 * @return FS_Plugin_Tag[]
10960 */
10961 private static function get_all_updates() {
10962 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10963
10964 if ( ! is_array( $updates ) ) {
10965 $updates = array();
10966 }
10967
10968 return $updates;
10969 }
10970
10971 /**
10972 * @author Vova Feldman (@svovaf)
10973 * @since 1.0.6
10974 *
10975 * @return array<number,FS_Plugin[]>|false
10976 */
10977 private static function get_all_addons() {
10978 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10979
10980 if ( ! is_array( $addons ) ) {
10981 $addons = array();
10982 }
10983
10984 return $addons;
10985 }
10986
10987 /**
10988 * @author Vova Feldman (@svovaf)
10989 * @since 1.0.6
10990 *
10991 * @return number[]|false
10992 */
10993 private static function get_all_account_addons() {
10994 $addons = self::$_accounts->get_option( 'account_addons', array() );
10995
10996 if ( ! is_array( $addons ) ) {
10997 $addons = array();
10998 }
10999
11000 return $addons;
11001 }
11002
11003 /**
11004 * Check if user has connected his account (opted-in).
11005 *
11006 * Note:
11007 * If the user opted-in and opted-out on a later stage,
11008 * this will still return true. If you want to check if the
11009 * user is currently opted-in, use:
11010 * `$fs->is_registered() && $fs->is_tracking_allowed()`
11011 *
11012 * @author Vova Feldman (@svovaf)
11013 * @since 1.0.1
11014 *
11015 * @param bool $ignore_anonymous_state Since 2.5.1
11016 *
11017 * @return bool
11018 */
11019 function is_registered( $ignore_anonymous_state = false ) {
11020 return (
11021 is_object( $this->_user ) &&
11022 (
11023 $this->is_premium() ||
11024 $ignore_anonymous_state ||
11025 ! $this->is_anonymous()
11026 )
11027 );
11028 }
11029
11030 /**
11031 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
11032 *
11033 * @author Leo Fajardo (@leorw)
11034 * @since 1.2.1.5
11035 *
11036 * @return bool
11037 */
11038 function is_tracking_allowed( $blog_id = null, $install = null ) {
11039 if ( is_null( $install ) ) {
11040 $install = is_null( $blog_id ) ?
11041 $this->_site :
11042 $this->get_install_by_blog_id( $blog_id );
11043 }
11044
11045 return (
11046 is_object( $install ) &&
11047 FS_Permission_Manager::instance( $this )->is_homepage_url_tracking_allowed( $blog_id )
11048 );
11049 }
11050
11051 /**
11052 * Returns TRUE if the user never opted-in or manually opted-out.
11053 *
11054 * @author Vova Feldman (@svovaf)
11055 * @since 1.2.1.5
11056 *
11057 * @param int|null $blog_id
11058 *
11059 * @return bool
11060 */
11061 function is_tracking_prohibited( $blog_id = null ) {
11062 return (
11063 ! $this->is_registered( true ) ||
11064 ! $this->is_tracking_allowed( $blog_id )
11065 );
11066 }
11067
11068 /**
11069 * @author Leo Fajardo (@leorw)
11070 * @since 2.4.0
11071 *
11072 * @return bool
11073 */
11074 function is_bundle_license_auto_activation_enabled() {
11075 return $this->is_addon() ?
11076 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
11077 $this->_is_bundle_license_auto_activation_enabled;
11078 }
11079
11080 /**
11081 * @author Vova Feldman (@svovaf)
11082 * @since 1.0.4
11083 *
11084 * @return FS_Plugin
11085 */
11086 function get_plugin() {
11087 return $this->_plugin;
11088 }
11089
11090 /**
11091 * @author Vova Feldman (@svovaf)
11092 * @since 1.0.3
11093 *
11094 * @return FS_User
11095 */
11096 function get_user() {
11097 return $this->_user;
11098 }
11099
11100 /**
11101 * @author Vova Feldman (@svovaf)
11102 * @since 1.0.3
11103 *
11104 * @return FS_Site
11105 */
11106 function get_site() {
11107 return $this->_site;
11108 }
11109
11110 /**
11111 * @author Leo Fajardo (@leorw)
11112 * @since 2.5.0
11113 */
11114 function store_site( $site ) {
11115 $this->_site = $site;
11116 $this->_store_site( true );
11117 }
11118
11119 /**
11120 * 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).
11121 *
11122 * @author Leo Fajardo (@leorw)
11123 * @since 2.5.0
11124 */
11125 function delete_current_install( $back_up ) {
11126 // Back up and delete the unique ID.
11127 if ( $back_up ) {
11128 self::$_accounts->set_option( 'prev_unique_id', $this->get_anonymous_id() );
11129 }
11130
11131 self::$_accounts->set_option( 'unique_id', null );
11132
11133 if ( $back_up ) {
11134 // 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).
11135 $this->back_up_site();
11136 }
11137
11138 $this->_delete_site();
11139 $this->_site = null;
11140 }
11141
11142 /**
11143 * @author Leo Fajardo (@leorw)
11144 * @since 2.5.0
11145 */
11146 function restore_backup_site() {
11147 self::$_accounts->set_option(
11148 'unique_id',
11149 self::$_accounts->get_option( 'prev_unique_id' )
11150 );
11151
11152 $sites = self::get_all_sites( $this->_module_type, null, true );
11153 $this->store_site( clone $sites[ $this->_slug ] );
11154 }
11155
11156 /**
11157 * Get plugin add-ons.
11158 *
11159 * @author Vova Feldman (@svovaf)
11160 * @since 1.0.6
11161 *
11162 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
11163 *
11164 * @param bool $flush
11165 *
11166 * @return FS_Plugin[]|false
11167 */
11168 function get_addons( $flush = false ) {
11169 $this->_logger->entrance();
11170
11171 if ( ! $this->_has_addons ) {
11172 return false;
11173 }
11174
11175 $addons = $this->sync_addons( $flush );
11176
11177 return ( ! is_array( $addons ) || empty( $addons ) ) ?
11178 false :
11179 $addons;
11180 }
11181
11182 /**
11183 * @author Vova Feldman (@svovaf)
11184 * @since 1.0.6
11185 *
11186 * @return number[]|false
11187 */
11188 function get_account_addons() {
11189 $this->_logger->entrance();
11190
11191 $addons = self::get_all_account_addons();
11192
11193 if ( ! is_array( $addons ) ||
11194 ! isset( $addons[ $this->_plugin->id ] ) ||
11195 ! is_array( $addons[ $this->_plugin->id ] ) ||
11196 0 === count( $addons[ $this->_plugin->id ] )
11197 ) {
11198 return false;
11199 }
11200
11201 return $addons[ $this->_plugin->id ];
11202 }
11203
11204 /**
11205 * Check if user has any
11206 *
11207 * @author Vova Feldman (@svovaf)
11208 * @since 1.1.6
11209 *
11210 * @return bool
11211 */
11212 function has_account_addons() {
11213 $addons = $this->get_account_addons();
11214
11215 return is_array( $addons ) && ( 0 < count( $addons ) );
11216 }
11217
11218
11219 /**
11220 * Get add-on by ID (from local data).
11221 *
11222 * @author Vova Feldman (@svovaf)
11223 * @since 1.0.6
11224 *
11225 * @param number $id
11226 *
11227 * @return FS_Plugin|false
11228 */
11229 function get_addon( $id ) {
11230 $this->_logger->entrance();
11231
11232 $addons = $this->get_addons();
11233
11234 if ( is_array( $addons ) ) {
11235 foreach ( $addons as $addon ) {
11236 if ( $id == $addon->id ) {
11237 return $addon;
11238 }
11239 }
11240 }
11241
11242 return false;
11243 }
11244
11245 /**
11246 * Get add-on by slug (from local data).
11247 *
11248 * @author Vova Feldman (@svovaf)
11249 * @since 1.0.6
11250 *
11251 * @param string $slug
11252 *
11253 * @param bool $flush
11254 *
11255 * @return FS_Plugin|false
11256 */
11257 function get_addon_by_slug( $slug, $flush = false ) {
11258 $this->_logger->entrance();
11259
11260 $addons = $this->get_addons( $flush );
11261
11262 if ( is_array( $addons ) ) {
11263 foreach ( $addons as $addon ) {
11264 if ( $slug === $addon->slug ) {
11265 return $addon;
11266 }
11267 }
11268 }
11269
11270 return false;
11271 }
11272
11273 /**
11274 * @var array<number,object[]> {
11275 * @key number Add-on ID.
11276 * @val object[] The add-on's plans and prices object.
11277 * }
11278 */
11279 private $plans_and_pricing_by_addon_id;
11280
11281 /**
11282 * @author Leo Fajardo (@leorw)
11283 * @since 2.3.0
11284 *
11285 * @return array<number,object[]> {
11286 * @key number Add-on ID.
11287 * @val object[] The add-on's plans and prices object.
11288 * }
11289 */
11290 function _get_addons_plans_and_pricing_map_by_id() {
11291 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
11292 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
11293
11294 $plans_and_pricing_by_addon_id = array();
11295 if ( $this->is_api_result_object( $result, 'addons' ) ) {
11296 foreach ( $result->addons as $addon ) {
11297 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
11298 }
11299 }
11300
11301 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
11302 }
11303
11304 return $this->plans_and_pricing_by_addon_id;
11305 }
11306
11307 /**
11308 * @author Leo Fajardo (@leorw)
11309 * @since 2.3.0
11310 *
11311 * @param number $addon_id
11312 * @param bool $is_installed
11313 *
11314 * @return array
11315 */
11316 function _get_addon_info( $addon_id, $is_installed ) {
11317 $addon = $this->get_addon( $addon_id );
11318
11319 if ( ! is_object( $addon ) ) {
11320 // Unexpected call.
11321 return array();
11322 }
11323
11324 $slug = $addon->slug;
11325
11326 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
11327
11328 if ( ! fs_is_network_admin() ) {
11329 // Get blog-level activated installations.
11330 $sites = self::maybe_get_entities_account_option( 'sites', array() );
11331 } else {
11332 $sites = null;
11333
11334 if ( $this->is_addon_activated( $addon_id ) &&
11335 $this->get_addon_instance( $addon_id )->is_network_active()
11336 ) {
11337 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
11338 // Get network-level activated installations.
11339 $sites = self::maybe_get_entities_account_option(
11340 'sites',
11341 array(),
11342 $addon_storage->network_install_blog_id
11343 );
11344 }
11345 }
11346 }
11347
11348 $addon_info = array(
11349 'is_connected' => false,
11350 'slug' => $slug,
11351 'title' => $addon->title,
11352 'is_whitelabeled' => $addon_storage->is_whitelabeled
11353 );
11354
11355 if ( ! $is_installed ) {
11356 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
11357
11358 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
11359 $has_paid_plan = false;
11360 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
11361
11362 if ( is_array( $plans ) && count( $plans ) > 0 ) {
11363 foreach ( $plans as $plan ) {
11364 if ( isset( $plan->pricing ) &&
11365 is_array( $plan->pricing ) &&
11366 count( $plan->pricing ) > 0
11367 ) {
11368 $has_paid_plan = true;
11369 break;
11370 }
11371 }
11372 }
11373
11374 $addon_info['has_paid_plan'] = $has_paid_plan;
11375 }
11376 }
11377
11378 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
11379 return $addon_info;
11380 }
11381
11382 $site = $sites[ $slug ];
11383
11384 $addon_info['is_connected'] = (
11385 ( $addon->parent_plugin_id == $this->get_id() ) &&
11386 is_object( $site ) &&
11387 FS_Site::is_valid_id( $site->id ) &&
11388 FS_User::is_valid_id( $site->user_id ) &&
11389 FS_Plugin_Plan::is_valid_id( $site->plan_id )
11390 );
11391
11392 if ( $addon_info['is_connected'] && $is_installed ) {
11393 return $addon_info;
11394 }
11395
11396 $addon_info['site'] = $site;
11397
11398 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
11399 if ( isset( $plugins_data[ $slug ] ) ) {
11400 $plugin_data = $plugins_data[ $slug ];
11401
11402 $addon_info['version'] = $plugin_data->version;
11403 }
11404
11405 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
11406 if ( isset( $all_plans[ $slug ] ) ) {
11407 $plans = $all_plans[ $slug ];
11408
11409 foreach ( $plans as $plan ) {
11410 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
11411 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
11412 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
11413 break;
11414 }
11415 }
11416 }
11417
11418 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
11419 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
11420 foreach ( $licenses[ $addon_id ] as $license ) {
11421 if ( $license->id == $site->license_id ) {
11422 $addon_info['license'] = $license;
11423 break;
11424 }
11425 }
11426 }
11427
11428 if ( isset( $addon_info['license'] ) ) {
11429 if ( isset( $addon_storage->subscriptions ) &&
11430 ! empty( $addon_storage->subscriptions )
11431 ) {
11432 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11433
11434 foreach ( $addon_subscriptions as $subscription ) {
11435 if ( $subscription->license_id == $site->license_id ) {
11436 $addon_info['subscription'] = $subscription;
11437 break;
11438 }
11439 }
11440 }
11441 }
11442
11443 return $addon_info;
11444 }
11445
11446 /**
11447 * @author Vova Feldman (@svovaf)
11448 * @since 2.0.0
11449 *
11450 * @param number $user_id
11451 *
11452 * @return FS_User
11453 */
11454 static function _get_user_by_id( $user_id ) {
11455 self::$_static_logger->entrance( "user_id = {$user_id}" );
11456
11457 $users = self::get_all_users();
11458
11459 if ( is_array( $users ) ) {
11460 if ( isset( $users[ $user_id ] ) &&
11461 $users[ $user_id ] instanceof FS_User &&
11462 $user_id == $users[ $user_id ]->id
11463 ) {
11464 return $users[ $user_id ];
11465 }
11466
11467 // If user wasn't found by the key, iterate over all the users collection.
11468 foreach ( $users as $user ) {
11469 /**
11470 * @var FS_User $user
11471 */
11472 if ( $user_id == $user->id ) {
11473 return $user;
11474 }
11475 }
11476 }
11477
11478 return null;
11479 }
11480
11481 /**
11482 * Checks if a Freemius user_id is associated with a super-admin.
11483 *
11484 * @author Vova Feldman (@svovaf)
11485 * @since 2.0.0
11486 *
11487 * @param number $user_id
11488 *
11489 * @return bool
11490 */
11491 private static function is_super_admin( $user_id ) {
11492 $is_super_admin = false;
11493
11494 $user = self::_get_user_by_id( $user_id );
11495
11496 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11497 self::require_pluggable_essentials();
11498
11499 $wp_user = get_user_by( 'email', $user->email );
11500
11501 if ( $wp_user instanceof WP_User ) {
11502 $super_admins = get_super_admins();
11503 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11504 }
11505 }
11506
11507 return $is_super_admin;
11508 }
11509
11510 #----------------------------------------------------------------------------------
11511 #region Plans & Licensing
11512 #----------------------------------------------------------------------------------
11513
11514 /**
11515 * Check if running premium plugin code.
11516 *
11517 * @author Vova Feldman (@svovaf)
11518 * @since 1.0.5
11519 *
11520 * @return bool
11521 */
11522 function is_premium() {
11523 /**
11524 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11525 * `register_constructor_hooks` method.
11526 *
11527 * @author Leo Fajardo (@leorw)
11528 * @since 2.2.3
11529 */
11530 return is_object( $this->_plugin ) ?
11531 $this->_plugin->is_premium :
11532 false;
11533 }
11534
11535 /**
11536 * Get site's plan ID.
11537 *
11538 * @author Vova Feldman (@svovaf)
11539 * @since 1.0.2
11540 *
11541 * @return number
11542 */
11543 function get_plan_id() {
11544 return $this->_site->plan_id;
11545 }
11546
11547 /**
11548 * Get site's plan title.
11549 *
11550 * @author Vova Feldman (@svovaf)
11551 * @since 1.0.2
11552 *
11553 * @return string
11554 */
11555 function get_plan_title() {
11556 $plan = $this->get_plan();
11557
11558 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11559 }
11560
11561 /**
11562 * Get site's plan name.
11563 *
11564 * @author Vova Feldman (@svovaf)
11565 * @since 2.0.0
11566 *
11567 * @return string
11568 */
11569 function get_plan_name() {
11570 $plan = $this->get_plan();
11571
11572 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11573 }
11574
11575 /**
11576 * @author Vova Feldman (@svovaf)
11577 * @since 1.0.9
11578 *
11579 * @return FS_Plugin_Plan|false
11580 */
11581 function get_plan() {
11582 if ( ! is_object( $this->_site ) ) {
11583 return false;
11584 }
11585
11586 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11587 $this->_get_plan_by_id( $this->_site->plan_id ) :
11588 false;
11589 }
11590
11591 /**
11592 * @author Vova Feldman (@svovaf)
11593 * @since 1.0.3
11594 *
11595 * @return bool
11596 */
11597 function is_trial() {
11598 $this->_logger->entrance();
11599
11600 if ( ! $this->is_registered( true ) || ! is_object( $this->_site ) ) {
11601 return false;
11602 }
11603
11604 return $this->_site->is_trial();
11605 }
11606
11607 /**
11608 * Check if currently in a trial with payment method (credit card or paypal).
11609 *
11610 * @author Vova Feldman (@svovaf)
11611 * @since 1.1.7
11612 *
11613 * @return bool
11614 */
11615 function is_paid_trial() {
11616 $this->_logger->entrance();
11617
11618 if ( ! $this->is_trial() ) {
11619 return false;
11620 }
11621
11622 if ( ! $this->has_active_valid_license() ) {
11623 return false;
11624 }
11625
11626 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11627 return false;
11628 }
11629
11630 /**
11631 * @var FS_Subscription $subscription
11632 */
11633 $subscription = $this->_get_subscription( $this->_license->id );
11634
11635 return ( is_object( $subscription ) && $subscription->is_active() );
11636 }
11637
11638 /**
11639 * Check if trial already utilized.
11640 *
11641 * @since 1.0.9
11642 *
11643 * @return bool
11644 */
11645 function is_trial_utilized() {
11646 $this->_logger->entrance();
11647
11648 if ( ! $this->is_registered() ) {
11649 return false;
11650 }
11651
11652 return $this->_site->is_trial_utilized();
11653 }
11654
11655 /**
11656 * Get trial plan information (if in trial).
11657 *
11658 * @author Vova Feldman (@svovaf)
11659 * @since 1.0.9
11660 *
11661 * @return bool|FS_Plugin_Plan
11662 */
11663 function get_trial_plan() {
11664 $this->_logger->entrance();
11665
11666 if ( ! $this->is_trial() ) {
11667 return false;
11668 }
11669
11670 // Try to load plan from local cache.
11671 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11672
11673 if ( ! is_object( $trial_plan ) ) {
11674 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11675
11676 /**
11677 * If managed to fetch the plan, add it to the plans collection.
11678 */
11679 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11680 if ( ! is_array( $this->_plans ) ) {
11681 $this->_plans = array();
11682 }
11683
11684 $this->_plans[] = $trial_plan;
11685 $this->_store_plans();
11686 }
11687 }
11688
11689 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11690 return $trial_plan;
11691 }
11692
11693 /**
11694 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11695 */
11696 $trial_plan = new FS_Plugin_Plan();
11697 $trial_plan->id = $this->_site->trial_plan_id;
11698 $trial_plan->name = 'pro';
11699 $trial_plan->title = 'Pro';
11700
11701 return $trial_plan;
11702 }
11703
11704 /**
11705 * Check if the user has an activate, non-expired license on current plugin's install.
11706 *
11707 * @since 1.0.9
11708 *
11709 * @return bool
11710 */
11711 function is_paying() {
11712 $this->_logger->entrance();
11713
11714 if ( ! $this->is_registered( true ) ) {
11715 return false;
11716 }
11717
11718 if ( ! $this->has_paid_plan() ) {
11719 return false;
11720 }
11721
11722 return (
11723 ! $this->is_trial() &&
11724 'free' !== $this->get_plan_name() &&
11725 $this->has_active_valid_license()
11726 );
11727 }
11728
11729 /**
11730 * @author Vova Feldman (@svovaf)
11731 * @since 1.0.4
11732 *
11733 * @return bool
11734 */
11735 function is_free_plan() {
11736 if ( ! $this->is_registered() ) {
11737 return true;
11738 }
11739
11740 if ( ! $this->has_paid_plan() ) {
11741 return true;
11742 }
11743
11744 return (
11745 'free' === $this->get_plan_name() ||
11746 ! $this->has_features_enabled_license()
11747 );
11748 }
11749
11750 /**
11751 * @author Vova Feldman (@svovaf)
11752 * @since 1.0.5
11753 *
11754 * @return bool
11755 */
11756 function _has_premium_license() {
11757 $this->_logger->entrance();
11758
11759 $premium_license = $this->_get_available_premium_license();
11760
11761 return ( false !== $premium_license );
11762 }
11763
11764 /**
11765 * Check if user has any licenses associated with the plugin (including expired or blocking).
11766 *
11767 * @author Vova Feldman (@svovaf)
11768 * @since 1.1.7.3
11769 *
11770 * @param bool $including_foreign
11771 *
11772 * @return bool
11773 */
11774 function has_any_license( $including_foreign = true ) {
11775 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11776 return false;
11777 }
11778
11779 if ( $including_foreign ) {
11780 return true;
11781 }
11782
11783 foreach ( $this->_licenses as $license ) {
11784 if ( $this->_user->id == $license->user_id ) {
11785 return true;
11786 }
11787 }
11788
11789 return false;
11790 }
11791
11792 /**
11793 * @author Vova Feldman (@svovaf)
11794 * @since 1.0.5
11795 *
11796 * @param bool|null $is_localhost
11797 *
11798 * @return FS_Plugin_License|false
11799 */
11800 function _get_available_premium_license( $is_localhost = null ) {
11801 $this->_logger->entrance();
11802
11803 $licenses = $this->get_available_premium_licenses( $is_localhost );
11804 if ( ! empty( $licenses ) ) {
11805 return $licenses[0];
11806 }
11807
11808 return false;
11809 }
11810
11811 /**
11812 * @author Vova Feldman (@svovaf)
11813 * @since 1.0.5
11814 *
11815 * @param bool|null $is_localhost
11816 *
11817 * @return FS_Plugin_License[]
11818 */
11819 function get_available_premium_licenses( $is_localhost = null ) {
11820 $this->_logger->entrance();
11821
11822 $licenses = array();
11823 if ( ! $this->has_paid_plan() ) {
11824 return $licenses;
11825 }
11826
11827 if ( is_array( $this->_licenses ) ) {
11828 foreach ( $this->_licenses as $license ) {
11829 if ( ! $license->can_activate( $is_localhost ) ) {
11830 continue;
11831 }
11832
11833 $licenses[] = $license;
11834 }
11835 }
11836
11837 return $licenses;
11838 }
11839
11840 /**
11841 * Sync local plugin plans with remote server.
11842 *
11843 * 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).
11844 *
11845 * @author Vova Feldman (@svovaf)
11846 * @since 1.0.5
11847 *
11848 * @return FS_Plugin_Plan[]|object
11849 */
11850 function _sync_plans() {
11851 $plans = $this->_fetch_plugin_plans();
11852
11853 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11854 $plans_map = array();
11855 foreach ( $plans as $plan ) {
11856 $plans_map[ $plan->id ] = true;
11857 }
11858
11859 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11860
11861 foreach ( $plans_ids_to_keep as $plan_id ) {
11862 if ( isset( $plans_map[ $plan_id ] ) ) {
11863 continue;
11864 }
11865
11866 $missing_plan = self::_get_plan_by_id( $plan_id );
11867
11868 if ( is_object( $missing_plan ) ) {
11869 $plans[] = $missing_plan;
11870 }
11871 }
11872
11873 $this->_plans = $plans;
11874 $this->_store_plans();
11875 }
11876
11877 $this->do_action( 'after_plans_sync', $plans );
11878
11879 return $this->_plans;
11880 }
11881
11882 /**
11883 * Check if specified plan exists locally. If not, fetch it and store it.
11884 *
11885 * @author Vova Feldman (@svovaf)
11886 * @since 2.0.0
11887 *
11888 * @param number $plan_id
11889 *
11890 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11891 */
11892 private function sync_plan_if_not_exist( $plan_id ) {
11893 $plan = self::_get_plan_by_id( $plan_id );
11894
11895 if ( is_object( $plan ) ) {
11896 // Plan already exists.
11897 return $plan;
11898 }
11899
11900 $plan = $this->fetch_plan_by_id( $plan_id );
11901
11902 if ( $plan instanceof FS_Plugin_Plan ) {
11903 $this->_plans[] = $plan;
11904 $this->_store_plans();
11905
11906 return $plan;
11907 }
11908
11909 return $plan;
11910 }
11911
11912 /**
11913 * Check if specified license exists locally. If not, fetch it and store it.
11914 *
11915 * @author Vova Feldman (@svovaf)
11916 * @since 2.0.0
11917 *
11918 * @param number $license_id
11919 * @param string $license_key
11920 *
11921 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11922 */
11923 private function sync_license_if_not_exist( $license_id, $license_key ) {
11924 $license = $this->_get_license_by_id( $license_id );
11925
11926 if ( is_object( $license ) ) {
11927 // License already exists.
11928 return $license;
11929 }
11930
11931 $license = $this->fetch_license_by_key( $license_id, $license_key );
11932
11933 if ( $license instanceof FS_Plugin_License ) {
11934 $this->_licenses[] = $license;
11935
11936 $this->set_license( $license );
11937
11938 $this->_store_licenses();
11939
11940 return $license;
11941 }
11942
11943 return $license;
11944 }
11945
11946 /**
11947 * Get a collection of unique plan IDs that are associated with any installs in the network.
11948 *
11949 * @author Leo Fajardo (@leorw)
11950 * @since 2.0.0
11951 *
11952 * @return number[]
11953 */
11954 private function get_plans_ids_associated_with_installs() {
11955 if ( ! is_multisite() ) {
11956 if ( ! is_object( $this->_site ) ||
11957 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11958 ) {
11959 return array();
11960 }
11961
11962 return array( $this->_site->plan_id );
11963 }
11964
11965 $plan_ids = array();
11966 $sites = self::get_sites();
11967 foreach ( $sites as $site ) {
11968 $blog_id = self::get_site_blog_id( $site );
11969 $install = $this->get_install_by_blog_id( $blog_id );
11970
11971 if ( ! is_object( $install ) ||
11972 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11973 ) {
11974 continue;
11975 }
11976
11977 $plan_ids[ $install->plan_id ] = true;
11978 }
11979
11980 return array_keys( $plan_ids );
11981 }
11982
11983 /**
11984 * Get a collection of unique license IDs that are associated with any installs in the network.
11985 *
11986 * @author Leo Fajardo (@leorw)
11987 * @since 2.0.0
11988 *
11989 * @return number[]
11990 */
11991 private function get_license_ids_associated_with_installs() {
11992 if ( ! $this->_is_network_active ) {
11993 if ( ! is_object( $this->_site ) ||
11994 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11995 ) {
11996 return array();
11997 }
11998
11999 return array( $this->_site->license_id );
12000 }
12001
12002 $license_ids = array();
12003 $sites = self::get_sites();
12004 foreach ( $sites as $site ) {
12005 $blog_id = self::get_site_blog_id( $site );
12006 $install = $this->get_install_by_blog_id( $blog_id );
12007
12008 if ( ! is_object( $install ) ||
12009 ! FS_Plugin_License::is_valid_id( $install->license_id )
12010 ) {
12011 continue;
12012 }
12013
12014 $license_ids[ $install->license_id ] = true;
12015 }
12016
12017 return array_keys( $license_ids );
12018 }
12019
12020 /**
12021 * @author Vova Feldman (@svovaf)
12022 * @since 1.0.5
12023 *
12024 * @param number $id
12025 *
12026 * @return FS_Plugin_Plan|false
12027 */
12028 function _get_plan_by_id( $id ) {
12029 $this->_logger->entrance();
12030
12031 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
12032 $this->_sync_plans();
12033 }
12034
12035 foreach ( $this->_plans as $plan ) {
12036 if ( $id == $plan->id ) {
12037 return $plan;
12038 }
12039 }
12040
12041 return false;
12042 }
12043
12044 /**
12045 * @author Vova Feldman (@svovaf)
12046 * @since 1.1.8.1
12047 *
12048 * @param string $name
12049 *
12050 * @return FS_Plugin_Plan|false
12051 */
12052 private function get_plan_by_name( $name ) {
12053 $this->_logger->entrance();
12054
12055 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
12056 $this->_sync_plans();
12057 }
12058
12059 foreach ( $this->_plans as $plan ) {
12060 if ( $name == $plan->name ) {
12061 return $plan;
12062 }
12063 }
12064
12065 return false;
12066 }
12067
12068 /**
12069 * Sync local licenses with remote server.
12070 *
12071 * @author Vova Feldman (@svovaf)
12072 * @since 1.0.6
12073 *
12074 * @param number|bool $site_license_id
12075 * @param number|null $blog_id
12076 *
12077 * @return FS_Plugin_License[]|object
12078 */
12079 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
12080 $this->_logger->entrance();
12081
12082 $is_network_admin = fs_is_network_admin();
12083
12084 if ( $is_network_admin && is_null( $blog_id ) ) {
12085 $all_licenses = self::get_all_licenses( $this->_module_id );
12086 } else {
12087 $all_licenses = $this->get_user_licenses( $this->_user->id );
12088 }
12089
12090 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
12091
12092 $all_licenses_map = array();
12093 foreach ( $all_licenses as $license ) {
12094 $all_licenses_map[ $license->id ] = true;
12095 }
12096
12097 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
12098
12099 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
12100 $licenses_map = array();
12101 foreach ( $licenses as $license ) {
12102 $licenses_map[ $license->id ] = true;
12103 }
12104
12105 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
12106 // foreach ( $license_ids_to_keep as $license_id ) {
12107 // if ( isset( $licenses_map[ $license_id ] ) ) {
12108 // continue;
12109 // }
12110 //
12111 // $missing_license = self::_get_license_by_id( $license_id, false );
12112 // if ( is_object( $missing_license ) ) {
12113 // $licenses[] = $missing_license;
12114 // $licenses_map[ $missing_license->id ] = true;
12115 // }
12116 // }
12117
12118 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
12119
12120 foreach ( $user_license_ids as $key => $license_id ) {
12121 if ( ! isset( $licenses_map[ $license_id ] ) ) {
12122 // Remove access to licenses that no longer exist.
12123 unset( $user_license_ids[ $key ] );
12124 }
12125 }
12126
12127 if ( ! empty( $user_license_ids ) ) {
12128 foreach ( $licenses_map as $license_id => $value ) {
12129 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
12130 // Associate new licenses with the user who triggered the license syncing.
12131 $user_license_ids[] = $license_id;
12132 }
12133 }
12134
12135 $user_license_ids = array_unique( $user_license_ids );
12136 } else {
12137 $user_license_ids = array_keys( $licenses_map );
12138 }
12139
12140 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
12141 $user_licenses = array();
12142 foreach ( $licenses as $license ) {
12143 if ( ! in_array( $license->id, $user_license_ids ) ) {
12144 continue;
12145 }
12146
12147 $user_licenses[] = $license;
12148 }
12149
12150 $this->_licenses = $user_licenses;
12151 } else {
12152 $this->_licenses = $licenses;
12153 }
12154
12155 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
12156
12157 $this->_store_licenses( true, $this->_module_id, $licenses );
12158 }
12159
12160 // Update current license.
12161 if ( is_object( $this->_license ) ) {
12162 $license = $this->_get_license_by_id( $this->_license->id );
12163
12164 if ( is_object( $license ) ) {
12165 /**
12166 * `$license` can be `false` in case a user change action has just been completed and this method
12167 * has synced the `$this->_licenses` collection for the new user. In this case, the
12168 * `$this->_licenses` collection may have only the newly activated license that is associated with
12169 * the new user. `set_license` will eventually be called in the same request by the logic that
12170 * follows outside this method which will detect that the install's license has been updated, and
12171 * then `_update_site_license` will be called which in turn will call `set_license`.
12172 *
12173 * @author Leo Fajardo (@leorw)
12174 * @since 2.3.2
12175 */
12176 $this->set_license( $license );
12177 }
12178 }
12179
12180 return $this->_licenses;
12181 }
12182
12183 /**
12184 * @author Vova Feldman (@svovaf)
12185 * @since 1.0.5
12186 *
12187 * @param number $id
12188 * @param bool $sync_licenses
12189 *
12190 * @return FS_Plugin_License|false
12191 */
12192 function _get_license_by_id( $id, $sync_licenses = true ) {
12193 $this->_logger->entrance();
12194
12195 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
12196 return false;
12197 }
12198
12199 /**
12200 * When running from the network level admin and opted-in from the network,
12201 * check if the license exists in the network user licenses collection.
12202 *
12203 * @author Vova Feldman (@svovaf)
12204 * @since 2.0.0
12205 */
12206 if ( fs_is_network_admin() &&
12207 $this->is_network_registered() &&
12208 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
12209 ) {
12210 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
12211
12212 foreach ( $licenses as $license ) {
12213 if ( $id == $license->id ) {
12214 return $license;
12215 }
12216 }
12217 }
12218
12219 if ( ! $this->has_any_license() && $sync_licenses ) {
12220 $this->_sync_licenses( $id );
12221 }
12222
12223 if ( is_array( $this->_licenses ) ) {
12224 foreach ( $this->_licenses as $license ) {
12225 if ( $id == $license->id ) {
12226 return $license;
12227 }
12228 }
12229 }
12230
12231 return false;
12232 }
12233
12234 /**
12235 * 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.
12236 *
12237 * @author Vova Feldman (@svovaf)
12238 * @since 2.0.0
12239 *
12240 * @param number $id
12241 *
12242 * @return FS_Plugin_License
12243 */
12244 private function get_license_by_id( $id ) {
12245 $licenses = self::get_all_licenses( $this->_module_id );
12246
12247 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
12248 foreach ( $licenses as $license ) {
12249 if ( $id == $license->id ) {
12250 return $license;
12251 }
12252 }
12253 }
12254
12255 return null;
12256 }
12257
12258 /**
12259 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
12260 *
12261 * @author Vova Feldman (@svovaf)
12262 * @since 2.0.0
12263 *
12264 * @return \FS_Plugin_License|mixed
12265 */
12266 private function sync_site_license() {
12267 $api = $this->get_api_user_scope();
12268
12269 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
12270
12271 if ( ! $this->is_api_result_entity( $result ) ) {
12272 return $result;
12273 }
12274
12275 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
12276 $this->_store_licenses();
12277
12278 return $license;
12279 }
12280
12281 /**
12282 * Get all user's available licenses for the current module.
12283 *
12284 * @author Vova Feldman (@svovaf)
12285 * @since 2.0.0
12286 *
12287 * @param number $user_id
12288 *
12289 * @return FS_Plugin_License[]
12290 */
12291 private function get_user_licenses( $user_id ) {
12292 $all_licenses = self::get_all_licenses( $this->_module_id );
12293 if ( empty( $all_licenses ) ) {
12294 return array();
12295 }
12296
12297 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
12298 if ( empty( $user_license_ids ) ) {
12299 return array();
12300 }
12301
12302 $licenses = array();
12303 foreach ( $all_licenses as $license ) {
12304 if ( in_array( $license->id, $user_license_ids ) ) {
12305 $licenses[] = $license;
12306 }
12307 }
12308
12309 return $licenses;
12310 }
12311
12312 /**
12313 * Checks if the context license is network activated except on the given blog ID.
12314 *
12315 * @author Vova Feldman (@svovaf)
12316 * @since 2.0.0
12317 *
12318 * @param int $except_blog_id
12319 *
12320 * @return bool
12321 */
12322 private function is_license_network_active( $except_blog_id = 0 ) {
12323 $this->_logger->entrance();
12324
12325 if ( ! is_object( $this->_license ) ) {
12326 return false;
12327 }
12328
12329 $sites = self::get_sites();
12330
12331 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
12332 // There are more sites than the number of activations, so license cannot be network activated.
12333 return false;
12334 }
12335
12336 foreach ( $sites as $site ) {
12337 $blog_id = self::get_site_blog_id( $site );
12338
12339 if ( $except_blog_id == $blog_id ) {
12340 // Skip excluded blog.
12341 continue;
12342 }
12343
12344 $install = $this->get_install_by_blog_id( $blog_id );
12345
12346 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
12347 return false;
12348 }
12349 }
12350
12351 return true;
12352 }
12353
12354 /**
12355 * 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.
12356 *
12357 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
12358 *
12359 * @author Vova Feldman (@svovaf)
12360 * @since 2.0.0
12361 *
12362 * @param \FS_User $user
12363 * @param \FS_Plugin_License $license
12364 *
12365 * @return bool
12366 */
12367 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
12368 $this->_logger->entrance();
12369
12370 $result = $this->can_activate_license_on_network( $license );
12371
12372 if ( false === $result ) {
12373 return false;
12374 }
12375
12376 $installs_without_license = $result['installs'];
12377 if ( ! empty( $installs_without_license ) ) {
12378 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
12379 }
12380
12381 $disconnected_site_ids = $result['sites'];
12382 if ( ! empty( $disconnected_site_ids ) ) {
12383 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
12384 }
12385
12386 $this->link_license_2_user( $license->id, $user->id );
12387
12388 // Sync license after activations.
12389 $license->activated += $result['production_count'];
12390 $license->activated_local += $result['localhost_count'];
12391
12392 // $this->_store_licenses()
12393
12394 return true;
12395 }
12396
12397 /**
12398 * Checks if the given license can be activated on the whole network.
12399 *
12400 * @author Vova Feldman (@svovaf)
12401 * @since 2.0.0
12402 *
12403 * @param \FS_Plugin_License $license
12404 *
12405 * @return false|array {
12406 * @type array[int]FS_Site $installs Blog ID to install map.
12407 * @type int[] $sites Non-connected blog IDs.
12408 * @type int $production_count Production sites count.
12409 * @type int $localhost_count Production sites count.
12410 * }
12411 */
12412 private function can_activate_license_on_network( FS_Plugin_License $license ) {
12413 $sites = self::get_sites();
12414
12415 $production_count = 0;
12416 $localhost_count = 0;
12417
12418 $installs_without_license = array();
12419 $disconnected_site_ids = array();
12420
12421 foreach ( $sites as $site ) {
12422 $blog_id = self::get_site_blog_id( $site );
12423 $install = $this->get_install_by_blog_id( $blog_id );
12424
12425 if ( is_object( $install ) ) {
12426 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
12427 // License already activated on the install.
12428 continue;
12429 }
12430
12431 $url = $install->url;
12432
12433 $installs_without_license[ $blog_id ] = $install;
12434 } else {
12435 $url = is_object( $site ) ?
12436 $site->siteurl :
12437 self::get_unfiltered_site_url( $blog_id );
12438
12439 $disconnected_site_ids[] = $blog_id;
12440 }
12441
12442 if ( FS_Site::is_localhost_by_address( $url ) ) {
12443 $localhost_count ++;
12444 } else {
12445 $production_count ++;
12446 }
12447 }
12448
12449 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12450 return false;
12451 }
12452
12453 return array(
12454 'installs' => $installs_without_license,
12455 'sites' => $disconnected_site_ids,
12456 'production_count' => $production_count,
12457 'localhost_count' => $localhost_count,
12458 );
12459 }
12460
12461 /**
12462 * Activate a given license on a collection of installs.
12463 *
12464 * @author Vova Feldman (@svovaf)
12465 * @since 2.0.0
12466 *
12467 * @param \FS_User $user
12468 * @param string $license_key
12469 * @param array $blog_2_install_map {
12470 * @key int Blog ID.
12471 * @value FS_Site Blog's associated install.
12472 * }
12473 *
12474 * @return mixed|true
12475 */
12476 private function activate_license_on_many_installs(
12477 FS_User $user,
12478 $license_key,
12479 array $blog_2_install_map
12480 ) {
12481 $params = array(
12482 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12483 );
12484
12485 $install_2_blog_map = array();
12486 foreach ( $blog_2_install_map as $blog_id => $install ) {
12487 $params[] = array( 'id' => $install->id, 'url' => $install->url );
12488
12489 $install_2_blog_map[ $install->id ] = $blog_id;
12490 }
12491
12492 $result = $this->get_api_user_scope_by_user( $user )->call(
12493 "plugins/{$this->_plugin->id}/installs.json",
12494 'PUT',
12495 $params
12496 );
12497
12498 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12499 return $result;
12500 }
12501
12502 foreach ( $result->installs as $r_install ) {
12503 $install = new FS_Site( $r_install );
12504 $install->is_disconnected = false;
12505
12506 // Update install.
12507 $this->_store_site(
12508 true,
12509 $install_2_blog_map[ $r_install->id ],
12510 $install
12511 );
12512 }
12513
12514 return true;
12515 }
12516
12517 /**
12518 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12519 *
12520 * @author Vova Feldman (@svovaf)
12521 * @since 2.3.1
12522 *
12523 * @param \FS_User $user
12524 * @param string $license_key
12525 *
12526 * @return true|mixed True if successful, otherwise, the API result.
12527 */
12528 private function activate_license_on_site( FS_User $user, $license_key ) {
12529 return $this->activate_license_on_many_sites( $user, $license_key );
12530 }
12531
12532 /**
12533 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12534 *
12535 * @author Vova Feldman (@svovaf)
12536 * @since 2.0.0
12537 *
12538 * @param \FS_User $user
12539 * @param string $license_key
12540 * @param int[] $site_ids
12541 *
12542 * @return true|mixed True if successful, otherwise, the API result.
12543 */
12544 private function activate_license_on_many_sites(
12545 FS_User $user,
12546 $license_key,
12547 array $site_ids = array()
12548 ) {
12549 $sites = array();
12550 foreach ( $site_ids as $site_id ) {
12551 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12552 }
12553
12554 // Install the plugin.
12555 $result = $this->create_installs_with_user(
12556 $user,
12557 $license_key,
12558 false,
12559 $sites,
12560 false,
12561 true
12562 );
12563
12564 if ( ! $this->is_api_result_entity( $result ) &&
12565 ! $this->is_api_result_object( $result, 'installs' )
12566 ) {
12567 return $result;
12568 }
12569
12570 $installs = array();
12571
12572 if ( $this->is_api_result_entity( $result ) ) {
12573 $install = new FS_Site( $result );
12574
12575 $this->_user = $user;
12576
12577 $this->_store_site( true, null, $install );
12578
12579 $this->_site = $install;
12580
12581 $this->reset_anonymous_mode();
12582 } else {
12583 foreach ( $result->installs as $install ) {
12584 $installs[] = new FS_Site( $install );
12585 }
12586
12587 // Map site addresses to their blog IDs.
12588 $address_to_blog_map = $this->get_address_to_blog_map();
12589
12590 $first_blog_id = null;
12591
12592 foreach ( $installs as $install ) {
12593 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12594 $blog_id = $address_to_blog_map[ $address ];
12595
12596 $this->_store_site( true, $blog_id, $install );
12597
12598 $this->reset_anonymous_mode( $blog_id );
12599
12600 if ( is_null( $first_blog_id ) ) {
12601 $first_blog_id = $blog_id;
12602 }
12603 }
12604
12605 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12606 $this->_storage->network_install_blog_id = $first_blog_id;
12607 }
12608 }
12609
12610 return true;
12611 }
12612
12613 /**
12614 * Sync site's license with user licenses.
12615 *
12616 * @author Vova Feldman (@svovaf)
12617 * @since 1.0.6
12618 *
12619 * @param FS_Plugin_License|null $new_license
12620 *
12621 * @return FS_Plugin_License|null
12622 */
12623 function _update_site_license( $new_license ) {
12624 $this->_logger->entrance();
12625
12626 /**
12627 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12628 * accordingly so that it will also handle the case when an ownership change is done via license
12629 * activation.
12630 *
12631 * @author Leo Fajardo (@leorw)
12632 * @since 2.3.2
12633 */
12634 $this->set_license( $new_license );
12635
12636 if ( ! is_object( $new_license ) ) {
12637 $this->_site->license_id = null;
12638 $this->_sync_site_subscription( null );
12639
12640 return $this->_license;
12641 }
12642
12643 $this->_site->license_id = $this->_license->id;
12644
12645 if ( ! is_array( $this->_licenses ) ) {
12646 $this->_licenses = array();
12647 }
12648
12649 $is_license_found = false;
12650 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12651 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12652 $this->_licenses[ $i ] = $new_license;
12653
12654 $is_license_found = true;
12655 break;
12656 }
12657 }
12658
12659 // If new license just append.
12660 if ( ! $is_license_found ) {
12661 $this->_licenses[] = $new_license;
12662 }
12663
12664 $this->_sync_site_subscription( $new_license );
12665
12666 return $this->_license;
12667 }
12668
12669 /**
12670 * @author Vova Feldman (@svovaf)
12671 * @since 2.3.1
12672 *
12673 * @param \FS_Plugin_License $license
12674 */
12675 private function set_license( FS_Plugin_License $license = null ) {
12676 $this->_license = $license;
12677
12678 $this->maybe_update_whitelabel_flag( $license );
12679 }
12680
12681 /**
12682 * @author Leo Fajardo (@leorw)
12683 * @since 2.3.1
12684 *
12685 * @param FS_Plugin_License $license
12686 */
12687 private function maybe_update_whitelabel_flag( $license ) {
12688 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12689 $this->_storage->is_whitelabeled :
12690 false;
12691
12692 if ( is_object( $license ) ) {
12693 $license_user = self::_get_user_by_id( $license->user_id );
12694
12695 if ( ! is_object( $license_user ) ) {
12696 // If foreign license, do not update the `is_whitelabeled` flag.
12697 return;
12698 }
12699
12700 if ( $this->is_addon() ) {
12701 /**
12702 * Store the last license data to the parent's storage since it's needed only when showing the
12703 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12704 * iterate over the add-ons just to get the last license data.
12705 */
12706 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12707 } else {
12708 $this->store_last_activated_license_data( $license );
12709 }
12710
12711 if ( $license->is_whitelabeled ) {
12712 // Activated a developer license, data should be hidden.
12713 $is_whitelabeled = true;
12714 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12715 // The account owner activated a regular license key, no need to hide the data.
12716 $is_whitelabeled = false;
12717 }
12718 }
12719
12720 $this->_storage->is_whitelabeled = $is_whitelabeled;
12721
12722 // Reset the whitelabeled status after update.
12723 $this->is_whitelabeled = null;
12724 if ( $this->is_addon() ) {
12725 $parent_fs = $this->get_parent_instance();
12726
12727 if ( is_object( $parent_fs ) ) {
12728 $parent_fs->is_whitelabeled = null;
12729 }
12730 }
12731 }
12732
12733 /**
12734 * @author Leo Fajardo (@leorw)
12735 * @since 2.3.1
12736 *
12737 * @param FS_Plugin_License $license
12738 * @param FS_User $license_user
12739 */
12740 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12741 if ( ! is_object( $license_user ) ) {
12742 $this->_storage->last_license_key = md5( $license->secret_key );
12743 $this->_storage->last_license_user_id = null;
12744 } else {
12745 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12746 $this->_storage->last_license_user_id = $license_user->id;
12747 }
12748 }
12749
12750 /**
12751 * @author Leo Fajardo (@leorw)
12752 * @since 2.3.1
12753 *
12754 * @param bool $ignore_data_debug_mode
12755 *
12756 * @return bool
12757 */
12758 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12759 if ( true !== $this->_storage->is_whitelabeled ) {
12760 return false;
12761 } else if ( $ignore_data_debug_mode ) {
12762 return true;
12763 }
12764
12765 $fs = $this->is_addon() ?
12766 $this->get_parent_instance() :
12767 $this;
12768
12769 return ! $fs->is_data_debug_mode();
12770 }
12771
12772 /**
12773 * @author Leo Fajardo (@leorw)
12774 * @since 2.3.1
12775 *
12776 * @return number
12777 */
12778 function get_last_license_user_id() {
12779 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12780 $this->_storage->last_license_user_id :
12781 null;
12782 }
12783
12784 /**
12785 * @author Leo Fajardo (@leorw)
12786 * @since 2.3.1
12787 *
12788 * @param int $blog_id
12789 * @param bool $ignore_data_debug_mode
12790 *
12791 * @return bool
12792 */
12793 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12794 if ( ! is_null( $blog_id ) ) {
12795 $this->switch_to_blog( $blog_id );
12796 }
12797
12798 if ( ! is_null( $this->is_whitelabeled ) ) {
12799 $is_whitelabeled = $this->is_whitelabeled;
12800 } else {
12801 $is_whitelabeled = false;
12802
12803 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12804
12805 if ( ! $this->has_addons() ) {
12806 $is_whitelabeled = $is_whitelabeled_flag;
12807 } else if ( $is_whitelabeled_flag ) {
12808 $is_whitelabeled = true;
12809 } else {
12810 if ( $this->is_registered() || $this->is_premium() ) {
12811 $addon_ids = $this->get_updated_account_addons();
12812 } else {
12813 $addons = self::get_all_addons();
12814
12815 $plugin_addons = isset( $addons[ $this->_plugin->id ] ) ?
12816 $addons[ $this->_plugin->id ] :
12817 array();
12818
12819 $addon_ids = array();
12820 foreach ( $plugin_addons as $addon ) {
12821 $addon_ids[] = $addon->id;
12822 }
12823 }
12824
12825 $installed_addons = $this->get_installed_addons();
12826 foreach ( $installed_addons as $fs_addon ) {
12827 $addon_ids[] = $fs_addon->get_id();
12828 }
12829
12830 if ( ! empty( $addon_ids ) ) {
12831 $addon_ids = array_unique( $addon_ids );
12832
12833 $is_network_level = (
12834 fs_is_network_admin() &&
12835 $this->is_network_active()
12836 );
12837
12838 foreach ( $addon_ids as $addon_id ) {
12839 $addon = $this->get_addon( $addon_id );
12840
12841 if ( ! is_object( $addon ) ) {
12842 continue;
12843 }
12844
12845 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12846 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12847 self::get_addon_instance( $addon_id ) :
12848 null;
12849
12850 $was_addon_network_activated = false;
12851
12852 if ( is_object( $fs_addon ) ) {
12853 $was_addon_network_activated = $fs_addon->is_network_active();
12854 } else if ( $is_network_level ) {
12855 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12856 }
12857
12858 $network_delegated_connection = (
12859 $was_addon_network_activated &&
12860 $addon_storage->get( 'is_delegated_connection', false, true )
12861 );
12862
12863 if (
12864 $is_network_level &&
12865 ( ! $was_addon_network_activated || $network_delegated_connection )
12866 ) {
12867 $sites = self::get_sites();
12868
12869 /**
12870 * If in network admin area and the add-on was not network-activated or network-activated
12871 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12872 */
12873 foreach ( $sites as $site ) {
12874 $site_info = $this->get_site_info( $site );
12875
12876 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12877 $is_whitelabeled = true;
12878 break;
12879 }
12880 }
12881
12882 if ( $is_whitelabeled ) {
12883 break;
12884 }
12885 } else {
12886 /**
12887 * This will be executed when any of the following is met:
12888 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12889 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12890 * 3. Add-on was not network-activated and in site admin area.
12891 */
12892 if ( true === $addon_storage->is_whitelabeled ) {
12893 $is_whitelabeled = true;
12894 break;
12895 }
12896 }
12897 }
12898 }
12899 }
12900
12901 $this->is_whitelabeled = $is_whitelabeled;
12902
12903 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12904 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12905 }
12906
12907 if ( ! is_null( $blog_id ) ) {
12908 $this->restore_current_blog();
12909 }
12910 }
12911
12912 return (
12913 $is_whitelabeled &&
12914 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12915 );
12916 }
12917
12918 /**
12919 * Sync site's subscription.
12920 *
12921 * @author Vova Feldman (@svovaf)
12922 * @since 1.0.9
12923 *
12924 * @param FS_Plugin_License|null $license
12925 *
12926 * @return bool|\FS_Subscription
12927 */
12928 private function _sync_site_subscription( $license ) {
12929 if ( ! is_object( $license ) ) {
12930 $this->delete_unused_subscriptions();
12931
12932 return false;
12933 }
12934
12935 // Load subscription details if not lifetime.
12936 $subscription = $license->is_lifetime() ?
12937 false :
12938 $this->_fetch_site_license_subscription();
12939
12940 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12941 $this->store_subscription( $subscription );
12942 } else {
12943 $this->delete_unused_subscriptions();
12944 }
12945
12946 return $subscription;
12947 }
12948
12949 /**
12950 * @author Vova Feldman (@svovaf)
12951 * @since 1.0.6
12952 *
12953 * @return bool|\FS_Plugin_License
12954 */
12955 function _get_license() {
12956 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12957 return $this->_license;
12958 }
12959
12960 return $this->_get_available_premium_license();
12961 }
12962
12963 /**
12964 * @param number $license_id
12965 *
12966 * @return null|\FS_Subscription
12967 */
12968 function _get_subscription( $license_id ) {
12969 if ( ! isset( $this->_storage->subscriptions ) ||
12970 empty( $this->_storage->subscriptions )
12971 ) {
12972 return null;
12973 }
12974
12975 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12976 if ( $subscription->license_id == $license_id ) {
12977 return $subscription;
12978 }
12979 }
12980
12981 return null;
12982 }
12983
12984 /**
12985 * @author Leo Fajardo (@leorw)
12986 * @since 2.0.0
12987 *
12988 * @param FS_Subscription $subscription
12989 */
12990 function store_subscription( FS_Subscription $subscription ) {
12991 if ( ! isset( $this->_storage->subscriptions ) ) {
12992 $this->_storage->subscriptions = array();
12993 }
12994
12995 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12996 $this->_storage->subscriptions = array( $subscription );
12997
12998 return;
12999 }
13000
13001 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
13002
13003 $updated_subscription = false;
13004 foreach ( $subscriptions as $key => $existing_subscription ) {
13005 if ( $existing_subscription->id == $subscription->id ) {
13006 $subscriptions[ $key ] = $subscription;
13007 $updated_subscription = true;
13008 break;
13009 }
13010 }
13011
13012 if ( ! $updated_subscription ) {
13013 $subscriptions[] = $subscription;
13014 }
13015
13016 $this->_storage->subscriptions = $subscriptions;
13017 }
13018
13019 /**
13020 * @author Leo Fajardo (@leorw)
13021 * @since 2.0.0
13022 */
13023 function delete_unused_subscriptions() {
13024 if ( ! isset( $this->_storage->subscriptions ) ||
13025 empty( $this->_storage->subscriptions ) ||
13026 // Clean up only if there are already at least 3 subscriptions.
13027 ( count( $this->_storage->subscriptions ) < 3 )
13028 ) {
13029 return;
13030 }
13031
13032 if ( ! is_multisite() ) {
13033 // If not multisite, there should only be 1 subscription, so just clear the array.
13034 $this->_storage->subscriptions = array();
13035
13036 return;
13037 }
13038
13039 $subscriptions_to_keep_by_license_id_map = array();
13040 $sites = self::get_sites();
13041 foreach ( $sites as $site ) {
13042 $blog_id = self::get_site_blog_id( $site );
13043 $install = $this->get_install_by_blog_id( $blog_id );
13044
13045 if ( ! is_object( $install ) ||
13046 ! FS_Plugin_License::is_valid_id( $install->license_id )
13047 ) {
13048 continue;
13049 }
13050
13051 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
13052 }
13053
13054 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
13055 $this->_storage->subscriptions = array();
13056
13057 return;
13058 }
13059
13060 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
13061 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
13062 unset( $this->_storage->subscriptions[ $key ] );
13063 }
13064 }
13065 }
13066
13067 /**
13068 * @author Vova Feldman (@svovaf)
13069 * @since 1.0.2
13070 *
13071 * @param string $plan Plan name
13072 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
13073 *
13074 * @return bool
13075 */
13076 function is_plan( $plan, $exact = false ) {
13077 $this->_logger->entrance();
13078
13079 if ( ! $this->is_registered() ) {
13080 return false;
13081 }
13082
13083 $plan = strtolower( $plan );
13084
13085 $current_plan_name = $this->get_plan_name();
13086
13087 if ( $current_plan_name === $plan ) {
13088 // Exact plan.
13089 return true;
13090 } else if ( $exact ) {
13091 // Required exact, but plans are different.
13092 return false;
13093 }
13094
13095 $current_plan_order = - 1;
13096 $required_plan_order = PHP_INT_MAX;
13097 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
13098 if ( $plan === $this->_plans[ $i ]->name ) {
13099 $required_plan_order = $i;
13100 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
13101 $current_plan_order = $i;
13102 }
13103 }
13104
13105 return ( $current_plan_order > $required_plan_order );
13106 }
13107
13108 /**
13109 * Check if module has only one plan.
13110 *
13111 * @author Vova Feldman (@svovaf)
13112 * @since 1.2.1.7
13113 *
13114 * @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.
13115 *
13116 * @return bool
13117 */
13118 function is_single_plan( $double_check = false ) {
13119 $this->_logger->entrance();
13120
13121 if ( ! $this->is_registered() ||
13122 ! is_array( $this->_plans ) ||
13123 0 === count( $this->_plans )
13124 ) {
13125 return true;
13126 }
13127
13128 $has_free_plan = $this->has_free_plan();
13129
13130 if ( ! $has_free_plan && $double_check ) {
13131 foreach ( $this->_plans as $plan ) {
13132 if ( $plan->is_free() ) {
13133 $has_free_plan = true;
13134 break;
13135 }
13136 }
13137 }
13138
13139 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
13140 }
13141
13142 /**
13143 * Check if plan based on trial. If not in trial mode, should return false.
13144 *
13145 * @since 1.0.9
13146 *
13147 * @param string $plan Plan name
13148 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
13149 *
13150 * @return bool
13151 */
13152 function is_trial_plan( $plan, $exact = false ) {
13153 $this->_logger->entrance();
13154
13155 if ( ! $this->is_registered() ) {
13156 return false;
13157 }
13158
13159 if ( ! $this->is_trial() ) {
13160 return false;
13161 }
13162
13163 $trial_plan = $this->get_trial_plan();
13164
13165 if ( $trial_plan->name === $plan ) {
13166 // Exact plan.
13167 return true;
13168 } else if ( $exact ) {
13169 // Required exact, but plans are different.
13170 return false;
13171 }
13172
13173 $current_plan_order = - 1;
13174 $required_plan_order = - 1;
13175 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
13176 if ( $plan === $this->_plans[ $i ]->name ) {
13177 $required_plan_order = $i;
13178 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
13179 $current_plan_order = $i;
13180 }
13181 }
13182
13183 return ( $current_plan_order > $required_plan_order );
13184 }
13185
13186 /**
13187 * Check if plugin has any paid plans.
13188 *
13189 * @author Vova Feldman (@svovaf)
13190 * @since 1.0.7
13191 *
13192 * @return bool
13193 */
13194 function has_paid_plan() {
13195 return $this->_has_paid_plans ||
13196 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
13197 }
13198
13199 /**
13200 * Check if plugin has any plan with a trail.
13201 *
13202 * @author Vova Feldman (@svovaf)
13203 * @since 1.0.9
13204 *
13205 * @return bool
13206 */
13207 function has_trial_plan() {
13208 /**
13209 * @author Vova Feldman(@svovaf)
13210 * @since 1.2.1.5
13211 *
13212 * Allow setting a trial from the SDK without calling the API.
13213 * But, if the user did opt-in, continue using the real data from the API.
13214 */
13215 if ( $this->_trial_days >= 0 ) {
13216 return true;
13217 }
13218
13219 return $this->_storage->get( 'has_trial_plan', false );
13220 }
13221
13222 /**
13223 * Check if plugin has any free plan, or is it premium only.
13224 *
13225 * Note: If no plans configured, assume plugin is free.
13226 *
13227 * @author Vova Feldman (@svovaf)
13228 * @since 1.0.7
13229 *
13230 * @return bool
13231 */
13232 function has_free_plan() {
13233 return ! $this->is_only_premium();
13234 }
13235
13236 /**
13237 * Displays a license activation dialog box when the user clicks on the "Activate License"
13238 * or "Change License" link on the plugins
13239 * page.
13240 *
13241 * @author Leo Fajardo (@leorw)
13242 * @since 1.1.9
13243 */
13244 function _add_license_activation_dialog_box() {
13245 $vars = array(
13246 'id' => $this->_module_id,
13247 );
13248
13249 fs_require_template( 'forms/license-activation.php', $vars );
13250 fs_require_template( 'forms/resend-key.php', $vars );
13251 }
13252
13253 /**
13254 * Displays an email address update dialog box when the user clicks on the email address "Edit" button on the "Account" page.
13255 *
13256 * @author Leo Fajardo (@leorw)
13257 * @since 2.5.0
13258 */
13259 function _add_email_address_update_dialog_box() {
13260 $vars = array( 'id' => $this->_module_id );
13261
13262 fs_require_template( 'forms/email-address-update.php', $vars );
13263 }
13264
13265 /**
13266 * @author Leo Fajardo (@leorw)
13267 * @since 2.5.0
13268 */
13269 function _add_email_address_update_option() {
13270 if ( ! $this->should_handle_user_change() ) {
13271 return;
13272 }
13273
13274 // Add email address update AJAX handler.
13275 $this->add_ajax_action( 'update_email_address', array( &$this, '_email_address_update_ajax_handler' ) );
13276 }
13277
13278 /**
13279 * @author Leo Fajardo (@leorw)
13280 * @since 2.5.0
13281 */
13282 function _email_address_update_ajax_handler() {
13283 $this->check_ajax_referer( 'update_email_address' );
13284
13285 $new_email_address = fs_request_get( 'email_address' );
13286 $transfer_type = fs_request_get( 'transfer_type' );
13287
13288 $result = $this->update_email( $new_email_address );
13289
13290 if ( ! FS_Api::is_api_error( $result ) ) {
13291 self::shoot_ajax_success();
13292 }
13293
13294 $error = '';
13295
13296 if ( FS_Api::is_api_error_object( $result ) ) {
13297 switch ( $result->error->code ) {
13298 case 'user_exist':
13299 case 'account_verification_required':
13300 $error = array(
13301 'code' => 'change_ownership',
13302 'url' => $this->get_account_url( 'change_owner', array(
13303 'state' => 'init',
13304 'candidate_email' => $new_email_address,
13305 'transfer_type' => $transfer_type,
13306 ) ),
13307 );
13308
13309 break;
13310 }
13311 }
13312
13313 if ( empty( $error ) ) {
13314 $error = is_object( $result ) ?
13315 var_export( $result->error, true ) :
13316 $result;
13317 }
13318
13319 self::shoot_ajax_failure( $error );
13320 }
13321
13322 /**
13323 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
13324 *
13325 * @author Leo Fajardo (@leorw)
13326 * @since 2.3.2
13327 *
13328 * @return number[]
13329 */
13330 function get_installs_ids_with_foreign_licenses() {
13331 $installs = array();
13332
13333 if (
13334 is_object( $this->_license ) &&
13335 $this->_site->user_id != $this->_license->user_id
13336 ) {
13337 $installs[] = $this->_site->id;
13338 }
13339
13340 /**
13341 * Also try to get foreign licenses for the context product's add-ons.
13342 */
13343 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
13344
13345 foreach ( $installs_by_slug_map as $slug => $install_info ) {
13346 if ( $slug == $this->get_slug() ) {
13347 continue;
13348 }
13349
13350 $install = $install_info['install'];
13351 $license = $install_info['license'];
13352
13353 if (
13354 is_object( $license ) &&
13355 $install->user_id != $license->user_id
13356 ) {
13357 $installs[] = $install->id;
13358 }
13359 }
13360
13361 return $installs;
13362 }
13363
13364 /**
13365 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
13366 *
13367 * @author Leo Fajardo (@leorw)
13368 * @since 2.3.2
13369 *
13370 * @param number[] $install_ids
13371 */
13372 function _add_user_change_dialog_box( $install_ids ) {
13373 $vars = array(
13374 'id' => $this->_module_id,
13375 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
13376 );
13377
13378 fs_require_template( 'forms/user-change.php', $vars );
13379 }
13380
13381 /**
13382 * @author Leo Fajardo (@leorw)
13383 * @since 2.3.1
13384 */
13385 function _add_data_debug_mode_dialog_box() {
13386 $vars = array(
13387 'id' => $this->_module_id,
13388 );
13389
13390 fs_require_template( 'forms/data-debug-mode.php', $vars );
13391 }
13392
13393 /**
13394 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
13395 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
13396 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
13397 * is only activated on a single production site.
13398 *
13399 * @author Leo Fajardo (@leorw)
13400 * @since 2.2.1
13401 *
13402 * @param bool $is_license_deactivation
13403 *
13404 * @return array
13405 */
13406 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
13407 if ( fs_is_network_admin() ) {
13408 // Subscription cancellation dialog box is currently not supported for multisite networks.
13409 return array();
13410 }
13411
13412 if ( $this->is_whitelabeled() ) {
13413 return array();
13414 }
13415
13416 $license = $this->_get_license();
13417
13418 /**
13419 * 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.
13420 *
13421 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
13422 * @since 2.2.1
13423 */
13424 if ( ! is_object( $license ) ||
13425 $license->is_lifetime() ||
13426 ( ! $license->is_single_site() && $license->activated > 1 )
13427 ) {
13428 return array();
13429 }
13430
13431 /**
13432 * @var FS_Subscription $subscription
13433 */
13434 $subscription = $this->_get_subscription( $license->id );
13435 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
13436 return array();
13437 }
13438
13439 return array(
13440 'id' => $this->_module_id,
13441 'license' => $license,
13442 'has_trial' => $this->is_paid_trial(),
13443 'is_license_deactivation' => $is_license_deactivation,
13444 );
13445 }
13446
13447 /**
13448 * @author Leo Fajardo (@leorw)
13449 * @since 2.0.2
13450 */
13451 function _add_premium_version_upgrade_selection_dialog_box() {
13452 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
13453 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
13454 return;
13455 }
13456
13457 $vars = array(
13458 'id' => $this->_module_id,
13459 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
13460 $modules_update->response[ $this->_plugin_basename ]->new_version :
13461 $modules_update->response[ $this->_plugin_basename ]['new_version']
13462 );
13463
13464 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13465 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13466 }
13467
13468 /**
13469 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13470 * page.
13471 *
13472 * @author Leo Fajardo (@leorw)
13473 * @since 1.2.1.5
13474 */
13475 function _add_optout_dialog() {
13476 if ( $this->is_theme() ) {
13477 $vars = null;
13478 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13479 }
13480
13481 $vars = array( 'id' => $this->_module_id );
13482 fs_require_template( 'forms/optout.php', $vars );
13483 }
13484
13485 /**
13486 * Prepare page to include all required UI and logic for the license activation dialog.
13487 *
13488 * @author Vova Feldman (@svovaf)
13489 * @since 1.2.0
13490 */
13491 function _add_license_activation() {
13492 if ( $this->is_migration() ) {
13493 return;
13494 }
13495
13496 if ( ! $this->is_user_admin() ) {
13497 // Only admins can activate a license.
13498 return;
13499 }
13500
13501 if ( ! $this->has_paid_plan() ) {
13502 // Module doesn't have any paid plans.
13503 return;
13504 }
13505
13506 if (
13507 $this->has_premium_version() &&
13508 ! $this->is_premium() &&
13509 /**
13510 * Also handle the case when an upgrade was made using the free version.
13511 *
13512 * @author Leo Fajardo (@leorw)
13513 * @since 2.3.2
13514 */
13515 ! is_object( $this->_get_license() )
13516 ) {
13517 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13518 return;
13519 }
13520
13521 // Add license activation link and AJAX request handler.
13522 if ( self::is_plugins_page() ) {
13523 $is_network_admin = fs_is_network_admin();
13524
13525 if (
13526 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13527 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13528 ) {
13529 if (
13530 $this->is_premium() ||
13531 ( $this->has_paid_plan() && ! $this->has_premium_version() )
13532 ) {
13533 /**
13534 * @since 1.2.0 Add license action link only on plugins page.
13535 */
13536 $this->_add_license_action_link();
13537 }
13538 }
13539 }
13540
13541 // Add license activation AJAX callback.
13542 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13543
13544 // Add resend license AJAX callback.
13545 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13546 }
13547
13548 /**
13549 * Prepares page to include all required UI and logic for the "Change User" dialog.
13550 *
13551 * @author Leo Fajardo (@leorw)
13552 * @since 2.3.2
13553 */
13554 function _add_user_change_option() {
13555 if ( ! $this->should_handle_user_change() ) {
13556 return;
13557 }
13558
13559 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13560
13561 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13562 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13563 return;
13564 }
13565
13566 // Add user change AJAX handler.
13567 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13568 }
13569
13570 /**
13571 * @author Leo Fajardo (@leorw)
13572 * @since 2.3.2
13573 */
13574 function should_handle_user_change() {
13575 if ( ! $this->is_user_admin() ) {
13576 // Only admins can change user.
13577 return false;
13578 }
13579
13580 if ( $this->is_addon() ) {
13581 return false;
13582 }
13583
13584 if ( ! $this->is_registered() ) {
13585 return false;
13586 }
13587
13588 if (
13589 $this->is_network_active() &&
13590 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13591 ) {
13592 // Handle only on site-level "Account" section for now.
13593 return false;
13594 }
13595
13596 return true;
13597 }
13598
13599 /**
13600 * @author Leo Fajardo (@leorw)
13601 * @since 2.0.2
13602 */
13603 function _add_premium_version_upgrade_selection() {
13604 if ( ! $this->is_user_admin() ) {
13605 return;
13606 }
13607
13608 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13609 // This is relevant only to the free versions and premium versions without an active license.
13610 return;
13611 }
13612
13613 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13614 $this->_add_premium_version_upgrade_selection_action();
13615 }
13616 }
13617
13618 /**
13619 * @author Edgar Melkonyan
13620 * @since 2.4.1
13621 *
13622 * @throws Freemius_Exception
13623 */
13624 function _toggle_whitelabel_mode_ajax_handler() {
13625 $this->_logger->entrance();
13626
13627 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13628
13629 if ( ! $this->is_user_admin() ) {
13630 // Only for admins.
13631 self::shoot_ajax_failure();
13632 }
13633
13634 $license = $this->get_api_user_scope()->call(
13635 "/licenses/{$this->_site->license_id}.json",
13636 'put',
13637 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13638 );
13639
13640 if ( ! $this->is_api_result_entity( $license ) ) {
13641 self::shoot_ajax_failure(
13642 FS_Api::is_api_error_object( $license ) ?
13643 $license->error->message :
13644 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13645 );
13646 }
13647
13648 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13649 $this->_store_licenses();
13650
13651 $this->_sync_license();
13652
13653 if ( ! $license->is_whitelabeled ) {
13654 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13655 } else {
13656 $this->_admin_notices->add_sticky(
13657 sprintf(
13658 $this->get_text_inline(
13659 '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.',
13660 'license_whitelabeled'
13661 ),
13662 "<strong>{$this->get_plugin_title()}</strong>",
13663 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13664 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13665 ),
13666 'license_whitelabeled'
13667 );
13668 }
13669
13670 self::shoot_ajax_response( array( 'success' => true ) );
13671 }
13672
13673 /**
13674 * @author Leo Fajardo (@leorw)
13675 * @since 2.3.0
13676 */
13677 function _add_beta_mode_update_handler() {
13678 if ( ! $this->is_user_admin() ) {
13679 return;
13680 }
13681
13682 if ( ! $this->is_premium() ) {
13683 return;
13684 }
13685
13686 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13687 }
13688
13689 /**
13690 * @author Leo Fajardo (@leorw)
13691 * @since 2.3.0
13692 */
13693 function _set_beta_mode_ajax_handler() {
13694 $this->_logger->entrance();
13695
13696 $this->check_ajax_referer( 'set_beta_mode' );
13697
13698 if ( ! $this->is_user_admin() ) {
13699 // Only for admins.
13700 self::shoot_ajax_failure();
13701 }
13702
13703 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13704
13705 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13706 self::shoot_ajax_failure();
13707 }
13708
13709 $site = $this->api_site_call(
13710 '',
13711 'put',
13712 array(
13713 'is_beta' => ( 'true' == $is_beta ),
13714 'fields' => 'is_beta'
13715 )
13716 );
13717
13718 if ( ! $this->is_api_result_entity( $site ) ) {
13719 self::shoot_ajax_failure(
13720 FS_Api::is_api_error_object( $site ) ?
13721 $site->error->message :
13722 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13723 );
13724 }
13725
13726 $this->_site->is_beta = $site->is_beta;
13727 $this->_store_site();
13728
13729 self::shoot_ajax_response( array( 'success' => true ) );
13730 }
13731
13732 /**
13733 * License activation WP AJAX handler.
13734 *
13735 * @author Leo Fajardo (@leorw)
13736 * @since 1.1.9
13737 *
13738 * @uses Freemius::activate_license()
13739 */
13740 function _activate_license_ajax_action() {
13741 $this->_logger->entrance();
13742
13743 $this->check_ajax_referer( 'activate_license' );
13744
13745 $license_key = trim( fs_request_get_raw( 'license_key' ) );
13746
13747 if ( empty( $license_key ) ) {
13748 $license_id = trim( fs_request_get_raw( 'license_id' ) );
13749
13750 if ( FS_Plugin_License::is_valid_id( $license_id ) ) {
13751 $license = $this->_get_license_by_id( $license_id, false );
13752
13753 if ( is_object( $license ) ) {
13754 $license_key = $license->secret_key;
13755 }
13756 }
13757 }
13758
13759 if ( empty( $license_key ) ) {
13760 exit;
13761 }
13762
13763 $sites = fs_is_network_admin() ?
13764 fs_request_get( 'sites', array(), 'post' ) :
13765 array();
13766
13767 $result = $this->activate_license(
13768 $license_key,
13769 $sites,
13770 fs_request_get_bool( 'is_marketing_allowed', null ),
13771 fs_request_get( 'blog_id', null ),
13772 fs_request_get( 'module_id', null, 'post' ),
13773 fs_request_get( 'user_id', null ),
13774 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
13775 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null )
13776 );
13777
13778 if (
13779 $result['success'] &&
13780 $this->is_bundle_license_auto_activation_enabled()
13781 ) {
13782 $license = new FS_Plugin_License();
13783 $license->secret_key = $license_key;
13784
13785 $this->maybe_activate_bundle_license( $license, $sites );
13786 }
13787
13788 echo json_encode( $result );
13789
13790 exit;
13791 }
13792
13793 /**
13794 * User change WP AJAX handler.
13795 *
13796 * @author Leo Fajardo (@leorw)
13797 * @since 2.3.2
13798 */
13799 function _user_change_ajax_action() {
13800 $this->_logger->entrance();
13801
13802 $this->check_ajax_referer( 'change_user' );
13803
13804 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13805 $new_user_id = fs_request_get( 'user_id' );
13806
13807 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13808 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13809 }
13810
13811 $params = array();
13812
13813 if ( ! empty( $new_email_address ) ) {
13814 $params['user_email'] = $new_email_address;
13815 } else {
13816 $params['user_id'] = $new_user_id;
13817 }
13818
13819 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13820 $install_ids = array();
13821
13822 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13823 $install_ids[ $slug ] = $install_info['install']->id;
13824 }
13825
13826 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13827
13828 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13829
13830 if ( FS_Api::is_api_error( $install ) ) {
13831 $error = '';
13832
13833 if ( is_object( $install ) ) {
13834 switch ( $install->error->code ) {
13835 case 'user_exist':
13836 $error = (
13837 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13838 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
13839 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>' ) .
13840 sprintf(
13841 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13842 $this->get_account_url( 'change_owner', array(
13843 'state' => 'init',
13844 'candidate_email' => $new_email_address
13845 ) ),
13846 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
13847 )
13848 );
13849 break;
13850 }
13851 }
13852
13853 if ( empty( $error ) ) {
13854 $error = FS_Api::is_api_error_object( $install ) ?
13855 $install->error->message :
13856 var_export( $install->error, true );
13857 }
13858
13859 self::shoot_ajax_failure( $error );
13860 } else {
13861 if (
13862 // If successful ownership change.
13863 $this->get_user()->id != $install->user_id ||
13864 ! empty( $new_email_address )
13865 ) {
13866 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
13867 }
13868 }
13869
13870 self::shoot_ajax_success();
13871 }
13872
13873 /**
13874 * @author Leo Fajardo (@leorw)
13875 * @since 2.3.2.14
13876 */
13877 function starting_migration() {
13878 if ( ! empty( $this->_storage->license_migration ) ) {
13879 // Do not overwrite the data if already set.
13880 return;
13881 }
13882
13883 $this->_storage->license_migration = array(
13884 'is_migrating' => true,
13885 'start_timestamp' => time()
13886 );
13887 }
13888
13889 /**
13890 * @author Leo Fajardo (@leorw)
13891 * @since 2.3.2.14
13892 */
13893 function is_migration() {
13894 if ( $this->is_addon() ) {
13895 return $this->get_parent_instance()->is_migration();
13896 }
13897
13898 if ( empty( $this->_storage->license_migration ) ) {
13899 return false;
13900 }
13901
13902 if ( ! $this->_storage->license_migration['is_migrating'] ) {
13903 return false;
13904 }
13905
13906 return (
13907 // Return `true` if the migration is within 5 minutes from the starting time.
13908 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
13909 );
13910 }
13911
13912 /**
13913 *
13914 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
13915 *
13916 * @author Vova Feldman (@svovaf)
13917 * @since 2.3.0
13918 *
13919 * @param string $license_key
13920 * @param null|bool $is_marketing_allowed
13921 * @param null|number $plugin_id
13922 * @param array $sites
13923 * @param int $blog_id
13924 *
13925 * @return array {
13926 * @var bool $success
13927 * @var string $error
13928 * @var string $next_page
13929 * }
13930 *
13931 * @uses Freemius::activate_license()
13932 */
13933 function activate_migrated_license(
13934 $license_key,
13935 $is_marketing_allowed = null,
13936 $plugin_id = null,
13937 $sites = array(),
13938 $blog_id = null
13939 ) {
13940 $this->_logger->entrance();
13941
13942 $result = $this->activate_license(
13943 $license_key,
13944 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
13945 $this->get_sites_for_network_level_optin() :
13946 $sites,
13947 $is_marketing_allowed,
13948 $blog_id,
13949 $plugin_id
13950 );
13951
13952 // No need to show the sticky after license activation notice after migrating a license.
13953 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13954
13955 return $result;
13956 }
13957
13958 /**
13959 * @author Leo Fajardo (@leorw)
13960 * @since 2.3.1
13961 *
13962 * @return string
13963 */
13964 function get_pricing_js_path() {
13965 if ( ! isset( $this->_pricing_js_path ) ) {
13966 $pricing_js_path = $this->apply_filters( 'freemius_pricing_js_path', '' );
13967
13968 if ( empty( $pricing_js_path ) ) {
13969 global $fs_active_plugins;
13970
13971 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
13972 if ( $data->plugin_path == $this->get_plugin_basename() ) {
13973 $plugin_or_theme_root_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
13974
13975 $pricing_js_path = $plugin_or_theme_root_dir
13976 . '/'
13977 // The basename will be `plugins`, `themes`, or the basename of a custom plugins or themes directory.
13978 . str_replace( '../' . basename( $plugin_or_theme_root_dir ) . '/', '', $sdk_path )
13979 . '/includes/freemius-pricing/freemius-pricing.js';
13980
13981 break;
13982 }
13983 }
13984 }
13985
13986 $this->_pricing_js_path = $pricing_js_path;
13987 }
13988
13989 return $this->_pricing_js_path;
13990 }
13991
13992 /**
13993 * @author Leo Fajardo (@leorw)
13994 * @since 2.3.1
13995 *
13996 * @return bool
13997 */
13998 function should_use_external_pricing() {
13999 if ( is_null( $this->_use_external_pricing ) ) {
14000 $pricing_js_path = $this->get_pricing_js_path();
14001
14002 $this->_use_external_pricing = ( empty( $pricing_js_path ) || ! file_exists( $pricing_js_path ) );
14003 }
14004
14005 return $this->_use_external_pricing;
14006 }
14007
14008 /**
14009 * The implementation of this method was previously in `_activate_license_ajax_action()`.
14010 *
14011 * @author Vova Feldman (@svovaf)
14012 * @since 2.2.4
14013 * @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).
14014 * @param string $license_key
14015 * @param array $sites
14016 * @param null|bool $is_marketing_allowed
14017 * @param null|int $blog_id
14018 * @param null|number $plugin_id
14019 * @param null|number $license_owner_id
14020 * @param bool|null $is_extensions_tracking_allowed
14021 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint.
14022 *
14023 *
14024 * @return array {
14025 * @var bool $success
14026 * @var string $error
14027 * @var string $next_page
14028 * }
14029 */
14030 private function activate_license(
14031 $license_key,
14032 $sites = array(),
14033 $is_marketing_allowed = null,
14034 $blog_id = null,
14035 $plugin_id = null,
14036 $license_owner_id = null,
14037 $is_extensions_tracking_allowed = null,
14038 $is_diagnostic_tracking_allowed = null
14039 ) {
14040 $this->_logger->entrance();
14041
14042 $license_key = trim( $license_key );
14043
14044 $is_network_activation_or_migration = (
14045 fs_is_network_admin() ||
14046 ( ! empty( $sites ) && $this->is_migration() )
14047 );
14048
14049 if ( ! $is_network_activation_or_migration ) {
14050 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
14051 $sites = array();
14052 }
14053
14054 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
14055 $this :
14056 $this->get_addon_instance( $plugin_id );
14057
14058 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
14059 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
14060 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
14061 ) );
14062
14063 $error = false;
14064 $next_page = false;
14065
14066 $has_valid_blog_id = is_numeric( $blog_id );
14067
14068 $user = null;
14069
14070 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
14071 /**
14072 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
14073 *
14074 * @author Vova Feldman (@svovaf)
14075 */
14076 $user = $fs->get_parent_instance()->get_current_or_network_user();
14077 } else if ( $fs->is_registered() ) {
14078 $user = $fs->get_current_or_network_user();
14079 }
14080
14081 if ( $has_valid_blog_id ) {
14082 /**
14083 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
14084 *
14085 * @author Leo Fajardo (@leorw)
14086 */
14087 $fs->switch_to_blog( $blog_id );
14088 }
14089
14090 if ( is_object( $user ) ) {
14091 $result = true;
14092
14093 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
14094 // If no specific blog ID was provided, activate the license for all sites in the network.
14095 $blog_2_install_map = array();
14096 $site_ids = array();
14097
14098 foreach ( $sites as $site ) {
14099 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
14100 continue;
14101 }
14102
14103 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
14104
14105 if ( is_object( $install ) ) {
14106 $blog_2_install_map[ $site['blog_id'] ] = $install;
14107 } else {
14108 $site_ids[] = $site['blog_id'];
14109 }
14110 }
14111
14112 if ( ! empty( $blog_2_install_map ) ) {
14113 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
14114 }
14115
14116 if ( true === $result && ! empty( $site_ids ) ) {
14117 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
14118 }
14119 } else {
14120 if ( $fs->is_registered() ) {
14121 $params = array(
14122 'license_key' => $fs->apply_filters( 'license_key', $license_key )
14123 );
14124
14125 $install_ids = array();
14126
14127 $change_owner = FS_User::is_valid_id( $license_owner_id );
14128
14129 if ( $change_owner ) {
14130 $params['user_id'] = $license_owner_id;
14131
14132 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
14133
14134 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
14135 $install_ids[ $slug ] = $install_info['install']->id;
14136 }
14137
14138 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
14139 }
14140
14141 $api = $fs->get_api_site_scope();
14142
14143 $result = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
14144
14145 if ( ! FS_Api::is_api_error( $result ) ) {
14146 $install = $result;
14147
14148 $fs->reconnect_locally( $has_valid_blog_id );
14149
14150 if (
14151 $change_owner &&
14152 // If successful ownership change.
14153 $fs->get_user()->id != $install->user_id
14154 ) {
14155 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
14156 }
14157 }
14158 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
14159 $result = $fs->activate_license_on_site( $user, $license_key );
14160 }
14161 }
14162
14163 $is_connected = null;
14164
14165 if ( true !== $result && ! FS_Api::is_api_result_entity( $result ) ) {
14166 if ( FS_Api::is_blocked( $result ) ) {
14167 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
14168
14169 $is_connected = false;
14170 }
14171
14172 $error = FS_Api::is_api_error_object( $result ) ?
14173 $result->error->message :
14174 var_export( $result, true );
14175 } else {
14176 $is_connected = true;
14177
14178 $fs->network_upgrade_mode_completed();
14179
14180 $fs->_user = $user;
14181
14182 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
14183 $fs->_site = $fs->get_network_install();
14184 }
14185
14186 $fs->_sync_license( true, $has_valid_blog_id );
14187
14188 $this->maybe_sync_install_user();
14189
14190 $next_page = $fs->is_addon() ?
14191 $fs->get_parent_instance()->get_account_url() :
14192 $fs->get_after_activation_url( 'after_connect_url' );
14193 }
14194
14195 $fs->update_connectivity_info( $is_connected );
14196 } else {
14197 $next_page = $fs->opt_in(
14198 false,
14199 false,
14200 false,
14201 $license_key,
14202 false,
14203 false,
14204 false,
14205 $is_marketing_allowed,
14206 $sites
14207 );
14208
14209 if ( isset( $next_page->error ) ) {
14210 $error = $next_page->error;
14211 } else {
14212 if ( $is_network_activation_or_migration ) {
14213 /**
14214 * Get the list of sites that were just opted-in (and license activated).
14215 * This is an optimization for the next part below saving some DB queries.
14216 */
14217 $connected_sites = array();
14218 foreach ( $sites as $site ) {
14219 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
14220 $connected_sites[ $site['blog_id'] ] = true;
14221 }
14222 }
14223
14224 $all_sites = self::get_sites();
14225 $pending_blog_ids = array();
14226
14227 /**
14228 * 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.
14229 *
14230 * @author Vova Feldman (@svovaf)
14231 */
14232 foreach ( $all_sites as $site ) {
14233 $blog_id = self::get_site_blog_id( $site );
14234
14235 if ( isset( $connected_sites[ $blog_id ] ) ) {
14236 // Site was just connected.
14237 continue;
14238 }
14239
14240 if ( $fs->is_installed_on_site( $blog_id ) ) {
14241 // Site was already connected before.
14242 continue;
14243 }
14244
14245 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
14246 // Site's connection was delegated.
14247 continue;
14248 }
14249
14250 if ( $fs->is_anonymous_site( $blog_id ) ) {
14251 // Site connection was already skipped.
14252 continue;
14253 }
14254
14255 $pending_blog_ids[] = $blog_id;
14256 }
14257
14258 if ( ! empty( $pending_blog_ids ) ) {
14259 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
14260 $fs->skip_connection( $pending_blog_ids );
14261 } else {
14262 $fs->delegate_connection( $pending_blog_ids );
14263 }
14264 }
14265 }
14266 }
14267 }
14268
14269 if ( false === $error && true === $fs->_storage->require_license_activation ) {
14270 $fs->_storage->require_license_activation = false;
14271 }
14272
14273 $result = array(
14274 'success' => ( false === $error )
14275 );
14276
14277 if ( false !== $error ) {
14278 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
14279 } else {
14280 if ( $fs->is_addon() || $fs->has_addons() ) {
14281 /**
14282 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
14283 * an updated valid user licenses collection will be fetched from the server which is used to also
14284 * update the account add-ons (add-ons the user has licenses for).
14285 *
14286 * @author Leo Fajardo (@leorw)
14287 * @since 2.2.4
14288 */
14289 $fs->purge_valid_user_licenses_cache();
14290 }
14291
14292 $result['next_page'] = $next_page;
14293 }
14294
14295 return $result;
14296 }
14297
14298 /**
14299 * @author Leo Fajardo (@leorw)
14300 * @since 2.3.2
14301 *
14302 * @return array {
14303 * @key string Product slug.
14304 * @value array {
14305 * @property FS_Site $site
14306 * @property FS_Plugin_License $license
14307 * }
14308 * }
14309 */
14310 private function get_parent_and_addons_installs_info() {
14311 $fs = $this->is_addon() ?
14312 $this->get_parent_instance() :
14313 $this;
14314
14315 $installed_addons_ids = array();
14316
14317 $installed_addons_instances = $fs->get_installed_addons();
14318 foreach ( $installed_addons_instances as $instance ) {
14319 $installed_addons_ids[] = $instance->get_id();
14320 }
14321
14322 $addons_ids = array_unique( array_merge(
14323 $installed_addons_ids,
14324 $fs->get_updated_account_addons()
14325 ) );
14326
14327 // Add parent product info.
14328 $installs_info_by_slug_map = array(
14329 $fs->get_slug() => array(
14330 'install' => $fs->get_site(),
14331 'license' => $fs->_get_license()
14332 )
14333 );
14334
14335 foreach ( $addons_ids as $addon_id ) {
14336 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
14337
14338 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
14339
14340 if ( ! isset( $addon_info['is_connected'] ) || ! $addon_info['is_connected'] ) {
14341 // Add-on is not associated with an install entity.
14342 continue;
14343 }
14344
14345 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
14346 'install' => $addon_info['site'],
14347 'license' => isset( $addon_info['license'] ) ?
14348 $addon_info['license'] :
14349 null
14350 );
14351 }
14352
14353 return $installs_info_by_slug_map;
14354 }
14355
14356 /**
14357 * @author Leo Fajardo (@leorw)
14358 * @since 1.2.3.1
14359 */
14360 function _network_activate_ajax_action() {
14361 $this->_logger->entrance();
14362
14363 $this->check_ajax_referer( 'network_activate' );
14364
14365 $plugin_id = fs_request_get( 'module_id', '', 'post' );
14366 $fs = ( $plugin_id == $this->_module_id ) ?
14367 $this :
14368 $this->get_addon_instance( $plugin_id );
14369
14370 $error = false;
14371
14372 $sites = fs_request_get( 'sites', array(), 'post' );
14373 if ( is_array( $sites ) && ! empty( $sites ) ) {
14374 $sites_by_action = array(
14375 'allow' => array(),
14376 'delegate' => array(),
14377 'skip' => array()
14378 );
14379
14380 foreach ( $sites as $site ) {
14381 $sites_by_action[ $site['action'] ][] = $site;
14382 }
14383
14384 $total_sites = count( $sites );
14385 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
14386
14387 $next_page = '';
14388
14389 $has_any_install = fs_request_get_bool( 'has_any_install' );
14390
14391 if ( $total_sites === $total_sites_to_delegate &&
14392 ! $this->is_network_upgrade_mode() &&
14393 ! $has_any_install
14394 ) {
14395 $this->delegate_connection();
14396 } else {
14397 if ( ! empty( $sites_by_action['delegate'] ) ) {
14398 $this->delegate_connection( self::get_sites_blog_ids( $sites_by_action['delegate'] ) );
14399 }
14400
14401 if ( ! empty( $sites_by_action['skip'] ) ) {
14402 $this->skip_connection( self::get_sites_blog_ids( $sites_by_action['skip'] ) );
14403 }
14404
14405 if ( empty( $sites_by_action['allow'] ) ) {
14406 if ( $has_any_install ) {
14407 $first_install = $fs->find_first_install();
14408
14409 if ( ! is_null( $first_install ) ) {
14410 $fs->_site = $first_install['install'];
14411 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
14412
14413 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
14414 $fs->_storage->network_user_id = $fs->_user->id;
14415 }
14416 }
14417 } else {
14418 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
14419 $next_page = $fs->opt_in(
14420 false,
14421 false,
14422 false,
14423 false,
14424 false,
14425 false,
14426 false,
14427 fs_request_get_bool( 'is_marketing_allowed', null ),
14428 $sites_by_action['allow']
14429 );
14430 } else {
14431 $next_page = $fs->install_with_user(
14432 $this->get_network_user(),
14433 false,
14434 false,
14435 false,
14436 true,
14437 $sites_by_action['allow']
14438 );
14439 }
14440
14441 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
14442 $error = $next_page->error;
14443 }
14444 }
14445 }
14446
14447 if ( empty( $next_page ) ) {
14448 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
14449 }
14450 } else {
14451 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
14452 }
14453
14454 $result = array(
14455 'success' => ( false === $error )
14456 );
14457
14458 if ( false !== $error ) {
14459 $result['error'] = $error;
14460 } else {
14461 $result['next_page'] = $next_page;
14462 }
14463
14464 echo json_encode( $result );
14465
14466 exit;
14467 }
14468
14469 /**
14470 * Billing update AJAX callback.
14471 *
14472 * @author Vova Feldman (@svovaf)
14473 * @since 1.2.1.5
14474 */
14475 function _update_billing_ajax_action() {
14476 $this->_logger->entrance();
14477
14478 $this->check_ajax_referer( 'update_billing' );
14479
14480 if ( ! $this->is_user_admin() ) {
14481 // Only for admins.
14482 self::shoot_ajax_failure();
14483 }
14484
14485 $billing = fs_request_get( 'billing' );
14486
14487 $api = $this->get_api_user_scope();
14488 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
14489 'plugin_id' => $this->get_parent_id(),
14490 ) ) );
14491
14492 if ( ! $this->is_api_result_entity( $result ) ) {
14493 self::shoot_ajax_failure();
14494 }
14495
14496 // Purge cached billing.
14497 $this->get_api_user_scope()->purge_cache( 'billing.json' );
14498
14499 self::shoot_ajax_success();
14500 }
14501
14502 /**
14503 * Trial start for anonymous users (AJAX callback).
14504 *
14505 * @author Vova Feldman (@svovaf)
14506 * @since 1.2.1.5
14507 */
14508 function _start_trial_ajax_action() {
14509 $this->_logger->entrance();
14510
14511 $this->check_ajax_referer( 'start_trial' );
14512
14513 if ( ! $this->is_user_admin() ) {
14514 // Only for admins.
14515 self::shoot_ajax_failure();
14516 }
14517
14518 $trial_data = fs_request_get( 'trial' );
14519
14520 $next_page = $this->opt_in(
14521 false,
14522 false,
14523 false,
14524 false,
14525 false,
14526 $trial_data['plan_id']
14527 );
14528
14529 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14530 self::shoot_ajax_failure(
14531 isset( $next_page->error ) ?
14532 $next_page->error->message :
14533 var_export( $next_page, true )
14534 );
14535 }
14536
14537 $this->shoot_ajax_success( array(
14538 'next_page' => $next_page,
14539 ) );
14540 }
14541
14542 /**
14543 * @author Leo Fajardo (@leorw)
14544 * @since 1.2.0
14545 */
14546 function _resend_license_key_ajax_action() {
14547 $this->_logger->entrance();
14548
14549 $this->check_ajax_referer( 'resend_license_key' );
14550
14551 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14552
14553 if ( empty( $email_address ) ) {
14554 exit;
14555 }
14556
14557 $error = false;
14558
14559 $api = $this->get_api_plugin_scope();
14560 $result = $api->call( '/licenses/resend.json', 'post',
14561 array(
14562 'email' => $email_address,
14563 'url' => home_url(),
14564 )
14565 );
14566
14567 if ( is_object( $result ) && isset( $result->error ) ) {
14568 $error = $result->error;
14569
14570 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14571 $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' );
14572 } else if ( 'no_license' === $error->code ) {
14573 $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' );
14574 } else {
14575 $error = $error->message;
14576 }
14577 }
14578
14579 $licenses = array(
14580 'success' => ( false === $error )
14581 );
14582
14583 if ( false !== $error ) {
14584 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14585 }
14586
14587 echo json_encode( $licenses );
14588
14589 exit;
14590 }
14591
14592 /**
14593 * @author Vova Feldman (@svovaf)
14594 * @since 1.2.1.8
14595 *
14596 * @var string
14597 */
14598 private static $_pagenow;
14599
14600 /**
14601 * Get current page or the referer if executing a WP AJAX request.
14602 *
14603 * @author Vova Feldman (@svovaf)
14604 * @since 1.2.1.8
14605 *
14606 * @return string
14607 */
14608 static function get_current_page() {
14609 if ( ! isset( self::$_pagenow ) ) {
14610 global $pagenow;
14611 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14612 /**
14613 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14614 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14615 *
14616 * @author Leo Fajardo (@leorw)
14617 * @since 2.2.3
14618 */
14619 if ( is_network_admin() ) {
14620 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14621 } else if ( is_user_admin() ) {
14622 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14623 } else {
14624 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14625 }
14626
14627 $pagenow = $self_matches[1];
14628 $pagenow = trim( $pagenow, '/' );
14629 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14630 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14631 $pagenow = 'index.php';
14632 } else {
14633 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14634 $pagenow = strtolower( $self_matches[1] );
14635 if ( '.php' !== substr($pagenow, -4, 4) )
14636 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14637 }
14638 }
14639
14640 self::$_pagenow = $pagenow;
14641
14642 if ( self::is_ajax() &&
14643 'admin-ajax.php' === $pagenow
14644 ) {
14645 $referer = fs_get_raw_referer();
14646
14647 if ( is_string( $referer ) ) {
14648 $parts = explode( '?', $referer );
14649
14650 self::$_pagenow = basename( $parts[0] );
14651 }
14652 }
14653 }
14654
14655 return self::$_pagenow;
14656 }
14657
14658 /**
14659 * Helper method to check if user in the plugins page.
14660 *
14661 * @author Vova Feldman (@svovaf)
14662 * @since 1.2.1.5
14663 *
14664 * @return bool
14665 */
14666 static function is_plugins_page() {
14667 return ( 'plugins.php' === self::get_current_page() );
14668 }
14669
14670 /**
14671 * @author Leo Fajardo (@leorw)
14672 * @since 2.2.3
14673 *
14674 * @return bool
14675 */
14676 static function is_plugin_install_page() {
14677 return ( 'plugin-install.php' === self::get_current_page() );
14678 }
14679
14680 /**
14681 * @author Leo Fajardo (@leorw)
14682 * @since 2.0.2
14683 *
14684 * @return bool
14685 */
14686 static function is_updates_page() {
14687 return ( 'update-core.php' === self::get_current_page() );
14688 }
14689
14690 /**
14691 * Helper method to check if user in the themes page.
14692 *
14693 * @author Vova Feldman (@svovaf)
14694 * @since 1.2.2.6
14695 *
14696 * @return bool
14697 */
14698 static function is_themes_page() {
14699 return ( 'themes.php' === self::get_current_page() );
14700 }
14701
14702 #----------------------------------------------------------------------------------
14703 #region Affiliation
14704 #----------------------------------------------------------------------------------
14705
14706 /**
14707 * @author Leo Fajardo (@leorw)
14708 * @since 1.2.3
14709 *
14710 * @return bool
14711 */
14712 function has_affiliate_program() {
14713 if ( ! is_object( $this->_plugin ) ) {
14714 return false;
14715 }
14716
14717 return $this->_plugin->has_affiliate_program();
14718 }
14719
14720 /**
14721 * Get Plugin ID under which we will track affiliate application.
14722 *
14723 * This could either be the Bundle ID or the main plugin ID.
14724 *
14725 * @return number Bundle ID if developer has provided one, else the main plugin ID.
14726 */
14727 private function get_plugin_id_for_affiliate_terms() {
14728 return $this->has_bundle_context() ?
14729 $this->get_bundle_id() :
14730 $this->_plugin->id;
14731 }
14732
14733 /**
14734 * @author Leo Fajardo (@leorw)
14735 * @since 1.2.4
14736 */
14737 private function fetch_affiliate_terms() {
14738 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14739 /**
14740 * In case we have a bundle set in SDK configuration, we would like to use that for affiliates, not the main plugin.
14741 */
14742 $plugins_api = $this->has_bundle_context() ?
14743 $this->get_api_bundle_scope() :
14744 $this->get_api_plugin_scope();
14745
14746 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14747
14748 /**
14749 * 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.
14750 */
14751 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14752 return;
14753 }
14754
14755 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14756 }
14757 }
14758
14759 /**
14760 * @author Leo Fajardo (@leorw)
14761 * @since 1.2.4
14762 */
14763 private function fetch_affiliate_and_custom_terms() {
14764 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14765 $application_data = $this->_storage->affiliate_application_data;
14766 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14767
14768 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14769
14770 $users_api = $this->get_api_user_scope();
14771 $result = $users_api->get( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14772 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14773 if ( ! empty( $result->affiliates ) ) {
14774 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14775
14776 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14777 $application_data['status'] = $affiliate->status;
14778 $this->_storage->affiliate_application_data = $application_data;
14779 }
14780
14781 if ( $affiliate->is_using_custom_terms ) {
14782 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14783 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14784 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14785 }
14786 }
14787
14788 $this->affiliate = $affiliate;
14789 }
14790 }
14791 }
14792 }
14793
14794 /**
14795 * @author Leo Fajardo (@leorw)
14796 * @since 1.2.3
14797 */
14798 private function fetch_affiliate_and_terms() {
14799 $this->_logger->entrance();
14800
14801 $this->fetch_affiliate_terms();
14802 $this->fetch_affiliate_and_custom_terms();
14803 }
14804
14805 /**
14806 * @author Leo Fajardo (@leorw)
14807 * @since 1.2.3
14808 *
14809 * @return FS_Affiliate
14810 */
14811 function get_affiliate() {
14812 return $this->affiliate;
14813 }
14814
14815
14816 /**
14817 * @author Leo Fajardo (@leorw)
14818 * @since 1.2.3
14819 *
14820 * @return FS_AffiliateTerms
14821 */
14822 function get_affiliate_terms() {
14823 return is_object( $this->custom_affiliate_terms ) ?
14824 $this->custom_affiliate_terms :
14825 $this->plugin_affiliate_terms;
14826 }
14827
14828 /**
14829 * @author Leo Fajardo (@leorw)
14830 * @since 1.2.3
14831 */
14832 function _submit_affiliate_application() {
14833 $this->_logger->entrance();
14834
14835 $this->check_ajax_referer( 'submit_affiliate_application' );
14836
14837 if ( ! $this->is_user_admin() ) {
14838 // Only for admins.
14839 self::shoot_ajax_failure();
14840 }
14841
14842 $affiliate = fs_request_get( 'affiliate' );
14843
14844 if ( empty( $affiliate['promotion_methods'] ) ) {
14845 unset( $affiliate['promotion_methods'] );
14846 }
14847
14848 if ( ! empty( $affiliate['additional_domains'] ) ) {
14849 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
14850 }
14851
14852 if ( ! $this->is_registered() ) {
14853 $email_address = isset( $affiliate['email'] ) ? $affiliate['email'] : '';
14854
14855 if ( ! is_email( $email_address ) ) {
14856 self::shoot_ajax_failure('Invalid email address.');
14857 }
14858
14859 // Opt in but don't track usage.
14860 $next_page = $this->opt_in(
14861 $email_address,
14862 false,
14863 false,
14864 false,
14865 false,
14866 false,
14867 true
14868 );
14869
14870 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14871 self::shoot_ajax_failure(
14872 isset( $next_page->error ) ?
14873 $next_page->error->message :
14874 var_export( $next_page, true )
14875 );
14876 } else if ( $this->is_pending_activation() ) {
14877 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' ) );
14878 }
14879 }
14880
14881 $this->fetch_affiliate_terms();
14882
14883 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14884
14885 $api = $this->get_api_user_scope();
14886 $result = $api->call(
14887 ( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
14888 'post',
14889 $affiliate
14890 );
14891
14892 if ( $this->is_api_error( $result ) ) {
14893 self::shoot_ajax_failure(
14894 isset( $result->error ) ?
14895 $result->error->message :
14896 var_export( $result, true )
14897 );
14898 } else {
14899 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
14900 $this->_admin_notices->remove_sticky( 'affiliate_program' );
14901 }
14902
14903 $affiliate_application_data = array(
14904 'status' => 'pending',
14905 'stats_description' => $affiliate['stats_description'],
14906 'promotion_method_description' => $affiliate['promotion_method_description'],
14907 );
14908
14909 if ( ! empty( $affiliate['promotion_methods'] ) ) {
14910 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
14911 }
14912
14913 if ( ! empty( $affiliate['domain'] ) ) {
14914 $affiliate_application_data['domain'] = $affiliate['domain'];
14915 }
14916
14917 if ( ! empty( $affiliate['additional_domains'] ) ) {
14918 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
14919 }
14920
14921 $this->_storage->affiliate_application_data = $affiliate_application_data;
14922 }
14923
14924 // Purge cached affiliate.
14925 $api->purge_cache( 'affiliate.json' );
14926
14927 self::shoot_ajax_success( $result );
14928 }
14929
14930 /**
14931 * @author Leo Fajardo (@leorw)
14932 * @since 1.2.3
14933 *
14934 * @return array|null
14935 */
14936 function get_affiliate_application_data() {
14937 if ( empty( $this->_storage->affiliate_application_data ) ) {
14938 return null;
14939 }
14940
14941 return $this->_storage->affiliate_application_data;
14942 }
14943
14944 #endregion Affiliation ------------------------------------------------------------
14945
14946 #----------------------------------------------------------------------------------
14947 #region URL Generators
14948 #----------------------------------------------------------------------------------
14949
14950 /**
14951 * Alias to pricing_url().
14952 *
14953 * @author Vova Feldman (@svovaf)
14954 * @since 1.0.2
14955 *
14956 * @uses pricing_url()
14957 *
14958 * @param string $period Billing cycle
14959 * @param bool $is_trial
14960 *
14961 * @return string
14962 */
14963 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14964 return $this->pricing_url( $period, $is_trial );
14965 }
14966
14967 /**
14968 * @author Vova Feldman (@svovaf)
14969 * @since 1.0.9
14970 *
14971 * @uses get_upgrade_url()
14972 *
14973 * @return string
14974 */
14975 function get_trial_url() {
14976 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
14977 }
14978
14979 /**
14980 * @author Leo Fajardo (@leorw)
14981 * @since 2.1.4
14982 *
14983 * @param string $new_version
14984 *
14985 * @return string
14986 */
14987 function version_upgrade_checkout_link( $new_version ) {
14988 if ( ! is_object( $this->_license ) ) {
14989 $url = $this->pricing_url();
14990
14991 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
14992 } else {
14993 $subscription = $this->_get_subscription( $this->_license->id );
14994
14995 $url = $this->checkout_url(
14996 is_object( $subscription ) ?
14997 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
14998 WP_FS__PERIOD_LIFETIME,
14999 false,
15000 array( 'licenses' => $this->_license->quota )
15001 );
15002
15003 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
15004 }
15005
15006 return sprintf(
15007 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
15008 sprintf(
15009 '<a href="%s">%s</a>',
15010 $this->apply_filters( 'update_notice_checkout_url', $url ),
15011 $purchase_license_text
15012 ),
15013 $new_version
15014 );
15015 }
15016
15017 /**
15018 * Plugin's pricing URL.
15019 *
15020 * @author Vova Feldman (@svovaf)
15021 * @since 1.0.4
15022 *
15023 * @param string $billing_cycle Billing cycle
15024 *
15025 * @param bool $is_trial
15026 *
15027 * @return string
15028 */
15029 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
15030 $this->_logger->entrance();
15031
15032 $params = array(
15033 'billing_cycle' => $billing_cycle
15034 );
15035
15036 if ( $is_trial ) {
15037 $params['trial'] = 'true';
15038 }
15039
15040 $url = $this->is_addon() ?
15041 $this->_parent->addon_url( $this->_slug ) :
15042 $this->_get_admin_page_url( 'pricing', $params );
15043
15044 return $this->apply_filters( 'pricing_url', $url );
15045 }
15046
15047 /**
15048 * Checkout page URL.
15049 *
15050 * @author Vova Feldman (@svovaf)
15051 * @since 1.0.6
15052 *
15053 * @param string $billing_cycle Billing cycle
15054 * @param bool $is_trial
15055 * @param array $extra (optional) Extra parameters, override other query params.
15056 * @param bool|null $network
15057 *
15058 * @return string
15059 */
15060 function checkout_url(
15061 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
15062 $is_trial = false,
15063 $extra = array(),
15064 $network = null
15065 ) {
15066 $this->_logger->entrance();
15067
15068 $params = array(
15069 'checkout' => 'true',
15070 'billing_cycle' => $billing_cycle,
15071 );
15072
15073 if ( $is_trial ) {
15074 $params['trial'] = 'true';
15075 }
15076
15077 /**
15078 * Params in extra override other params.
15079 */
15080 $params = array_merge( $params, $extra );
15081
15082 return $this->apply_filters( 'checkout_url', $this->_get_admin_page_url( 'pricing', $params, $network ) );
15083 }
15084
15085 /**
15086 * Add-on checkout URL.
15087 *
15088 * @author Vova Feldman (@svovaf)
15089 * @since 1.1.7
15090 *
15091 * @param number $addon_id
15092 * @param number $pricing_id
15093 * @param string $billing_cycle
15094 * @param bool $is_trial
15095 * @param bool|null $network
15096 *
15097 * @return string
15098 */
15099 function addon_checkout_url(
15100 $addon_id,
15101 $pricing_id,
15102 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
15103 $is_trial = false,
15104 $network = null
15105 ) {
15106 return $this->checkout_url( $billing_cycle, $is_trial, array(
15107 'plugin_id' => $addon_id,
15108 'pricing_id' => $pricing_id,
15109 ), $network );
15110 }
15111
15112 #endregion
15113
15114 #endregion ------------------------------------------------------------------
15115
15116 /**
15117 * Check if plugin has any add-ons.
15118 *
15119 * @author Vova Feldman (@svovaf)
15120 * @since 1.0.5
15121 *
15122 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
15123 *
15124 * @return bool
15125 */
15126 function has_addons() {
15127 $this->_logger->entrance();
15128
15129 return $this->_has_addons;
15130 }
15131
15132 /**
15133 * Check if plugin can work in anonymous mode.
15134 *
15135 * @author Vova Feldman (@svovaf)
15136 * @since 1.0.9
15137 *
15138 * @return bool
15139 *
15140 * @deprecated Please use is_enable_anonymous() instead.
15141 */
15142 function enable_anonymous() {
15143 return $this->_enable_anonymous;
15144 }
15145
15146 /**
15147 * Check if plugin can work in anonymous mode.
15148 *
15149 * @author Vova Feldman (@svovaf)
15150 * @since 1.1.9
15151 *
15152 * @return bool
15153 */
15154 function is_enable_anonymous() {
15155 return $this->_enable_anonymous;
15156 }
15157
15158 /**
15159 * Check if plugin is premium only (no free plans).
15160 *
15161 * @author Vova Feldman (@svovaf)
15162 * @since 1.1.9
15163 *
15164 * @return bool
15165 */
15166 function is_only_premium() {
15167 return $this->_is_premium_only;
15168 }
15169
15170 /**
15171 * Checks if the plugin's type is "plugin". The other type is "theme".
15172 *
15173 * @author Leo Fajardo (@leorw)
15174 * @since 1.2.2
15175 *
15176 * @return bool
15177 */
15178 function is_plugin() {
15179 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
15180 }
15181
15182 /**
15183 * @author Leo Fajardo (@leorw)
15184 * @since 1.2.2
15185 *
15186 * @return string
15187 */
15188 function get_module_type() {
15189 if ( ! isset( $this->_module_type ) ) {
15190 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
15191 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
15192 }
15193
15194 return $this->_module_type;
15195 }
15196
15197 /**
15198 * @author Leo Fajardo (@leorw)
15199 * @since 1.2.2
15200 *
15201 * @return string
15202 */
15203 function get_plugin_main_file_path() {
15204 return $this->_plugin_main_file_path;
15205 }
15206
15207 /**
15208 * Check if module has a premium code version.
15209 *
15210 * Serviceware module might be freemium without any
15211 * premium code version, where the paid features
15212 * are all part of the service.
15213 *
15214 * @author Vova Feldman (@svovaf)
15215 * @since 1.2.1.6
15216 *
15217 * @return bool
15218 */
15219 function has_premium_version() {
15220 return $this->_has_premium_version;
15221 }
15222
15223 /**
15224 * Check if feature supported with current site's plan.
15225 *
15226 * @author Vova Feldman (@svovaf)
15227 * @since 1.0.1
15228 *
15229 * @todo IMPLEMENT
15230 *
15231 * @param number $feature_id
15232 *
15233 * @throws Exception
15234 */
15235 function is_feature_supported( $feature_id ) {
15236 throw new Exception( 'not implemented' );
15237 }
15238
15239 /**
15240 * @author Vova Feldman (@svovaf)
15241 * @since 1.0.1
15242 *
15243 * @return bool Is running in SSL/HTTPS
15244 */
15245 function is_ssl() {
15246 return WP_FS__IS_HTTPS;
15247 }
15248
15249 /**
15250 * @author Vova Feldman (@svovaf)
15251 * @since 1.0.9
15252 *
15253 * @return bool Is running in AJAX call.
15254 *
15255 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
15256 */
15257 static function is_ajax() {
15258 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
15259 }
15260
15261 /**
15262 * Check if it's an AJAX call targeted for the current module.
15263 *
15264 * @author Vova Feldman (@svovaf)
15265 * @since 1.2.0
15266 *
15267 * @param array|string $actions Collection of AJAX actions.
15268 *
15269 * @return bool
15270 */
15271 function is_ajax_action( $actions ) {
15272 // Verify it's an ajax call.
15273 if ( ! self::is_ajax() ) {
15274 return false;
15275 }
15276
15277 // Verify the call is relevant for the plugin.
15278 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
15279 return false;
15280 }
15281
15282 // Verify it's one of the specified actions.
15283 if ( is_string( $actions ) ) {
15284 $actions = explode( ',', $actions );
15285 }
15286
15287 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15288 $ajax_action = fs_request_get( 'action' );
15289
15290 foreach ( $actions as $action ) {
15291 if ( $ajax_action === $this->get_action_tag( $action ) ) {
15292 return true;
15293 }
15294 }
15295 }
15296
15297 return false;
15298 }
15299
15300 /**
15301 * Check if it's an AJAX call targeted for current request.
15302 *
15303 * @author Vova Feldman (@svovaf)
15304 * @since 1.2.0
15305 *
15306 * @param array|string $actions Collection of AJAX actions.
15307 * @param number|null $module_id
15308 *
15309 * @return bool
15310 */
15311 static function is_ajax_action_static( $actions, $module_id = null ) {
15312 // Verify it's an ajax call.
15313 if ( ! self::is_ajax() ) {
15314 return false;
15315 }
15316
15317
15318 if ( ! empty( $module_id ) ) {
15319 // Verify the call is relevant for the plugin.
15320 if ( $module_id != fs_request_get( 'module_id' ) ) {
15321 return false;
15322 }
15323 }
15324
15325 // Verify it's one of the specified actions.
15326 if ( is_string( $actions ) ) {
15327 $actions = explode( ',', $actions );
15328 }
15329
15330 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15331 $ajax_action = fs_request_get( 'action' );
15332
15333 foreach ( $actions as $action ) {
15334 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
15335 return true;
15336 }
15337 }
15338 }
15339
15340 return false;
15341 }
15342
15343 /**
15344 * @author Vova Feldman (@svovaf)
15345 * @since 1.1.7
15346 *
15347 * @return bool
15348 */
15349 static function is_cron() {
15350 return ( defined( 'DOING_CRON' ) && DOING_CRON );
15351 }
15352
15353 /**
15354 * @author Leo Fajardo (@leorw)
15355 * @since 2.5.0
15356 *
15357 * @return bool
15358 */
15359 static function is_admin_post() {
15360 return ( 'admin-post.php' === self::get_current_page() );
15361 }
15362
15363 /**
15364 * Check if a real user is visiting the admin dashboard.
15365 *
15366 * @author Vova Feldman (@svovaf)
15367 * @since 1.1.7
15368 *
15369 * @return bool
15370 */
15371 function is_user_in_admin() {
15372 return (
15373 is_admin() &&
15374 ! self::is_ajax() &&
15375 ! self::is_cron() &&
15376 ! self::is_admin_post()
15377 );
15378 }
15379
15380 /**
15381 * Check if a real user is in the customizer view.
15382 *
15383 * @author Vova Feldman (@svovaf)
15384 * @since 1.2.2.7
15385 *
15386 * @return bool
15387 */
15388 static function is_customizer() {
15389 return is_customize_preview();
15390 }
15391
15392 /**
15393 * Check if running in HTTPS and if site's plan matching the specified plan.
15394 *
15395 * @param string $plan
15396 * @param bool $exact
15397 *
15398 * @return bool
15399 */
15400 function is_ssl_and_plan( $plan, $exact = false ) {
15401 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
15402 }
15403
15404 /**
15405 * Construct plugin's settings page URL.
15406 *
15407 * @author Vova Feldman (@svovaf)
15408 * @since 1.0.4
15409 *
15410 * @param string $page
15411 * @param array $params
15412 * @param bool|null $network
15413 *
15414 * @return string
15415 */
15416 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
15417 if ( is_null( $network ) ) {
15418 $network = (
15419 $this->_is_network_active &&
15420 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
15421 );
15422 }
15423
15424 if ( 0 < count( $params ) ) {
15425 foreach ( $params as $k => $v ) {
15426 $params[ $k ] = urlencode( $v );
15427 }
15428 }
15429
15430 $page_param = $this->_menu->get_slug( $page );
15431
15432 if ( empty( $page ) &&
15433 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
15434 $this->show_opt_in_on_themes_page()
15435 ) {
15436 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
15437
15438 return add_query_arg(
15439 $params,
15440 $this->admin_url( 'themes.php', 'admin', $network )
15441 );
15442 }
15443
15444 if ( ! $this->has_settings_menu() ) {
15445 if ( ! empty( $page ) ) {
15446 // Module doesn't have a setting page, but since the request is for
15447 // a specific Freemius page, use the admin.php path.
15448 return add_query_arg( array_merge( $params, array(
15449 'page' => $page_param,
15450 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15451 } else {
15452 if ( $this->is_activation_mode() ) {
15453 /**
15454 * @author Vova Feldman
15455 * @since 1.2.1.6
15456 *
15457 * If plugin doesn't have a settings page, create one for the opt-in screen.
15458 */
15459 return add_query_arg( array_merge( $params, array(
15460 'page' => $this->_slug,
15461 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15462 } else {
15463 // Plugin without a settings page.
15464 return add_query_arg(
15465 $params,
15466 $this->admin_url( 'plugins.php', 'admin', $network )
15467 );
15468 }
15469 }
15470 }
15471
15472 // Module has a submenu settings page.
15473 if ( ! $this->_menu->is_top_level() ) {
15474 $parent_slug = $this->_menu->get_parent_slug();
15475 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
15476 $parent_slug :
15477 'admin.php';
15478
15479 return add_query_arg( array_merge( $params, array(
15480 'page' => $page_param,
15481 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
15482 }
15483
15484 // Module has a top level CPT settings page.
15485 if ( $this->_menu->is_cpt() ) {
15486 if ( empty( $page ) && $this->is_activation_mode() ) {
15487 return add_query_arg( array_merge( $params, array(
15488 'page' => $page_param
15489 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15490 } else {
15491 if ( ! empty( $page ) ) {
15492 $params['page'] = $page_param;
15493 }
15494
15495 return add_query_arg(
15496 $params,
15497 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
15498 );
15499 }
15500 }
15501
15502 // Module has a custom top level settings page.
15503 return add_query_arg( array_merge( $params, array(
15504 'page' => $page_param,
15505 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15506 }
15507
15508 #--------------------------------------------------------------------------------
15509 #region Multisite
15510 #--------------------------------------------------------------------------------
15511
15512 /**
15513 * @author Leo Fajardo (@leorw)
15514 * @since 2.0.0
15515 *
15516 * @return bool
15517 */
15518 function is_network_active() {
15519 return $this->_is_network_active;
15520 }
15521
15522 /**
15523 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
15524 *
15525 * @author Leo Fajardo (@leorw)
15526 * @since 2.0.0
15527 *
15528 * @param bool|int[] $all_or_blog_ids
15529 */
15530 private function delegate_connection( $all_or_blog_ids = true ) {
15531 $this->_logger->entrance();
15532
15533 $this->_admin_notices->remove_sticky( 'connect_account' );
15534
15535 if ( true === $all_or_blog_ids ) {
15536 // All sites delegation.
15537 $this->_storage->store( 'is_delegated_connection', true, true );
15538 } else {
15539 // Specified sites delegation.
15540 foreach ( $all_or_blog_ids as $blog_id ) {
15541 $this->delegate_site_connection( $blog_id );
15542 }
15543 }
15544
15545 $this->network_upgrade_mode_completed();
15546 }
15547
15548 /**
15549 * Delegate specific network site conncetion to the site admin.
15550 *
15551 * @author Vova Feldman (@svovaf)
15552 * @since 2.0.0
15553 *
15554 * @param int $blog_id
15555 */
15556 private function delegate_site_connection( $blog_id ) {
15557 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
15558 }
15559
15560 /**
15561 * Check if super-admin delegated the connection of ALL sites to the site admins.
15562 *
15563 * @author Vova Feldman (@svovaf)
15564 * @since 2.0.0
15565 *
15566 * @return bool
15567 */
15568 function is_network_delegated_connection() {
15569 if ( ! $this->_is_network_active ) {
15570 return false;
15571 }
15572
15573 return $this->_storage->get( 'is_delegated_connection', false, true );
15574 }
15575
15576 /**
15577 * @author Leo Fajardo (@leorw)
15578 * @since 2.0.0
15579 *
15580 * @param int $blog_id
15581 *
15582 * @return bool
15583 */
15584 function is_site_delegated_connection( $blog_id = 0 ) {
15585 if ( ! $this->_is_network_active ) {
15586 return false;
15587 }
15588
15589 if ( 0 == $blog_id ) {
15590 $blog_id = get_current_blog_id();
15591 }
15592
15593 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15594 }
15595
15596 /**
15597 * Check if delegated the connection. When running within the network admin,
15598 * and haven't specified the blog ID, checks if network level delegated. If running
15599 * within a site admin or specified a blog ID, check if delegated the connection for
15600 * the current context site.
15601 *
15602 * If executed outside the the admin, check if delegated the connection
15603 * for the current context site OR the whole network.
15604 *
15605 * @author Vova Feldman (@svovaf)
15606 * @since 2.0.0
15607 *
15608 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15609 *
15610 * @return bool
15611 */
15612 function is_delegated_connection( $blog_id = 0 ) {
15613 if ( ! $this->_is_network_active ) {
15614 return false;
15615 }
15616
15617 if ( fs_is_network_admin() && 0 == $blog_id ) {
15618 return $this->is_network_delegated_connection();
15619 }
15620
15621 return (
15622 $this->is_network_delegated_connection() ||
15623 $this->is_site_delegated_connection( $blog_id )
15624 );
15625 }
15626
15627 /**
15628 * Check if the current module is active for the site.
15629 *
15630 * @author Vova Feldman (@svovaf)
15631 * @since 2.0.0
15632 *
15633 * @param int $blog_id
15634 *
15635 * @return bool
15636 */
15637 function is_active_for_site( $blog_id ) {
15638 if ( ! is_multisite() ) {
15639 // Not a multisite and this code is executed, means that the plugin is active.
15640 return true;
15641 }
15642
15643 if ( $this->is_theme() ) {
15644 // All themes are site level activated.
15645 return true;
15646 }
15647
15648 if ( $this->_is_network_active ) {
15649 // Plugin was network activated so it's active.
15650 return true;
15651 }
15652
15653 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15654 }
15655
15656 /**
15657 * @todo Implement pagination when accessing the subsites collection.
15658 *
15659 * @author Leo Fajardo (@leorw)
15660 * @since 2.0.0
15661 *
15662 * @param int $limit Default to 1,000
15663 * @param int $offset Default to 0
15664 *
15665 * @return array Active & public sites collection.
15666 */
15667 static function get_sites( $limit = 1000, $offset = 0 ) {
15668 if ( ! is_multisite() ) {
15669 return array();
15670 }
15671
15672 /**
15673 * For consistency with get_blog_list() which only return active public sites.
15674 *
15675 * @author Vova Feldman (@svovaf)
15676 */
15677 $args = array(
15678 /**
15679 * Commented out in order to handle the migration of site options whether the site is public or not.
15680 *
15681 * @author Leo Fajardo (@leorw)
15682 * @since 2.2.1
15683 */
15684 // 'public' => 1,
15685 'archived' => 0,
15686 'mature' => 0,
15687 'spam' => 0,
15688 'deleted' => 0,
15689 'number' => $limit,
15690 'offset' => $offset,
15691 );
15692
15693 return get_sites( $args );
15694 }
15695
15696 /**
15697 * Checks if a given blog is active.
15698 *
15699 * @author Vova Feldman (@svovaf)
15700 * @since 2.0.0
15701 *
15702 * @param $blog_id
15703 *
15704 * @return bool
15705 */
15706 private static function is_site_active( $blog_id ) {
15707 global $wpdb;
15708
15709 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15710
15711 if ( ! is_object( $blog_info ) ) {
15712 return false;
15713 }
15714
15715 return (
15716 true == $blog_info->public &&
15717 false == $blog_info->archived &&
15718 false == $blog_info->mature &&
15719 false == $blog_info->spam &&
15720 false == $blog_info->deleted
15721 );
15722 }
15723
15724 /**
15725 * Get a mapping between the site addresses to their blog IDs.
15726 *
15727 * @author Vova Feldman (@svovaf)
15728 * @since 2.0.0
15729 *
15730 * @return array {
15731 * @key string Site address without protocol with a trailing slash.
15732 * @value int Site's blog ID.
15733 * }
15734 */
15735 private function get_address_to_blog_map() {
15736 $sites = self::get_sites();
15737
15738 // Map site addresses to their blog IDs.
15739 $address_to_blog_map = array();
15740 foreach ( $sites as $site ) {
15741 $blog_id = self::get_site_blog_id( $site );
15742 $address = self::get_unfiltered_site_url( $blog_id, true, true );
15743 $address_to_blog_map[ $address ] = $blog_id;
15744 }
15745
15746 return $address_to_blog_map;
15747 }
15748
15749 /**
15750 * Get a mapping between the site addresses to their blog IDs.
15751 *
15752 * @author Vova Feldman (@svovaf)
15753 * @since 2.0.0
15754 *
15755 * @return array {
15756 * @key int Site's blog ID.
15757 * @value FS_Site Associated install.
15758 * }
15759 */
15760 function get_blog_install_map() {
15761 $sites = self::get_sites();
15762
15763 // Map site blog ID to its install.
15764 $install_map = array();
15765
15766 foreach ( $sites as $site ) {
15767 $blog_id = self::get_site_blog_id( $site );
15768 $install = $this->get_install_by_blog_id( $blog_id );
15769
15770 if ( is_object( $install ) ) {
15771 $install_map[ $blog_id ] = $install;
15772 }
15773 }
15774
15775 return $install_map;
15776 }
15777
15778 /**
15779 * @author Vova Feldman (@svovaf)
15780 * @since 2.5.1
15781 *
15782 * @param bool|null $is_delegated When `true`, returns only connection delegated blog IDs. When `false`, only non-delegated blog IDs.
15783 *
15784 * @return int[]
15785 */
15786 private function get_blog_ids( $is_delegated = null ) {
15787 $blog_ids = array();
15788
15789 $sites = self::get_sites();
15790 foreach ( $sites as $site ) {
15791 $blog_id = self::get_site_blog_id( $site );
15792
15793 if (
15794 is_null( $is_delegated ) ||
15795 $is_delegated === $this->is_site_delegated_connection( $blog_id )
15796 ) {
15797 $blog_ids[] = $blog_id;
15798 }
15799 }
15800
15801 return $blog_ids;
15802 }
15803
15804 /**
15805 * @author Vova Feldman (@svovaf)
15806 * @since 2.5.1
15807 *
15808 * @return int[]
15809 */
15810 private function get_non_delegated_blog_ids() {
15811 return $this->get_blog_ids( false );
15812 }
15813
15814 /**
15815 * Gets a map of module IDs that the given user has opted-in to.
15816 *
15817 * @author Leo Fajardo (@leorw)
15818 * @since 2.1.0
15819 *
15820 * @param number $fs_user_id
15821 *
15822 * @return array {
15823 * @key number $plugin_id
15824 * @value bool Always true.
15825 * }
15826 */
15827 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15828 self::$_static_logger->entrance();
15829
15830 if ( ! is_multisite() ) {
15831 $installs = array_merge(
15832 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15833 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15834 );
15835 } else {
15836 $sites = self::get_sites();
15837
15838 $installs = array();
15839 foreach ( $sites as $site ) {
15840 $blog_id = self::get_site_blog_id( $site );
15841
15842 $installs = array_merge(
15843 $installs,
15844 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15845 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15846 );
15847 }
15848 }
15849
15850 $module_ids_map = array();
15851 foreach ( $installs as $install ) {
15852 if ( is_object( $install ) &&
15853 FS_Site::is_valid_id( $install->id ) &&
15854 FS_User::is_valid_id( $install->user_id ) &&
15855 ( $install->user_id == $fs_user_id )
15856 ) {
15857 $module_ids_map[ $install->plugin_id ] = true;
15858 }
15859 }
15860
15861 return $module_ids_map;
15862 }
15863
15864 /**
15865 * @author Leo Fajardo (@leorw)
15866 *
15867 * @return null|array {
15868 * 'install' => FS_Site Module's install,
15869 * 'blog_id' => string The associated blog ID.
15870 * }
15871 */
15872 function find_first_install() {
15873 $sites = self::get_sites();
15874
15875 foreach ( $sites as $site ) {
15876 $blog_id = self::get_site_blog_id( $site );
15877 $install = $this->get_install_by_blog_id( $blog_id );
15878
15879 if ( is_object( $install ) ) {
15880 return array(
15881 'install' => $install,
15882 'blog_id' => $blog_id
15883 );
15884 }
15885 }
15886
15887 return null;
15888 }
15889
15890 /**
15891 * Switches the Freemius site level context to a specified blog.
15892 *
15893 * @author Vova Feldman (@svovaf)
15894 * @since 2.0.0
15895 *
15896 * @param int $blog_id
15897 * @param FS_Site $install
15898 * @param bool $flush
15899 *
15900 * @return bool Since 2.3.1 returns if a switch was made.
15901 */
15902 function switch_to_blog( $blog_id, FS_Site $install = null, $flush = false ) {
15903 if ( ! is_numeric( $blog_id ) ) {
15904 return false;
15905 }
15906
15907 if ( ! $flush && $blog_id == $this->_context_is_network_or_blog_id ) {
15908 return false;
15909 }
15910
15911 switch_to_blog( $blog_id );
15912 $this->_context_is_network_or_blog_id = $blog_id;
15913
15914 self::$_accounts->set_site_blog_context( $blog_id );
15915 $this->_storage->set_site_blog_context( $blog_id );
15916 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
15917
15918 $this->_site = is_object( $install ) ?
15919 $install :
15920 $this->get_install_by_blog_id( $blog_id );
15921
15922 $this->_user = false;
15923 $this->_licenses = false;
15924 $this->_license = null;
15925 $this->is_whitelabeled = null;
15926
15927 if ( is_object( $this->_site ) ) {
15928 // Try to fetch user from install.
15929 $this->_user = self::_get_user_by_id( $this->_site->user_id );
15930
15931 if ( ! is_object( $this->_user ) &&
15932 FS_User::is_valid_id( $this->_storage->prev_user_id )
15933 ) {
15934 // Try to fetch previously saved user.
15935 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
15936
15937 if ( ! is_object( $this->_user ) ) {
15938 // Fallback to network's user.
15939 $this->_user = $this->get_network_user();
15940 }
15941 }
15942
15943 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
15944
15945 if ( ! empty( $all_plugin_licenses ) ) {
15946 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
15947 $this->_license = null;
15948 } else {
15949 $license_found = false;
15950 foreach ( $all_plugin_licenses as $license ) {
15951 if ( $license->id == $this->_site->license_id ) {
15952 // License found.
15953 $this->_license = $license;
15954 $license_found = true;
15955 break;
15956 }
15957 }
15958
15959 if ( $license_found ) {
15960 $this->link_license_2_user( $this->_license->id, $this->_user->id );
15961 }
15962 }
15963
15964 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15965 }
15966 }
15967
15968 unset( $this->_site_api );
15969 unset( $this->_user_api );
15970
15971 return true;
15972 }
15973
15974 /**
15975 * Restore the blog context to the blog that originally loaded the module.
15976 *
15977 * @author Vova Feldman (@svovaf)
15978 * @since 2.0.0
15979 */
15980 function restore_current_blog() {
15981 $this->switch_to_blog( $this->_blog_id );
15982 }
15983
15984 /**
15985 * @author Vova Feldman (@svovaf)
15986 * @since 2.0.0
15987 *
15988 * @param array|WP_Site $site
15989 *
15990 * @return int
15991 */
15992 static function get_site_blog_id( &$site ) {
15993 return ( $site instanceof WP_Site ) ?
15994 $site->blog_id :
15995 ( is_object( $site ) && isset( $site->userblog_id ) ?
15996 $site->userblog_id :
15997 $site['blog_id'] );
15998 }
15999
16000 /**
16001 * @author Vova Feldman (@svovaf)
16002 * @since 2.5.1
16003 *
16004 * @param WP_Site[]|array[] $sites
16005 *
16006 * @return int[]
16007 */
16008 static function get_sites_blog_ids( $sites ) {
16009 $blog_ids = array();
16010 foreach ( $sites as $site ) {
16011 $blog_ids[] = self::get_site_blog_id( $site );
16012 }
16013
16014 return $blog_ids;
16015 }
16016
16017 /**
16018 * @author Leo Fajardo (@leorw)
16019 * @since 2.0.0
16020 *
16021 * @param array|WP_Site|null $site
16022 * @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.
16023 *
16024 * @return array
16025 */
16026 function get_site_info( $site = null, $load_registration = false ) {
16027 $this->_logger->entrance();
16028
16029 $switched = false;
16030
16031 $registration_date = null;
16032
16033 if ( is_null( $site ) ) {
16034 $url = self::get_unfiltered_site_url();
16035 $name = get_bloginfo( 'name' );
16036 $blog_id = null;
16037 } else {
16038 $blog_id = self::get_site_blog_id( $site );
16039
16040 if ( get_current_blog_id() != $blog_id ) {
16041 switch_to_blog( $blog_id );
16042 $switched = true;
16043 }
16044
16045 if ( $site instanceof WP_Site ) {
16046 $url = $site->siteurl;
16047 $name = $site->blogname;
16048 $registration_date = $site->registered;
16049 } else {
16050 $url = self::get_unfiltered_site_url( $blog_id );
16051 $name = get_bloginfo( 'name' );
16052 }
16053 }
16054
16055 if ( empty( $registration_date ) && $load_registration ) {
16056 $blog_details = get_blog_details( $blog_id, false );
16057
16058 if ( is_object( $blog_details ) && isset( $blog_details->registered ) ) {
16059 $registration_date = $blog_details->registered;
16060 }
16061 }
16062
16063 $info = array(
16064 'uid' => $this->get_anonymous_id( $blog_id ),
16065 'url' => $url,
16066 );
16067
16068 // Add these diagnostic information only if user allowed to track.
16069 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
16070 $info = array_merge( $info, array(
16071 'title' => $name,
16072 'language' => self::get_sanitized_language(),
16073 ) );
16074 }
16075
16076 if ( is_numeric( $blog_id ) ) {
16077 $info['blog_id'] = $blog_id;
16078 }
16079
16080 if ( ! empty( $registration_date ) ) {
16081 $info[ 'registration_date' ] = $registration_date;
16082 }
16083
16084 if ( $switched ) {
16085 restore_current_blog();
16086 }
16087
16088 return $info;
16089 }
16090
16091 /**
16092 * Load the module's install based on the blog ID.
16093 *
16094 * @author Vova Feldman (@svovaf)
16095 * @since 2.0.0
16096 *
16097 * @param int|null $blog_id
16098 *
16099 * @return FS_Site
16100 */
16101 function get_install_by_blog_id( $blog_id = null ) {
16102 $installs = self::get_all_sites( $this->_module_type, $blog_id );
16103 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
16104
16105 if ( is_object( $install ) &&
16106 is_numeric( $install->id ) &&
16107 is_numeric( $install->user_id ) &&
16108 FS_Plugin_Plan::is_valid_id( $install->plan_id )
16109 ) {
16110 // Load site.
16111 $install = clone $install;
16112 }
16113
16114 return $install;
16115 }
16116
16117 /**
16118 * Check if module is installed on a specified site.
16119 *
16120 * @author Vova Feldman (@svovaf)
16121 * @since 2.0.0
16122 *
16123 * @param int|null $blog_id
16124 *
16125 * @return bool
16126 */
16127 function is_installed_on_site( $blog_id = null ) {
16128 $installs = self::get_all_sites( $this->_module_type, $blog_id );
16129 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
16130
16131 return (
16132 is_object( $install ) &&
16133 is_numeric( $install->id ) &&
16134 is_numeric( $install->user_id ) &&
16135 FS_Plugin_Plan::is_valid_id( $install->plan_id )
16136 );
16137 }
16138
16139 /**
16140 * Check if super-admin connected at least one site via the network opt-in.
16141 *
16142 * @author Vova Feldman (@svovaf)
16143 * @since 2.0.0
16144 *
16145 * @return bool
16146 */
16147 function is_network_registered() {
16148 if ( ! $this->_is_network_active ) {
16149 return false;
16150 }
16151
16152 return FS_User::is_valid_id( $this->_storage->network_user_id );
16153 }
16154
16155 /**
16156 * Returns the main user associated with the network.
16157 *
16158 * @author Vova Feldman (@svovaf)
16159 * @since 2.0.0
16160 *
16161 * @return FS_User
16162 */
16163 function get_network_user() {
16164 if ( ! $this->_is_network_active ) {
16165 return null;
16166 }
16167
16168 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
16169 self::_get_user_by_id( $this->_storage->network_user_id ) :
16170 null;
16171 }
16172
16173 /**
16174 * Returns the current context user or the network's main user.
16175 *
16176 * @author Vova Feldman (@svovaf)
16177 * @since 2.0.0
16178 *
16179 * @return FS_User
16180 */
16181 function get_current_or_network_user() {
16182 return ( $this->_user instanceof FS_User ) ?
16183 $this->_user :
16184 $this->get_network_user();
16185 }
16186
16187 /**
16188 * Returns the main install associated with the network.
16189 *
16190 * @author Vova Feldman (@svovaf)
16191 * @since 2.0.0
16192 *
16193 * @return FS_Site
16194 */
16195 function get_network_install() {
16196 if ( ! $this->_is_network_active ) {
16197 return null;
16198 }
16199
16200 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
16201 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
16202 null;
16203 }
16204
16205 /**
16206 * Returns the blog ID that is associated with the main install.
16207 *
16208 * @author Leo Fajardo (@leorw)
16209 * @since 2.0.0
16210 *
16211 * @return int|null
16212 */
16213 function get_network_install_blog_id() {
16214 if ( ! $this->_is_network_active ) {
16215 return null;
16216 }
16217
16218 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
16219 $this->_storage->network_install_blog_id :
16220 null;
16221 }
16222
16223 /**
16224 * Returns the current context install or the network's main install.
16225 *
16226 * @author Vova Feldman (@svovaf)
16227 * @since 2.0.0
16228 *
16229 * @return FS_Site
16230 */
16231 function get_current_or_network_install() {
16232 return ( $this->_site instanceof FS_Site ) ?
16233 $this->_site :
16234 $this->get_network_install();
16235 }
16236
16237 /**
16238 * Check if executing a site level action from the network level admin.
16239 *
16240 * @author Vova Feldman (@svovaf)
16241 * @since 2.0.0
16242 *
16243 * @return false|int If yes, return the requested blog ID.
16244 */
16245 private function is_network_level_site_specific_action() {
16246 if ( ! $this->_is_network_active ) {
16247 return false;
16248 }
16249
16250 if ( ! fs_is_network_admin() ) {
16251 return false;
16252 }
16253
16254 $blog_id = fs_request_get( 'blog_id', '' );
16255
16256 return is_numeric( $blog_id ) ? $blog_id : false;
16257 }
16258
16259 /**
16260 * Check if executing an action from the network level admin.
16261 *
16262 * @author Vova Feldman (@svovaf)
16263 * @since 2.0.0
16264 *
16265 * @return bool
16266 */
16267 private function is_network_level_action() {
16268 return ( $this->_is_network_active && fs_is_network_admin() );
16269 }
16270
16271 /**
16272 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
16273 * 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.
16274 *
16275 * @author Vova Feldman (@svovaf)
16276 * @since 2.0.0
16277 *
16278 * @param int $context_blog_id
16279 */
16280 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
16281 $this->_logger->entrance();
16282
16283 if ( $this->_is_network_active ) {
16284 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
16285 $installs_map = $this->get_blog_install_map();
16286
16287 foreach ( $installs_map as $blog_id => $install ) {
16288 /**
16289 * @var FS_Site $install
16290 */
16291 if ( $context_blog_id == $blog_id ) {
16292 continue;
16293 }
16294
16295 if ( $install->user_id != $this->_storage->network_user_id ) {
16296 continue;
16297 }
16298
16299 // Switch reference to a blog that is opted-in and belong to the same super-admin.
16300 $this->_storage->network_install_blog_id = $blog_id;
16301 break;
16302 }
16303 }
16304 }
16305
16306 if ( ! $this->is_registered() ) {
16307 return;
16308 }
16309
16310 if ( $this->is_sync_cron_scheduled() &&
16311 $context_blog_id == $this->get_sync_cron_blog_id()
16312 ) {
16313 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
16314 }
16315
16316 if ( $this->is_install_sync_scheduled() &&
16317 $context_blog_id == $this->get_install_sync_cron_blog_id()
16318 ) {
16319 $this->schedule_install_sync( $context_blog_id );
16320 }
16321 }
16322
16323 /**
16324 * Executed after site deactivation, archive, or flag as spam.
16325 *
16326 * @author Vova Feldman (@svovaf)
16327 * @since 2.0.0
16328 *
16329 * @param int $context_blog_id
16330 */
16331 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
16332 $this->_logger->entrance();
16333
16334 $install = $this->get_install_by_blog_id( $context_blog_id );
16335
16336 if ( ! is_object( $install ) ) {
16337 // Site not connected.
16338 return;
16339 }
16340
16341 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16342
16343 if ( ! $this->is_registered() ) {
16344 return;
16345 }
16346
16347 $current_blog_id = get_current_blog_id();
16348
16349 $this->switch_to_blog( $context_blog_id );
16350
16351 // Send deactivation event.
16352 $this->sync_install( array(
16353 'is_active' => false,
16354 ) );
16355
16356 $this->switch_to_blog( $current_blog_id );
16357 }
16358
16359 /**
16360 * Executed after site deletion.
16361 *
16362 * @author Vova Feldman (@svovaf)
16363 * @since 2.0.0
16364 *
16365 * @param int $context_blog_id
16366 * @param bool $drop True if site's database tables should be dropped. Default is false.
16367 */
16368 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
16369 $this->_logger->entrance();
16370
16371 $install = $this->get_install_by_blog_id( $context_blog_id );
16372
16373 if ( ! is_object( $install ) ) {
16374 // Site not connected.
16375 return;
16376 }
16377
16378 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16379
16380 if ( ! $this->is_registered() ) {
16381 return;
16382 }
16383
16384 $current_blog_id = get_current_blog_id();
16385
16386 $this->switch_to_blog( $context_blog_id );
16387
16388 if ( $drop ) {
16389 // Delete install if dropping site DB.
16390 $this->delete_account_event();
16391 } else {
16392 // Send deactivation event.
16393 $this->sync_install( array(
16394 'is_active' => false,
16395 ) );
16396 }
16397
16398 $this->switch_to_blog( $current_blog_id );
16399 }
16400
16401 /**
16402 * Executed after site deletion, called from wp_delete_site
16403 *
16404 * @author Dario Curvino (@dudo)
16405 * @since 2.5.0
16406 *
16407 * @param WP_Site $old_site
16408 */
16409 public function _after_wpsite_deleted_callback( WP_Site $old_site ) {
16410 $this->_logger->entrance();
16411
16412 $this->_after_site_deleted_callback( $old_site->blog_id, true );
16413 }
16414
16415 /**
16416 * Executed after site re-activation.
16417 *
16418 * @author Vova Feldman (@svovaf)
16419 * @since 2.0.0
16420 *
16421 * @param int $context_blog_id
16422 */
16423 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
16424 $this->_logger->entrance();
16425
16426 $install = $this->get_install_by_blog_id( $context_blog_id );
16427
16428 if ( ! is_object( $install ) ) {
16429 // Site not connected.
16430 return;
16431 }
16432
16433 if ( ! self::is_site_active( $context_blog_id ) ) {
16434 // Site not yet active (can be in spam mode, archived, deleted...).
16435 return;
16436 }
16437
16438 $current_blog_id = get_current_blog_id();
16439
16440 $this->switch_to_blog( $context_blog_id );
16441
16442 // Send re-activation event.
16443 $this->sync_install( array(
16444 'is_active' => true,
16445 ) );
16446
16447 $this->switch_to_blog( $current_blog_id );
16448 }
16449
16450 #endregion Multisite
16451
16452 /**
16453 * @author Leo Fajardo (@leorw)
16454 *
16455 * @param string $path
16456 * @param string $scheme
16457 * @param bool $network
16458 *
16459 * @return string
16460 */
16461 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
16462 return ( $this->_is_network_active && $network ) ?
16463 network_admin_url( $path, $scheme ) :
16464 admin_url( $path, $scheme );
16465 }
16466
16467 /**
16468 * Check if currently in a specified admin page.
16469 *
16470 * @author Vova Feldman (@svovaf)
16471 * @since 1.2.2.7
16472 *
16473 * @param string $page
16474 *
16475 * @return bool
16476 */
16477 function is_admin_page( $page ) {
16478 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
16479 }
16480
16481 /**
16482 * Check if currently in the product's main admin page.
16483 *
16484 * @author Vova Feldman (@svovaf)
16485 * @since 2.3.1
16486 *
16487 * @return bool
16488 */
16489 function is_main_admin_page() {
16490 return $this->is_admin_page( '' );
16491 }
16492
16493 /**
16494 * Get module's main admin setting page URL.
16495 *
16496 * @author Vova Feldman (@svovaf)
16497 * @since 1.2.2.7
16498 *
16499 * @return string
16500 */
16501 function main_menu_url() {
16502 return $this->_menu->main_menu_url();
16503 }
16504
16505 /**
16506 * Check if currently on the theme's setting page or
16507 * on any of the Freemius added pages (via tabs).
16508 *
16509 * @author Vova Feldman (@svovaf)
16510 * @since 1.2.2.7
16511 *
16512 * @return bool
16513 *
16514 * @deprecated Please use is_product_settings_page() instead;
16515 */
16516 function is_theme_settings_page() {
16517 return $this->is_product_settings_page();
16518 }
16519
16520 /**
16521 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
16522 *
16523 * @author Vova Feldman (@svovaf)
16524 * @since 1.2.2.7
16525 *
16526 * @return bool
16527 */
16528 function is_product_settings_page() {
16529 $page = fs_request_get( 'page', '', 'get' );
16530 $menu_slug = $this->_menu->get_slug();
16531
16532 if ( $page === $menu_slug ) {
16533 return true;
16534 }
16535
16536 return fs_starts_with(
16537 // e.g., {$menu_slug}-account, {$menu_slug}-affiliation, etc.
16538 $page,
16539 ( $menu_slug . '-' )
16540 );
16541 }
16542
16543 /**
16544 * Plugin's account page + sync license URL.
16545 *
16546 * @author Vova Feldman (@svovaf)
16547 * @since 1.1.9.1
16548 *
16549 * @param bool|number $plugin_id
16550 * @param bool $add_action_nonce
16551 * @param array $params
16552 *
16553 * @return string
16554 */
16555 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
16556 if ( is_numeric( $plugin_id ) ) {
16557 $params['plugin_id'] = $plugin_id;
16558 }
16559
16560 return $this->get_account_url(
16561 $this->get_unique_affix() . '_sync_license',
16562 $params,
16563 $add_action_nonce
16564 );
16565 }
16566
16567 /**
16568 * Plugin's account URL.
16569 *
16570 * @author Vova Feldman (@svovaf)
16571 * @since 1.0.4
16572 *
16573 * @param bool|string $action
16574 * @param array $params
16575 *
16576 * @param bool $add_action_nonce
16577 *
16578 * @return string
16579 */
16580 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
16581 if ( is_string( $action ) ) {
16582 $params['fs_action'] = $action;
16583 }
16584
16585 self::require_pluggable_essentials();
16586
16587 return ( $add_action_nonce && is_string( $action ) ) ?
16588 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
16589 $this->_get_admin_page_url( 'account', $params );
16590 }
16591
16592 /**
16593 * @author Vova Feldman (@svovaf)
16594 * @since 1.2.0
16595 *
16596 * @param string $tab
16597 * @param bool $action
16598 * @param array $params
16599 * @param bool $add_action_nonce
16600 *
16601 * @return string
16602 *
16603 * @uses get_account_url()
16604 */
16605 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
16606 $params['tab'] = $tab;
16607
16608 return $this->get_account_url( $action, $params, $add_action_nonce );
16609 }
16610
16611 /**
16612 * Plugin's account URL.
16613 *
16614 * @author Vova Feldman (@svovaf)
16615 * @since 1.0.4
16616 *
16617 * @param bool|string $topic
16618 * @param bool|string $message
16619 * @param bool|string $summary Since 2.5.1.
16620 *
16621 * @return string
16622 */
16623 function contact_url( $topic = false, $message = false, $summary = false ) {
16624 $params = array();
16625 if ( is_string( $topic ) ) {
16626 $params['topic'] = $topic;
16627 }
16628 if ( is_string( $message ) ) {
16629 $params['message'] = $message;
16630 }
16631
16632 if ( is_string( $summary ) ) {
16633 $params['summary'] = $summary;
16634 }
16635
16636 if ( $this->is_addon() ) {
16637 $params['addon_id'] = $this->get_id();
16638
16639 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
16640 } else {
16641 return $this->_get_admin_page_url( 'contact', $params );
16642 }
16643 }
16644
16645 /**
16646 * Add-on direct info URL.
16647 *
16648 * @author Vova Feldman (@svovaf)
16649 * @since 1.1.0
16650 *
16651 * @param string $slug
16652 *
16653 * @return string
16654 */
16655 function addon_url( $slug ) {
16656 return $this->_get_admin_page_url( 'addons', array(
16657 'slug' => $slug
16658 ) );
16659 }
16660
16661 /**
16662 * Add-ons URL.
16663 *
16664 * @author Vova Feldman (@svovaf)
16665 * @since 2.4.5
16666 *
16667 * @return string
16668 */
16669 function get_addons_url() {
16670 return $this->_get_admin_page_url( 'addons' );
16671 }
16672
16673 /* Logger
16674 ------------------------------------------------------------------------------------------------------------------*/
16675 /**
16676 * @param string $id
16677 * @param bool $prefix_slug
16678 *
16679 * @return FS_Logger
16680 */
16681 function get_logger( $id = '', $prefix_slug = true ) {
16682 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16683 }
16684
16685 /**
16686 * Note: This method is used externally so don't delete it.
16687 *
16688 * @param $id
16689 * @param bool $load_options
16690 * @param bool $prefix_slug
16691 *
16692 * @return FS_Option_Manager
16693 */
16694 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16695 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16696 }
16697
16698 /* Security
16699 ------------------------------------------------------------------------------------------------------------------*/
16700 private static function _encrypt( $str ) {
16701 if ( is_null( $str ) ) {
16702 return null;
16703 }
16704
16705 /**
16706 * The encrypt/decrypt functions are used to protect
16707 * the user from messing up with some of the sensitive
16708 * data stored for the module as a JSON in the database.
16709 *
16710 * I used the same suggested hack by the theme review team.
16711 * For more details, look at the function `Base64UrlDecode()`
16712 * in `./sdk/FreemiusBase.php`.
16713 *
16714 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16715 *
16716 * @author Vova Feldman (@svovaf)
16717 * @since 1.2.2
16718 */
16719 $fn = 'base64' . '_encode';
16720
16721 return $fn( $str );
16722 }
16723
16724 static function _decrypt( $str ) {
16725 if ( is_null( $str ) ) {
16726 return null;
16727 }
16728
16729 /**
16730 * The encrypt/decrypt functions are used to protect
16731 * the user from messing up with some of the sensitive
16732 * data stored for the module as a JSON in the database.
16733 *
16734 * I used the same suggested hack by the theme review team.
16735 * For more details, look at the function `Base64UrlDecode()`
16736 * in `./sdk/FreemiusBase.php`.
16737 *
16738 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16739 *
16740 * @author Vova Feldman (@svovaf)
16741 * @since 1.2.2
16742 */
16743 $fn = 'base64' . '_decode';
16744
16745 return $fn( $str );
16746 }
16747
16748 /**
16749 * @author Vova Feldman (@svovaf)
16750 * @since 1.0.5
16751 *
16752 * @param FS_Entity $entity
16753 *
16754 * @return FS_Entity Return an encrypted clone entity.
16755 */
16756 private static function _encrypt_entity( FS_Entity $entity ) {
16757 $clone = clone $entity;
16758 $props = get_object_vars( $entity );
16759
16760 foreach ( $props as $key => $val ) {
16761 $clone->{$key} = self::_encrypt( $val );
16762 }
16763
16764 return $clone;
16765 }
16766
16767 /**
16768 * @author Vova Feldman (@svovaf)
16769 * @since 1.0.5
16770 *
16771 * @param FS_Entity $entity
16772 *
16773 * @return FS_Entity Return an decrypted clone entity.
16774 */
16775 private static function decrypt_entity( FS_Entity $entity ) {
16776 $clone = clone $entity;
16777 $props = get_object_vars( $entity );
16778
16779 foreach ( $props as $key => $val ) {
16780 $clone->{$key} = self::_decrypt( $val );
16781 }
16782
16783 return $clone;
16784 }
16785
16786 /**
16787 * @author Vova Feldman (@svovaf)
16788 * @since 1.0.7
16789 *
16790 * @param string $email
16791 *
16792 * @return FS_User|false
16793 */
16794 public static function _get_user_by_email( $email ) {
16795 self::$_static_logger->entrance();
16796
16797 $email = trim( strtolower( $email ) );
16798
16799 $users = self::get_all_users();
16800
16801 if ( is_array( $users ) ) {
16802 foreach ( $users as $user ) {
16803 if ( $email === trim( strtolower( $user->email ) ) ) {
16804 return $user;
16805 }
16806 }
16807 }
16808
16809 return false;
16810 }
16811
16812 #----------------------------------------------------------------------------------
16813 #region Account (Loading, Updates & Activation)
16814 #----------------------------------------------------------------------------------
16815
16816 /***
16817 * Load account information (user + site).
16818 *
16819 * @author Vova Feldman (@svovaf)
16820 * @since 1.0.1
16821 */
16822 private function _load_account() {
16823 $this->_logger->entrance();
16824
16825 $this->do_action( 'before_account_load' );
16826
16827 $users = self::get_all_users();
16828 $plans = self::get_all_plans( $this->_module_type );
16829
16830 if ( $this->_logger->is_on() && is_admin() ) {
16831 $this->_logger->log( 'users = ' . var_export( $users, true ) );
16832 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
16833 }
16834
16835 $site = fs_is_network_admin() ?
16836 $this->get_network_install() :
16837 $this->get_install_by_blog_id();
16838
16839 if ( fs_is_network_admin() &&
16840 $this->is_network_active() &&
16841 ! is_object( $site ) &&
16842 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
16843 ) {
16844 $first_install = $this->find_first_install();
16845
16846 if ( is_null( $first_install ) ) {
16847 unset( $this->_storage->network_install_blog_id );
16848 } else {
16849 $site = $first_install['install'];
16850 $this->_storage->network_install_blog_id = $first_install['blog_id'];
16851 }
16852 }
16853
16854 if ( is_object( $site ) &&
16855 is_numeric( $site->id ) &&
16856 is_numeric( $site->user_id ) &&
16857 FS_Plugin_Plan::is_valid_id( $site->plan_id )
16858 ) {
16859 // Load site.
16860 $this->_site = $site;
16861 }
16862
16863 $user = null;
16864 if ( fs_is_network_admin() && $this->_is_network_active ) {
16865 $user = $this->get_network_user();
16866 }
16867
16868 if ( is_object( $user ) ) {
16869 $this->_user = clone $user;
16870 } else if ( $this->_site ) {
16871 $user = self::_get_user_by_id( $this->_site->user_id );
16872
16873 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
16874 /**
16875 * Try to load the previous owner. This recovery is used for the following use-case:
16876 * 1. Opt-in
16877 * 2. Cloning site1 to site2
16878 * 3. Ownership switch in site1 (same applies for site2)
16879 * 4. Install data sync on site2
16880 * 5. Now site2's install is associated with the new owner which does not exists locally.
16881 */
16882 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
16883 }
16884
16885 if ( ! is_object( $user ) ) {
16886 /**
16887 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
16888 */
16889 if (
16890 ! isset( $this->_storage->user_recovery_from_install_last_attempt_timestamp ) ||
16891 time() > ( $this->_storage->user_recovery_from_install_last_attempt_timestamp + FS_Clone_Manager::CLONE_RESOLUTION_MAX_EXECUTION_TIME )
16892 ) {
16893 $user = $this->sync_user_by_current_install();
16894 } else {
16895 return;
16896 }
16897
16898 if ( is_object( $user ) ) {
16899 $this->_storage->user_was_recovered_from_install = true;
16900 } else {
16901 $this->_storage->user_recovery_from_install_attempts = isset( $this->_storage->user_recovery_from_install_attempts ) ?
16902 ( $this->_storage->user_recovery_from_install_attempts + 1 ) :
16903 1;
16904
16905 if ( $this->_storage->user_recovery_from_install_attempts >= 3 ) {
16906 $this->delete_current_install( false );
16907 } else {
16908 $this->_storage->user_recovery_from_install_last_attempt_timestamp = time();
16909
16910 return;
16911 }
16912 }
16913 }
16914
16915 $this->_user = ( $user instanceof FS_User ) ?
16916 clone $user :
16917 null;
16918 }
16919
16920 if ( is_object( $this->_user ) ) {
16921 // Load licenses.
16922 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16923 }
16924
16925 if ( is_object( $this->_site ) ) {
16926 // Load plans.
16927 $this->_plans = isset( $plans[ $this->_slug ] ) ?
16928 $plans[ $this->_slug ] :
16929 array();
16930
16931 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
16932 $this->_sync_plans();
16933 } else {
16934 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16935 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
16936 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
16937 } else {
16938 unset( $this->_plans[ $i ] );
16939 }
16940 }
16941 }
16942
16943 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
16944
16945 if ( $this->_site->version != $this->get_plugin_version() ) {
16946 // If stored install version is different than current installed plugin version,
16947 // then update plugin version event.
16948 $this->update_plugin_version_event();
16949 }
16950 }
16951
16952 if ( true === $this->_storage->require_license_activation &&
16953 ! fs_request_get_bool( 'require_license', true )
16954 ) {
16955 $this->_storage->require_license_activation = false;
16956 }
16957
16958 if ( $this->is_theme() ) {
16959 $this->_register_account_hooks();
16960 }
16961
16962 if ( $this->is_user_in_admin() && $this->is_clone() ) {
16963 if ( empty( FS_Clone_Manager::instance()->get_clone_identification_timestamp() ) ) {
16964 FS_Clone_Manager::instance()->store_clone_identification_timestamp();
16965 }
16966
16967 FS_Clone_Manager::instance()->maybe_update_clone_resolution_support_flag( $this->_storage->sdk_last_version );
16968 $this->send_pending_clone_update_once();
16969 }
16970 }
16971
16972 /**
16973 * Special user recovery mechanism.
16974 *
16975 * @author Vova Feldman (@svovaf)
16976 * @since 2.0.0
16977 *
16978 * @param number|null $site_user_id
16979 *
16980 * @return \FS_User|mixed
16981 */
16982 private function sync_user_by_current_install( $site_user_id = null ) {
16983 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
16984 $site_user_id :
16985 $this->_site->user_id;
16986
16987 $api = $this->get_api_site_scope();
16988
16989 $uid = $this->get_anonymous_id();
16990 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
16991
16992 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
16993
16994 if ( $this->is_api_result_entity( $result ) ) {
16995 $user = new FS_User( $result );
16996 $this->_user = $user;
16997 $this->_store_user();
16998
16999 return $user;
17000 }
17001
17002 $error_code = FS_Api::get_error_code( $result );
17003
17004 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
17005 /**
17006 * Those API errors will continue coming and are not recoverable with the
17007 * current site's data. Therefore, extend the API call's cached result to 7 days.
17008 */
17009 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
17010 }
17011
17012 return $result;
17013 }
17014
17015 /**
17016 * @author Vova Feldman (@svovaf)
17017 * @since 1.0.1
17018 *
17019 * @param FS_User $user
17020 * @param FS_Site $site
17021 * @param bool|array $plans
17022 */
17023 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
17024 $site->user_id = $user->id;
17025
17026 $this->_site = $site;
17027 $this->_user = $user;
17028 if ( false !== $plans ) {
17029 $this->_plans = $plans;
17030 }
17031
17032 $this->send_install_update();
17033
17034 $this->_store_account();
17035
17036 }
17037
17038 /**
17039 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
17040 * Each version is trimmed after the 16th char.
17041 *
17042 * @author Vova Feldman (@svovaf)
17043 * @since 2.2.1
17044 *
17045 * @return array
17046 */
17047 private function get_versions() {
17048 $versions = array();
17049 $versions['sdk_version'] = $this->version;
17050
17051 // Collect these diagnostic information only if it's allowed.
17052 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17053 $versions['platform_version'] = get_bloginfo( 'version' );
17054 $versions['programming_language_version'] = phpversion();
17055 }
17056
17057 foreach ( $versions as $k => $version ) {
17058 $versions[ $k ] = self::get_api_sanitized_property( $k, $version );
17059 }
17060
17061 return $versions;
17062 }
17063
17064 /**
17065 * Get sanitized site language.
17066 *
17067 * @param string $language
17068 * @param int $max_len
17069 *
17070 * @since 2.5.1
17071 * @author Vova Feldman (@svovaf)
17072 *
17073 * @return string
17074 */
17075 private static function get_sanitized_language( $language = '', $max_len = self::LANGUAGE_MAX_CHARS ) {
17076 if ( empty( $language ) ) {
17077 $language = get_bloginfo( 'language' );
17078 }
17079
17080 return substr( $language, 0, $max_len );
17081 }
17082
17083 /**
17084 * Get core version stripped from pre-release and build.
17085 *
17086 * @since 2.5.1
17087 * @author Vova Feldman (@svovaf)
17088 *
17089 * @param string $version
17090 * @param int $parts
17091 * @param int $max_len
17092 * @param bool $include_pre_release
17093 *
17094 * @return string
17095 */
17096 private static function get_core_version(
17097 $version,
17098 $parts = 3,
17099 $max_len = self::VERSION_MAX_CHARS,
17100 $include_pre_release = false
17101 ) {
17102 if ( empty( $version ) ) {
17103 // Version is empty.
17104 return '';
17105 }
17106
17107 if ( is_numeric( $version ) ) {
17108 $is_float_version = is_float( $version );
17109
17110 $version = (string) $version;
17111
17112 /**
17113 * 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.
17114 */
17115 if ( $is_float_version && false === strpos( $version, '.' ) ) {
17116 $version .= '.0';
17117 }
17118 }
17119
17120 if ( ! is_string( $version ) ) {
17121 return '';
17122 }
17123
17124 if ( $parts < 1 ) {
17125 return '';
17126 }
17127
17128 $pre_release_regex = $include_pre_release ?
17129 '(\-(alpha|beta|RC)([0-9]+)?)?' :
17130 '';
17131
17132 if ( 0 === preg_match( '/^([0-9]+(\.[0-9]+){0,' . ( $parts - 1 ) . '}' . $pre_release_regex . ')/i', $version, $matches ) ) {
17133 // Version is not starting with a digit.
17134 return '';
17135 }
17136
17137 return substr( $matches[1], 0, $max_len );
17138 }
17139
17140 /**
17141 * @param string $prop
17142 * @param mixed $val
17143 *
17144 * @return mixed
17145 *@author Vova Feldman (@svovaf)
17146 *
17147 * @since 2.5.1
17148 */
17149 private static function get_api_sanitized_property( $prop, $val ) {
17150 if ( ! is_string( $val ) || empty( $val ) ) {
17151 return $val;
17152 }
17153
17154 switch ( $prop ) {
17155 case 'programming_language_version':
17156 // Get core PHP version, which can have up to 3 parts (ignore pre-releases).
17157 return self::get_core_version( $val );
17158 case 'platform_version':
17159 // Get the exact WordPress version, which can have up to 3 parts (including pre-releases).
17160 return self::get_core_version( $val, 3, self::VERSION_MAX_CHARS, true );
17161 case 'sdk_version':
17162 // Get the exact SDK version, which can have up to 4 parts.
17163 return self::get_core_version( $val, 4 );
17164 case 'version':
17165 // Get the entire version but just limited in length.
17166 return substr( $val, 0, self::VERSION_MAX_CHARS );
17167 case 'language':
17168 return self::get_sanitized_language( $val );
17169 default:
17170 return $val;
17171 }
17172 }
17173
17174 /**
17175 * @author Leo Fajardo (@leorw)
17176 * @since 2.3.0
17177 *
17178 * @return bool
17179 */
17180 function has_beta_update() {
17181 return (
17182 ! empty( $this->_storage->beta_data ) &&
17183 ( true === $this->_storage->beta_data['is_beta'] ) &&
17184 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
17185 );
17186 }
17187
17188 /**
17189 * @author Leo Fajardo (@leorw)
17190 * @since 2.3.0
17191 *
17192 * @return bool
17193 */
17194 function is_beta() {
17195 return (
17196 ! empty( $this->_storage->beta_data ) &&
17197 ( true === $this->_storage->beta_data['is_beta'] ) &&
17198 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
17199 );
17200 }
17201
17202 /**
17203 * @author Vova Feldman (@svovaf)
17204 * @since 1.1.7.4
17205 *
17206 * @param array $override_with
17207 * @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.
17208 *
17209 * @return array
17210 */
17211 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
17212 $this->_logger->entrance();
17213
17214 $current_user = self::_get_current_wp_user();
17215
17216 $activation_action = $this->get_unique_affix() . '_activate_new';
17217 $return_url = $this->is_anonymous() ?
17218 // If skipped already, then return to the account page.
17219 $this->get_account_url( $activation_action, array(), false ) :
17220 // Return to the module's main page.
17221 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
17222
17223 $versions = $this->get_versions();
17224
17225 $params = array_merge( $versions, array(
17226 'user_firstname' => $current_user->user_firstname,
17227 'user_lastname' => $current_user->user_lastname,
17228 'user_email' => $current_user->user_email,
17229 'plugin_slug' => $this->_slug,
17230 'plugin_id' => $this->get_id(),
17231 'plugin_public_key' => $this->get_public_key(),
17232 'plugin_version' => $this->get_plugin_version(),
17233 'return_url' => fs_nonce_url( $return_url, $activation_action ),
17234 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
17235 'account',
17236 array( 'fs_action' => 'sync_user' )
17237 ), 'sync_user' ),
17238 'is_premium' => $this->is_premium(),
17239 'is_active' => true,
17240 'is_uninstalled' => false,
17241 'is_localhost' => WP_FS__IS_LOCALHOST,
17242 ) );
17243
17244 if ( $this->is_addon() ) {
17245 $parent_fs = $this->get_parent_instance();
17246
17247 $params['parent_plugin_slug'] = $parent_fs->_slug;
17248 $params['parent_plugin_id'] = $parent_fs->get_id();
17249 }
17250
17251 if ( true === $network_level_or_blog_id ) {
17252 if ( ! isset( $override_with['sites'] ) ) {
17253 $params['sites'] = $this->get_sites_for_network_level_optin();
17254 }
17255 } else {
17256 $site = is_numeric( $network_level_or_blog_id ) ?
17257 array( 'blog_id' => $network_level_or_blog_id ) :
17258 null;
17259
17260 $site = $this->get_site_info( $site );
17261
17262 $diagnostic_info = array();
17263 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17264 $diagnostic_info = array(
17265 'site_name' => $site['title'],
17266 'language' => self::get_sanitized_language( $site['language'] ),
17267 );
17268 }
17269
17270 $params = array_merge( $params, $diagnostic_info, array(
17271 'site_uid' => $site['uid'],
17272 'site_url' => $site['url'],
17273 ) );
17274 }
17275
17276 if ( $this->is_pending_activation() &&
17277 ! empty( $this->_storage->pending_license_key )
17278 ) {
17279 $params['license_key'] = $this->_storage->pending_license_key;
17280 }
17281
17282 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
17283 // Even though rand() is known for its security issues,
17284 // the timestamp adds another layer of protection.
17285 // It would be very hard for an attacker to get the secret key form here.
17286 // Plus, this should never run in production since the secret should never
17287 // be included in the production version.
17288 $params['ts'] = WP_FS__SCRIPT_START_TIME;
17289 $params['salt'] = md5( uniqid( rand() ) );
17290 $params['secure'] = md5(
17291 $params['ts'] .
17292 $params['salt'] .
17293 $this->get_secret_key()
17294 );
17295 }
17296
17297 if ( is_multisite() && function_exists( 'get_network' ) ) {
17298 $params['network_uid'] = $this->get_anonymous_network_id();
17299 }
17300
17301 return array_merge( $params, $override_with );
17302 }
17303
17304 /**
17305 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
17306 * 2. If there was an API error, return the API result.
17307 *
17308 * @author Vova Feldman (@svovaf)
17309 * @since 1.1.7.4
17310 *
17311 * @param string|bool $email
17312 * @param string|bool $first
17313 * @param string|bool $last
17314 * @param string|bool $license_key
17315 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
17316 * In this case, the user and site info will be sent to the server but no
17317 * data will be saved to the WP installation's database.
17318 * @param number|bool $trial_plan_id
17319 * @param bool $is_disconnected Whether to opt in without tracking.
17320 * @param null|bool $is_marketing_allowed
17321 * @param array $sites If network-level opt-in, an array of containing details of sites.
17322 * @param bool $redirect
17323 *
17324 * @return string|object
17325 * @use WP_Error
17326 */
17327 function opt_in(
17328 $email = false,
17329 $first = false,
17330 $last = false,
17331 $license_key = false,
17332 $is_uninstall = false,
17333 $trial_plan_id = false,
17334 $is_disconnected = false,
17335 $is_marketing_allowed = null,
17336 $sites = array(),
17337 $redirect = true
17338 ) {
17339 $this->_logger->entrance();
17340
17341 if ( false === $email ) {
17342 $current_user = self::_get_current_wp_user();
17343 $email = $current_user->user_email;
17344 }
17345
17346 /**
17347 * @since 1.2.1 If activating with license key, ignore the context-user
17348 * since the user will be automatically loaded from the license.
17349 */
17350 if ( empty( $license_key ) ) {
17351 // Clean up pending license if opt-ing in again.
17352 $this->_storage->remove( 'pending_license_key' );
17353
17354 if ( ! $is_uninstall ) {
17355 $fs_user = Freemius::_get_user_by_email( $email );
17356 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
17357 return $this->install_with_user(
17358 $fs_user,
17359 false,
17360 $trial_plan_id,
17361 $redirect,
17362 true,
17363 $sites
17364 );
17365 }
17366 }
17367 }
17368
17369 $user_info = array();
17370 if ( ! empty( $email ) ) {
17371 $user_info['user_email'] = $email;
17372 }
17373 if ( ! empty( $first ) ) {
17374 $user_info['user_firstname'] = $first;
17375 }
17376 if ( ! empty( $last ) ) {
17377 $user_info['user_lastname'] = $last;
17378 }
17379
17380 if ( ! empty( $sites ) ) {
17381 $is_network = true;
17382
17383 $user_info['sites'] = $sites;
17384 } else {
17385 $is_network = false;
17386 }
17387
17388 $params = $this->get_opt_in_params( $user_info, $is_network );
17389
17390 $filtered_license_key = false;
17391 if ( is_string( $license_key ) ) {
17392 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
17393 $params['license_key'] = $filtered_license_key;
17394 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17395 $params['trial_plan_id'] = $trial_plan_id;
17396 }
17397
17398 if ( $is_uninstall ) {
17399 $params['uninstall_params'] = array(
17400 'reason_id' => $this->_storage->uninstall_reason->id,
17401 'reason_info' => $this->_storage->uninstall_reason->info
17402 );
17403 }
17404
17405 if ( isset( $params['license_key'] ) ) {
17406 $fs_user = Freemius::_get_user_by_email( $email );
17407
17408 if ( is_object( $fs_user ) ) {
17409 /**
17410 * If opting in with a context license and the context WP Admin user already opted in
17411 * before from the current site, add the user context security params to avoid the
17412 * unnecessary email activation when the context license is owned by the same context user.
17413 *
17414 * @author Leo Fajardo (@leorw)
17415 * @since 1.2.3
17416 */
17417 $params = array_merge( $params, FS_Security::instance()->get_context_params(
17418 $fs_user,
17419 false,
17420 'install_with_existing_user'
17421 ) );
17422 }
17423 }
17424
17425 if ( is_bool( $is_marketing_allowed ) ) {
17426 $params['is_marketing_allowed'] = $is_marketing_allowed;
17427 }
17428
17429 $params['is_disconnected'] = $is_disconnected;
17430 $params['format'] = 'json';
17431 $params['is_extensions_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed();
17432 $params['is_diagnostic_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed();
17433
17434 $request = array(
17435 'method' => 'POST',
17436 'body' => $params,
17437 'timeout' => 60,
17438 );
17439
17440 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
17441 $response = self::safe_remote_post( $url, $request );
17442
17443 if ( is_wp_error( $response ) ) {
17444 /**
17445 * @var WP_Error $response
17446 */
17447 $result = new stdClass();
17448
17449 $error_code = $response->get_error_code();
17450 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
17451
17452 $result->error = (object) array(
17453 'type' => $error_type,
17454 'message' => $response->get_error_message(),
17455 'code' => $error_code,
17456 'http' => 402
17457 );
17458
17459 $this->maybe_modify_api_curl_error_message( $result );
17460
17461 if ( FS_Api::is_blocked( $result ) ) {
17462 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
17463 }
17464
17465 $is_connected = null;
17466
17467 if ( empty( $license_key ) && $this->is_enable_anonymous() ) {
17468 $this->skip_connection( fs_is_network_admin() );
17469
17470 $is_connected = ( ! FS_Api::is_blocked( $result ) );
17471 }
17472
17473 $this->update_connectivity_info( $is_connected );
17474
17475 return $result;
17476 }
17477
17478 $this->update_connectivity_info( true );
17479
17480 // Module is being uninstalled, don't handle the returned data.
17481 if ( $is_uninstall ) {
17482 return true;
17483 }
17484
17485 /**
17486 * 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.
17487 *
17488 * @author Vova Feldman (@svovaf)
17489 * @since 1.2.3
17490 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
17491 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
17492 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
17493 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
17494 */
17495 $decoded = is_string( $response['body'] ) ?
17496 json_decode( $response['body'] ) :
17497 null;
17498
17499 if ( empty( $decoded ) ) {
17500 return false;
17501 }
17502
17503 if ( ! $this->is_api_result_object( $decoded ) ) {
17504 if ( ! empty( $params['license_key'] ) ) {
17505 // Pass the fully entered license key to the failure handler.
17506 $params['license_key'] = $license_key;
17507 }
17508
17509 return $is_uninstall ?
17510 $decoded :
17511 $this->apply_filters( 'after_install_failure', $decoded, $params );
17512 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
17513 if ( $is_network ) {
17514 $site_ids = array();
17515 foreach ( $sites as $site ) {
17516 $site_ids[] = $site['blog_id'];
17517 }
17518
17519 /**
17520 * Store the sites so that they can be installed once the user has clicked on the activation link
17521 * in the email.
17522 *
17523 * @author Leo Fajardo (@leorw)
17524 */
17525 $this->_storage->pending_sites_info = array(
17526 'blog_ids' => $site_ids,
17527 'license_key' => $license_key,
17528 'trial_plan_id' => $trial_plan_id
17529 );
17530 }
17531
17532 // Pending activation, add message.
17533 return $this->set_pending_confirmation(
17534 ( isset( $decoded->email ) ?
17535 $decoded->email :
17536 true ),
17537 false,
17538 $filtered_license_key,
17539 ! empty( $params['trial_plan_id'] ),
17540 isset( $decoded->is_suspicious_email ) && $decoded->is_suspicious_email
17541 );
17542 } else if ( isset( $decoded->install_secret_key ) ) {
17543 return $this->install_with_new_user(
17544 $decoded->user_id,
17545 $decoded->user_public_key,
17546 $decoded->user_secret_key,
17547 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17548 $decoded->is_marketing_allowed :
17549 null ),
17550 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17551 $decoded->is_extensions_tracking_allowed :
17552 null ),
17553 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17554 $decoded->is_diagnostic_tracking_allowed :
17555 null ),
17556 $decoded->install_id,
17557 $decoded->install_public_key,
17558 $decoded->install_secret_key,
17559 false
17560 );
17561 } else if ( is_array( $decoded->installs ) ) {
17562 return $this->install_many_with_new_user(
17563 $decoded->user_id,
17564 $decoded->user_public_key,
17565 $decoded->user_secret_key,
17566 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17567 $decoded->is_marketing_allowed :
17568 null ),
17569 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17570 $decoded->is_extensions_tracking_allowed :
17571 null ),
17572 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17573 $decoded->is_diagnostic_tracking_allowed :
17574 null ),
17575 $decoded->installs,
17576 false
17577 );
17578 }
17579
17580 return $decoded;
17581 }
17582
17583 /**
17584 * Set user and site identities.
17585 *
17586 * @author Vova Feldman (@svovaf)
17587 * @since 1.0.9
17588 *
17589 * @param FS_User $user
17590 * @param FS_Site $site
17591 * @param bool $redirect
17592 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
17593 * redirect (or return a URL) to the account page with a special parameter to
17594 * trigger the auto installation processes.
17595 *
17596 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17597 */
17598 function setup_account(
17599 FS_User $user,
17600 FS_Site $site,
17601 $redirect = true,
17602 $auto_install = false
17603 ) {
17604 return $this->setup_network_account(
17605 $user,
17606 array( $site ),
17607 $redirect,
17608 $auto_install,
17609 false
17610 );
17611 }
17612
17613 /**
17614 * Set user and site identities.
17615 *
17616 * @author Vova Feldman (@svovaf)
17617 * @since 2.0.0
17618 *
17619 * @param FS_User $user
17620 * @param FS_Site[] $installs
17621 * @param bool $redirect
17622 * @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.
17623 * @param bool $is_network_level_opt_in
17624 *
17625 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17626 */
17627 function setup_network_account(
17628 FS_User $user,
17629 array $installs,
17630 $redirect = true,
17631 $auto_install = false,
17632 $is_network_level_opt_in = true
17633 ) {
17634 $first_install = $installs[0];
17635
17636 $this->_user = $user;
17637 $this->_site = $first_install;
17638
17639 $this->_sync_plans();
17640
17641 if ( $this->_storage->handle_gdpr_admin_notice &&
17642 $this->should_handle_gdpr_admin_notice() &&
17643 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
17644 ) {
17645 /**
17646 * Clear user lock after an opt-in.
17647 */
17648 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
17649 FS_User_Lock::instance()->unlock();
17650 }
17651
17652 if ( 1 < count( $installs ) ) {
17653 // Only network level opt-in can have more than one install.
17654 $is_network_level_opt_in = true;
17655 }
17656
17657 $this->update_connectivity_info( true );
17658
17659 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
17660 // If Freemius was OFF before, turn it on.
17661 $this->turn_on();
17662
17663 $this->handle_account_connection(
17664 $installs,
17665 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
17666 );
17667
17668 if ( is_numeric( $first_install->license_id ) ) {
17669 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
17670 }
17671
17672 $this->_admin_notices->remove_sticky( 'connect_account' );
17673
17674 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
17675 $this->clear_pending_activation_mode();
17676
17677 if ( ! $this->is_paying_or_trial() ) {
17678 $this->_admin_notices->add_sticky(
17679 sprintf( $this->get_text_inline( '%s opt-in was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
17680 'activation_complete'
17681 );
17682 }
17683 }
17684
17685 if ( $this->is_paying_or_trial() ) {
17686 if ( ! $this->is_premium() ||
17687 ! $this->has_premium_version() ||
17688 ! $this->has_settings_menu()
17689 ) {
17690 if ( $this->is_paying() ) {
17691 $this->add_complete_upgrade_instructions_notice(
17692 sprintf(
17693 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
17694 $this->get_plan_title()
17695 ),
17696 'plan_upgraded'
17697 );
17698 } else {
17699 $trial_plan = $this->get_trial_plan();
17700
17701 $this->add_complete_upgrade_instructions_notice(
17702 sprintf(
17703 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
17704 '<i>' . $this->get_plugin_name() . '</i>'
17705 ),
17706 'trial_started',
17707 $trial_plan->title
17708 );
17709 }
17710 }
17711
17712 $this->_admin_notices->remove_sticky( array(
17713 'trial_promotion',
17714 ) );
17715 }
17716
17717 $plugin_id = fs_request_get( 'plugin_id', false );
17718
17719 // Store activation time ONLY for plugins & themes (not add-ons).
17720 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
17721 if ( empty( $this->_storage->activation_timestamp ) ) {
17722 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
17723 }
17724 }
17725
17726 $next_page = '';
17727
17728 $extra = array();
17729 if ( $auto_install ) {
17730 $extra['auto_install'] = 'true';
17731 }
17732
17733 if ( is_numeric( $plugin_id ) ) {
17734 /**
17735 * @author Leo Fajardo (@leorw)
17736 * @since 1.2.1.6
17737 *
17738 * Also sync the license after an anonymous user subscribes.
17739 */
17740 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
17741 // Add-on was installed - sync license right after install.
17742 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
17743 }
17744 } else {
17745 /**
17746 * @author Vova Feldman (@svovaf)
17747 * @since 1.1.9 If site installed with a valid license, sync license.
17748 */
17749 if ( $this->is_paying() ) {
17750 $this->_sync_plugin_license(
17751 true,
17752 // Installs data is already synced in the beginning of this method directly or via _set_account().
17753 false
17754 );
17755 }
17756
17757 // Reload the page with the keys.
17758 $next_page = $this->is_anonymous() ?
17759 // If user previously skipped, redirect to account page.
17760 $this->get_account_url( false, $extra ) :
17761 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
17762 }
17763
17764 if ( ! empty( $next_page ) && $redirect ) {
17765 fs_redirect( $next_page );
17766 }
17767
17768 return $next_page;
17769 }
17770
17771 /**
17772 * Install plugin with new user information after approval.
17773 *
17774 * @author Vova Feldman (@svovaf)
17775 * @since 1.0.7
17776 */
17777 function _install_with_new_user() {
17778 $this->_logger->entrance();
17779
17780 if ( $this->is_registered() ) {
17781 return;
17782 }
17783
17784 $has_pending_activation_confirmation_param = fs_request_has( 'pending_activation' );
17785
17786 $this->update_license_required_permissions_if_anonymous();
17787
17788 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17789 // @todo This logic should be improved because it's executed on every load of a theme.
17790 $this->is_theme()
17791 ) {
17792 // check_admin_referer( $this->_slug . '_activate_new' );
17793
17794 if ( fs_request_has( 'user_secret_key' ) ) {
17795 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17796 $pending_sites_info = $this->_storage->pending_sites_info;
17797
17798 $this->install_many_pending_with_user(
17799 fs_request_get( 'user_id' ),
17800 fs_request_get_raw( 'user_public_key' ),
17801 fs_request_get_raw( 'user_secret_key' ),
17802 fs_request_get_bool( 'is_marketing_allowed', null ),
17803 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17804 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17805 $pending_sites_info['blog_ids'],
17806 $pending_sites_info['license_key'],
17807 $pending_sites_info['trial_plan_id']
17808 );
17809 } else {
17810 $this->install_with_new_user(
17811 fs_request_get( 'user_id' ),
17812 fs_request_get_raw( 'user_public_key' ),
17813 fs_request_get_raw( 'user_secret_key' ),
17814 fs_request_get_bool( 'is_marketing_allowed', null ),
17815 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17816 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17817 fs_request_get( 'install_id' ),
17818 fs_request_get_raw( 'install_public_key' ),
17819 fs_request_get_raw( 'install_secret_key' ),
17820 true,
17821 fs_request_get_bool( 'auto_install' )
17822 );
17823 }
17824 } else if ( $has_pending_activation_confirmation_param ) {
17825 $this->set_pending_confirmation(
17826 fs_request_get( 'user_email' ),
17827 true,
17828 false,
17829 false,
17830 fs_request_get_bool( 'is_suspicious_email' ),
17831 fs_request_get_bool( 'has_upgrade_context' ),
17832 fs_request_get( 'support_email_address' )
17833 );
17834 }
17835 }
17836 }
17837
17838 /**
17839 * @author Vova Feldman (@svovaf)
17840 * @since 2.0.0
17841 *
17842 * @param number $id
17843 * @param string $public_key
17844 * @param string $secret_key
17845 *
17846 * @return \FS_User
17847 */
17848 private function setup_user( $id, $public_key, $secret_key ) {
17849 $user = self::_get_user_by_id( $id );
17850
17851 if ( is_object( $user ) ) {
17852 $this->_user = $user;
17853 } else {
17854 $user = new FS_User();
17855 $user->id = $id;
17856 $user->public_key = $public_key;
17857 $user->secret_key = $secret_key;
17858
17859 $this->_user = $user;
17860 $user_result = $this->get_api_user_scope()->get();
17861 $user = new FS_User( $user_result );
17862
17863 $this->_user = $user;
17864 $this->_store_user();
17865 }
17866
17867 return $user;
17868 }
17869
17870 /**
17871 * Install plugin with new user.
17872 *
17873 * @author Vova Feldman (@svovaf)
17874 * @since 1.1.7.4
17875 *
17876 * @param number $user_id
17877 * @param string $user_public_key
17878 * @param string $user_secret_key
17879 * @param bool|null $is_marketing_allowed
17880 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17881 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17882 * @param number $install_id
17883 * @param string $install_public_key
17884 * @param string $install_secret_key
17885 * @param bool $redirect
17886 * @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.
17887 *
17888 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17889 */
17890 private function install_with_new_user(
17891 $user_id,
17892 $user_public_key,
17893 $user_secret_key,
17894 $is_marketing_allowed,
17895 $is_extensions_tracking_allowed,
17896 $is_diagnostic_tracking_allowed,
17897 $install_id,
17898 $install_public_key,
17899 $install_secret_key,
17900 $redirect = true,
17901 $auto_install = false
17902 ) {
17903 /**
17904 * This method is also executed after opting in with a license key since the
17905 * license can be potentially associated with a different owner.
17906 *
17907 * @since 2.0.0
17908 */
17909 $user = self::_get_user_by_id( $user_id );
17910
17911 if ( ! is_object( $user ) ) {
17912 $user = new FS_User();
17913 $user->id = $user_id;
17914 $user->public_key = $user_public_key;
17915 $user->secret_key = $user_secret_key;
17916
17917 $this->_user = $user;
17918 $user_result = $this->get_api_user_scope()->get();
17919 $user = new FS_User( $user_result );
17920 }
17921
17922 $this->_user = $user;
17923
17924 $site = new FS_Site();
17925 $site->id = $install_id;
17926 $site->public_key = $install_public_key;
17927 $site->secret_key = $install_secret_key;
17928
17929 $this->_site = $site;
17930 $site_result = $this->get_api_site_scope( true )->get();
17931 $site = new FS_Site( $site_result );
17932 $this->_site = $site;
17933
17934 if ( ! is_null( $is_marketing_allowed ) ) {
17935 $this->disable_opt_in_notice_and_lock_user();
17936 }
17937
17938 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17939 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17940 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17941 ) );
17942
17943 return $this->setup_account(
17944 $this->_user,
17945 $this->_site,
17946 $redirect,
17947 $auto_install
17948 );
17949 }
17950
17951 /**
17952 * Install plugin with user.
17953 *
17954 * @author Leo Fajardo (@leorw)
17955 * @since 2.0.0
17956 *
17957 * @param number $user_id
17958 * @param string $user_public_key
17959 * @param string $user_secret_key
17960 * @param bool|null $is_marketing_allowed
17961 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17962 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17963 * @param array $site_ids
17964 * @param bool $license_key
17965 * @param bool $trial_plan_id
17966 * @param bool $redirect
17967 *
17968 * @return void
17969 */
17970 private function install_many_pending_with_user(
17971 $user_id,
17972 $user_public_key,
17973 $user_secret_key,
17974 $is_marketing_allowed,
17975 $is_extensions_tracking_allowed,
17976 $is_diagnostic_tracking_allowed,
17977 $site_ids,
17978 $license_key = false,
17979 $trial_plan_id = false,
17980 $redirect = true
17981 ) {
17982 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17983
17984 if ( ! is_null( $is_marketing_allowed ) ) {
17985 $this->disable_opt_in_notice_and_lock_user();
17986 }
17987
17988 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17989 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17990 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17991 ) );
17992
17993 $sites = array();
17994 foreach ( $site_ids as $site_id ) {
17995 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
17996 }
17997
17998 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17999 }
18000
18001 /**
18002 * Multi-site install with a new user.
18003 *
18004 * @author Vova Feldman (@svovaf)
18005 * @since 2.0.0
18006 *
18007 * @param number $user_id
18008 * @param string $user_public_key
18009 * @param string $user_secret_key
18010 * @param bool|null $is_marketing_allowed
18011 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
18012 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
18013 * @param object[] $installs
18014 * @param bool $redirect
18015 * @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.
18016 *
18017 * @return string If redirect is `false`, returns the next page the user should be redirected to.
18018 */
18019 private function install_many_with_new_user(
18020 $user_id,
18021 $user_public_key,
18022 $user_secret_key,
18023 $is_marketing_allowed,
18024 $is_extensions_tracking_allowed,
18025 $is_diagnostic_tracking_allowed,
18026 array $installs,
18027 $redirect = true,
18028 $auto_install = false
18029 ) {
18030 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
18031
18032 if ( ! is_null( $is_marketing_allowed ) ) {
18033 $this->disable_opt_in_notice_and_lock_user();
18034 }
18035
18036 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
18037 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
18038 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
18039 ) );
18040
18041 $install_ids = array();
18042
18043 foreach ( $installs as $install ) {
18044 $install_ids[] = $install->id;
18045 }
18046
18047 $items_per_request = 25;
18048 $left = count( $install_ids );
18049 $offset = 0;
18050
18051 $installs = array();
18052 while ( $left > 0 ) {
18053 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, $items_per_request ) ) );
18054
18055 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18056 // @todo Handle API error.
18057 }
18058
18059 $installs = array_merge( $installs, $result->installs );
18060
18061 $left -= $items_per_request;
18062 $offset += $items_per_request;
18063 }
18064
18065 foreach ( $installs as &$install ) {
18066 $install = new FS_Site( $install );
18067 }
18068
18069 return $this->setup_network_account(
18070 $this->_user,
18071 $installs,
18072 $redirect,
18073 $auto_install
18074 );
18075 }
18076
18077 /**
18078 * @author Vova Feldman (@svovaf)
18079 * @since 1.1.7.4
18080 *
18081 * @param string|bool $email
18082 * @param bool $redirect
18083 * @param string|bool $license_key Since 1.2.1.5
18084 * @param bool $is_pending_trial Since 1.2.1.5
18085 * @param bool $is_suspicious_email Since 2.5.0
18086 * @param bool $has_upgrade_context Since 2.5.3
18087 * @param bool|string $support_email_address Since 2.5.3
18088 *
18089 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
18090 */
18091 private function set_pending_confirmation(
18092 $email = false,
18093 $redirect = true,
18094 $license_key = false,
18095 $is_pending_trial = false,
18096 $is_suspicious_email = false,
18097 $has_upgrade_context = false,
18098 $support_email_address = false
18099 ) {
18100 $is_network_admin = fs_is_network_admin();
18101
18102 if ( $this->_ignore_pending_mode && ! $has_upgrade_context ) {
18103 /**
18104 * If explicitly asked to ignore pending mode, set to anonymous mode
18105 * 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).
18106 *
18107 * @author Vova Feldman
18108 * @since 1.2.1.6
18109 */
18110 $this->skip_connection( $is_network_admin );
18111 } else {
18112 // Install must be activated via email since
18113 // user with the same email already exist.
18114 $this->_storage->is_pending_activation = true;
18115 $this->_add_pending_activation_notice(
18116 $email,
18117 $is_pending_trial,
18118 $is_suspicious_email,
18119 $has_upgrade_context,
18120 $support_email_address
18121 );
18122 }
18123
18124 if ( ! empty( $license_key ) ) {
18125 $this->_storage->pending_license_key = $license_key;
18126 }
18127
18128 // Remove the opt-in sticky notice.
18129 $this->_admin_notices->remove_sticky( array(
18130 'connect_account',
18131 'trial_promotion',
18132 ) );
18133
18134 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
18135
18136 if ( $redirect ) {
18137 // Reload the page with a pending activation message.
18138 fs_redirect( $next_page );
18139 }
18140
18141 return $next_page;
18142 }
18143
18144 /**
18145 * Install plugin with current logged WP user info.
18146 *
18147 * @author Vova Feldman (@svovaf)
18148 * @since 1.0.7
18149 */
18150 function _install_with_current_user() {
18151 $this->_logger->entrance();
18152
18153 if ( $this->is_registered() ) {
18154 return;
18155 }
18156
18157 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
18158 check_admin_referer( $this->get_unique_affix() . '_activate_existing' );
18159
18160 /**
18161 * @author Vova Feldman (@svovaf)
18162 * @since 1.1.9 Add license key if given.
18163 */
18164 $license_key = fs_request_get_raw( 'license_secret_key' );
18165
18166 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
18167 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
18168 FS_Permission_Manager::PERMISSION_EXTENSIONS => fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
18169 ) );
18170
18171 $this->install_with_current_user( $license_key );
18172 }
18173 }
18174
18175
18176 /**
18177 * @author Vova Feldman (@svovaf)
18178 * @since 1.1.7.4
18179 *
18180 * @param string|bool $license_key
18181 * @param number|bool $trial_plan_id
18182 * @param array $sites Since 2.0.0
18183 * @param bool $redirect
18184 *
18185 * @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.
18186 */
18187 function install_with_current_user(
18188 $license_key = false,
18189 $trial_plan_id = false,
18190 $sites = array(),
18191 $redirect = true
18192 ) {
18193 // Get current logged WP user.
18194 $current_user = self::_get_current_wp_user();
18195
18196 // Find the relevant FS user by the email.
18197 $user = self::_get_user_by_email( $current_user->user_email );
18198
18199 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
18200 }
18201
18202 /**
18203 * @author Vova Feldman (@svovaf)
18204 * @since 2.0.0
18205 *
18206 * @param \FS_User $user
18207 * @param string|bool $license_key
18208 * @param number|bool $trial_plan_id
18209 * @param bool $redirect
18210 * @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.
18211 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
18212 *
18213 * @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.
18214 */
18215 function install_with_user(
18216 FS_User $user,
18217 $license_key = false,
18218 $trial_plan_id = false,
18219 $redirect = true,
18220 $setup_account = true,
18221 $sites = array()
18222 ) {
18223 // We have to set the user before getting user scope API handler.
18224 $this->_user = $user;
18225
18226 // Install the plugin.
18227 $result = $this->create_installs_with_user(
18228 $user,
18229 $license_key,
18230 $trial_plan_id,
18231 $sites,
18232 $redirect
18233 );
18234
18235 if ( ! $this->is_api_result_entity( $result ) &&
18236 ! $this->is_api_result_object( $result, 'installs' )
18237 ) {
18238 // @todo Handler potential API error of the $result
18239 }
18240
18241 if ( empty( $sites ) ) {
18242 $site = new FS_Site( $result );
18243 $this->_site = $site;
18244
18245 if ( ! $setup_account ) {
18246 $this->_store_site();
18247
18248 $this->sync_plan_if_not_exist( $site->plan_id );
18249
18250 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
18251 $this->sync_license_if_not_exist( $site->license_id, $license_key );
18252 }
18253
18254 $this->_admin_notices->remove_sticky( 'connect_account', false );
18255
18256 return $site;
18257 }
18258
18259 return $this->setup_account( $this->_user, $this->_site, $redirect );
18260 } else {
18261 $installs = array();
18262 foreach ( $result->installs as $install ) {
18263 $installs[] = new FS_Site( $install );
18264 }
18265
18266 return $this->setup_network_account(
18267 $user,
18268 $installs,
18269 $redirect
18270 );
18271 }
18272 }
18273
18274 /**
18275 * Initiate an API request to create a collection of installs.
18276 *
18277 * @author Vova Feldman (@svovaf)
18278 * @since 2.0.0
18279 *
18280 * @param \FS_User $user
18281 * @param bool $license_key
18282 * @param bool $trial_plan_id
18283 * @param array $sites
18284 * @param bool $redirect
18285 * @param bool $silent
18286 *
18287 * @return object|mixed
18288 */
18289 private function create_installs_with_user(
18290 FS_User $user,
18291 $license_key = false,
18292 $trial_plan_id = false,
18293 $sites = array(),
18294 $redirect = false,
18295 $silent = false
18296 ) {
18297 $extra_install_params = array(
18298 'uid' => $this->get_anonymous_id(),
18299 'is_disconnected' => false,
18300 );
18301
18302 if ( ! empty( $license_key ) ) {
18303 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
18304
18305 if ( $silent ) {
18306 $extra_install_params['ignore_license_owner'] = true;
18307 }
18308 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
18309 $extra_install_params['trial_plan_id'] = $trial_plan_id;
18310 }
18311
18312 if ( ! empty( $sites ) ) {
18313 $extra_install_params['sites'] = $sites;
18314 }
18315
18316 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
18317
18318 // Install the plugin.
18319 $result = $this->get_api_user_scope_by_user( $user )->call(
18320 "/plugins/{$this->get_id()}/installs.json",
18321 'post',
18322 $args
18323 );
18324
18325 if ( ! $this->is_api_result_entity( $result ) &&
18326 ! $this->is_api_result_object( $result, 'installs' )
18327 ) {
18328 if ( ! empty( $args['license_key'] ) ) {
18329 // Pass the fully entered license key to the failure handler.
18330 $args['license_key'] = $license_key;
18331 }
18332
18333 $result = $this->apply_filters( 'after_install_failure', $result, $args );
18334
18335 if ( ! $silent ) {
18336 $this->_admin_notices->add(
18337 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18338 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
18339 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18340 'error'
18341 );
18342 }
18343
18344 if ( $redirect ) {
18345 /**
18346 * We set the user before getting the user scope API handler, so the user became temporarily
18347 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
18348 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
18349 * activation page based on the return value of `is_registered()`. In addition, in case the
18350 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
18351 * misleading plugin activation errors will be shown on the `Plugins` page.
18352 *
18353 * @author Leo Fajardo (@leorw)
18354 */
18355 $this->_user = null;
18356
18357 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
18358 }
18359 }
18360
18361 return $result;
18362 }
18363
18364 /**
18365 * Tries to activate add-on account based on parent plugin info.
18366 *
18367 * @author Vova Feldman (@svovaf)
18368 * @since 1.0.6
18369 *
18370 * @param Freemius $parent_fs
18371 * @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.
18372 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
18373 */
18374 private function _activate_addon_account(
18375 Freemius $parent_fs,
18376 $network_level_or_blog_id = null,
18377 FS_Plugin_License $bundle_license = null
18378 ) {
18379 if ( $this->is_registered() ) {
18380 // Already activated.
18381 return;
18382 }
18383
18384 $permission_ids = FS_Permission_Manager::get_all_permission_ids();
18385 $permissions = array();
18386 foreach ( $permission_ids as $permission_id ) {
18387 $permissions[ $permission_id ] = FS_Permission_Manager::instance( $parent_fs )->is_permission( $permission_id, true );
18388 }
18389
18390 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( $permissions );
18391
18392 /**
18393 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
18394 * already returns the data for the current blog.
18395 *
18396 * @author Leo Fajardo (@leorw)
18397 * @since 2.3.0
18398 */
18399 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
18400 array() :
18401 array( 'uid' => $this->get_anonymous_id() );
18402
18403 $params = $this->get_install_data_for_api(
18404 $uid_param_to_override,
18405 false,
18406 false,
18407 /**
18408 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
18409 * already includes the data for it.
18410 *
18411 * @author Leo Fajardo (@leorw)
18412 * @since 2.3.0
18413 */
18414 ( true !== $network_level_or_blog_id )
18415 );
18416
18417 if ( true === $network_level_or_blog_id ) {
18418 $params['sites'] = $this->get_sites_for_network_level_optin();
18419
18420 if ( empty( $params['sites'] ) ) {
18421 return;
18422 }
18423 }
18424
18425 if ( is_object( $bundle_license ) ) {
18426 $params['license_key'] = $bundle_license->secret_key;
18427 }
18428
18429 // Activate add-on with parent plugin credentials.
18430 $result = $parent_fs->get_api_site_scope()->call(
18431 "/addons/{$this->_plugin->id}/installs.json",
18432 'post',
18433 $params
18434 );
18435
18436 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18437 if ( is_object( $bundle_license ) ) {
18438 /**
18439 * 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.
18440 *
18441 * @author Leo Fajardo (@leorw)
18442 * @since 2.4.0
18443 */
18444 } else {
18445 $error_message = FS_Api::is_api_error_object( $result ) ?
18446 $result->error->message :
18447 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
18448
18449 $this->_admin_notices->add(
18450 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18451 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
18452 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18453 'error'
18454 );
18455 }
18456
18457 return;
18458 }
18459
18460 $addon_installs = $result->installs;
18461 foreach ( $addon_installs as $key => $addon_install ) {
18462 $addon_installs[ $key ] = new FS_Site( $addon_install );
18463 }
18464
18465 $first_install = $addon_installs[0];
18466
18467 // Get user information based on parent's plugin.
18468 $user = $parent_fs->get_user();
18469
18470 // First of all, set site and user info - otherwise we won't
18471 // be able to invoke API calls.
18472 $this->_site = $first_install;
18473 $this->_user = $user;
18474
18475 // Sync add-on plans.
18476 $this->_sync_plans();
18477
18478 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
18479
18480 // Get site's current plan.
18481 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
18482
18483 // Sync licenses.
18484 $this->_sync_licenses();
18485
18486 if ( ! fs_is_network_admin() ) {
18487 // Try to activate premium license.
18488 $this->_activate_license( true, $bundle_license );
18489
18490 if ( is_object( $bundle_license ) ) {
18491 $this->maybe_activate_bundle_license( $bundle_license );
18492 }
18493 } else {
18494 if ( is_object( $bundle_license ) ) {
18495 $premium_license = $bundle_license;
18496 } else {
18497 $license_id = fs_request_get( 'license_id' );
18498
18499 if ( is_object( $this->_site ) &&
18500 FS_Plugin_License::is_valid_id( $license_id ) &&
18501 $license_id == $this->_site->license_id
18502 ) {
18503 // License is already activated.
18504 return;
18505 }
18506
18507 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
18508 $this->_get_license_by_id( $license_id ) :
18509 $this->_get_available_premium_license();
18510 }
18511
18512 if ( is_object( $premium_license ) ) {
18513 $this->maybe_network_activate_addon_license( $premium_license );
18514 }
18515 }
18516 }
18517
18518 /**
18519 * @author Leo Fajardo (@leorw)
18520 * @since 2.3.0
18521 *
18522 * @param FS_Site[] $installs
18523 * @param bool $is_site_level
18524 */
18525 private function handle_account_connection( $installs, $is_site_level ) {
18526 $first_install = $installs[0];
18527
18528 if ( $is_site_level ) {
18529 $this->_set_account( $this->_user, $first_install );
18530
18531 $this->do_action( 'after_account_connection', $this->_user, $first_install );
18532 } else {
18533 $this->_store_user();
18534
18535 // Map site addresses to their blog IDs.
18536 $address_to_blog_map = $this->get_address_to_blog_map();
18537
18538 $first_blog_id = null;
18539 $blog_2_install_map = array();
18540 foreach ( $installs as $install ) {
18541 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
18542 $blog_id = $address_to_blog_map[ $address ];
18543
18544 $this->_store_site( true, $blog_id, $install );
18545
18546 if ( is_null( $first_blog_id ) ) {
18547 $first_blog_id = $blog_id;
18548 }
18549
18550 $blog_2_install_map[ $blog_id ] = $install;
18551 }
18552
18553 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
18554 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
18555 ) {
18556 // Store network user.
18557 $this->_storage->network_user_id = $this->_user->id;
18558 }
18559
18560 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
18561 $this->_storage->network_install_blog_id = $first_blog_id;
18562 }
18563
18564 if ( count( $installs ) === count( $address_to_blog_map ) ) {
18565 // Super admin opted in for all sites in the network.
18566 $this->_storage->is_network_connected = true;
18567 }
18568
18569 $this->_store_licenses( false );
18570
18571 self::$_accounts->store();
18572
18573 // Don't sync the installs data on network upgrade
18574 if ( ! $this->network_upgrade_mode_completed() ) {
18575 $this->send_installs_update();
18576 }
18577
18578 $current_blog = get_current_blog_id();
18579
18580 foreach ( $blog_2_install_map as $blog_id => $install ) {
18581 $this->switch_to_blog( $blog_id );
18582
18583 $this->do_action( 'after_account_connection', $this->_user, $install );
18584 }
18585
18586 // Switch install context back to the first install.
18587 $this->switch_to_blog(
18588 $current_blog,
18589 $first_install,
18590 ( $this->_site->id != $first_install->id )
18591 );
18592
18593 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
18594 }
18595 }
18596
18597 /**
18598 * Tries to activate parent account based on add-on's info.
18599 *
18600 * @author Vova Feldman (@svovaf)
18601 * @since 1.2.2.7
18602 *
18603 * @param Freemius $parent_fs
18604 */
18605 private function activate_parent_account( Freemius $parent_fs ) {
18606 if ( ! $this->is_addon() ) {
18607 // This is not an add-on.
18608 return;
18609 }
18610
18611 if ( $parent_fs->is_registered() ) {
18612 // Already activated.
18613 return;
18614 }
18615
18616 // Activate parent with add-on's user credentials.
18617 $parent_install = $this->get_api_user_scope()->call(
18618 "/plugins/{$parent_fs->_plugin->id}/installs.json",
18619 'post',
18620 $parent_fs->get_install_data_for_api( array(
18621 'uid' => $parent_fs->get_anonymous_id(),
18622 ), false, false )
18623 );
18624
18625 if ( isset( $parent_install->error ) ) {
18626 $this->_admin_notices->add(
18627 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18628 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
18629 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18630 'error'
18631 );
18632
18633 return;
18634 }
18635
18636 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
18637
18638 if ( $parent_fs->is_pending_activation() ) {
18639 $parent_fs->clear_pending_activation_mode();
18640 }
18641
18642 // Get user information based on parent's plugin.
18643 $user = $this->get_user();
18644
18645 // First of all, set site info - otherwise we won't
18646 // be able to invoke API calls.
18647 $parent_fs->_site = new FS_Site( $parent_install );
18648 $parent_fs->_user = $user;
18649
18650 // Sync add-on plans.
18651 $parent_fs->_sync_plans();
18652
18653 $parent_fs->update_license_required_permissions_if_anonymous();
18654
18655 $parent_fs->_set_account( $user, $parent_fs->_site );
18656 }
18657
18658 #endregion
18659
18660 #----------------------------------------------------------------------------------
18661 #region Admin Menu Items
18662 #----------------------------------------------------------------------------------
18663
18664 private $_menu_items = array();
18665
18666 /**
18667 * @author Vova Feldman (@svovaf)
18668 * @since 1.2.1.8
18669 *
18670 * @return array
18671 */
18672 function get_menu_items() {
18673 return $this->_menu_items;
18674 }
18675
18676 /**
18677 * @author Vova Feldman (@svovaf)
18678 * @since 1.0.7
18679 *
18680 * @return string
18681 */
18682 function get_menu_slug() {
18683 return $this->_menu->get_slug();
18684 }
18685
18686 /**
18687 * @author Vova Feldman (@svovaf)
18688 * @since 1.0.9
18689 */
18690 function _prepare_admin_menu() {
18691 // if ( ! $this->is_on() ) {
18692 // return;
18693 // }
18694
18695 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
18696 return;
18697 }
18698
18699 /**
18700 * When running from a site admin with a network activated module and the connection
18701 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
18702 * site level settings.
18703 *
18704 * @author Vova Feldman (@svovaf)
18705 * @since 2.0.0
18706 */
18707 $should_hide_site_admin_settings = (
18708 $this->_is_network_active &&
18709 ! fs_is_network_admin() &&
18710 ! $this->is_delegated_connection() &&
18711 ! $this->is_anonymous() &&
18712 ! $this->is_registered()
18713 );
18714
18715 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
18716
18717 if ( ( false === $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
18718 $should_hide_site_admin_settings
18719 ) {
18720 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
18721 } else {
18722 $this->do_action( fs_is_network_admin() ?
18723 'before_network_admin_menu_init' :
18724 'before_admin_menu_init'
18725 );
18726
18727 $this->add_menu_action();
18728
18729 $this->add_network_menu_when_missing();
18730
18731 $this->add_submenu_items();
18732 }
18733 }
18734
18735 /**
18736 * Admin dashboard menu items modifications.
18737 *
18738 * NOTE: admin_menu action executed before admin_init.
18739 *
18740 * @author Vova Feldman (@svovaf)
18741 * @since 1.0.7
18742 *
18743 */
18744 private function add_menu_action() {
18745 if ( $this->is_activation_mode() ) {
18746 if ( $this->show_opt_in_on_setting_page() ) {
18747 $this->override_plugin_menu_with_activation();
18748 } else {
18749 /**
18750 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
18751 */
18752 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18753 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
18754 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
18755 fs_request_get_bool( 'pending_activation' )
18756 ) {
18757 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
18758 }
18759 }
18760 } else {
18761 if ( ! $this->is_registered() ) {
18762 // If not registered try to install user.
18763 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18764 $this->_install_with_new_user();
18765 }
18766 } else if (
18767 fs_request_is_action( 'sync_user' ) &&
18768 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
18769 ) {
18770 $this->_handle_account_user_sync();
18771 }
18772 }
18773 }
18774
18775 /**
18776 * @author Vova Feldman (@svovaf)
18777 * @since 1.0.1
18778 */
18779 function _redirect_on_clicked_menu_link() {
18780 $this->_logger->entrance();
18781
18782 $page = fs_request_get('page');
18783 $page = is_string($page) ? strtolower($page) : '';
18784
18785 $this->_logger->log( 'page = ' . $page );
18786
18787 foreach ( $this->_menu_items as $priority => $items ) {
18788 foreach ( $items as $item ) {
18789 if ( isset( $item['url'] ) ) {
18790 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
18791 $this->_logger->log( 'Redirecting to ' . $item['url'] );
18792
18793 fs_redirect( $item['url'] );
18794 }
18795 }
18796 }
18797 }
18798 }
18799
18800 /**
18801 * Remove plugin's all admin menu items & pages, and replace with activation page.
18802 *
18803 * @author Vova Feldman (@svovaf)
18804 * @since 1.0.1
18805 */
18806 private function override_plugin_menu_with_activation() {
18807 $this->_logger->entrance();
18808
18809 $hook = false;
18810
18811 if ( ! $this->has_settings_menu() ) {
18812 // Add the opt-in page without a menu item.
18813 $hook = FS_Admin_Menu_Manager::add_subpage(
18814 '',
18815 $this->get_plugin_name(),
18816 $this->get_plugin_name(),
18817 'manage_options',
18818 $this->_slug,
18819 array( &$this, '_connect_page_render' )
18820 );
18821 } else if ( $this->_menu->is_top_level() ) {
18822 if ( $this->_menu->is_override_exact() ) {
18823 // Make sure the current page is matching the activation page.
18824 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18825 return;
18826 }
18827 }
18828
18829 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18830
18831 if ( false === $hook ) {
18832 // Create new menu item just for the opt-in.
18833 $hook = FS_Admin_Menu_Manager::add_page(
18834 $this->get_plugin_name(),
18835 $this->get_plugin_name(),
18836 'manage_options',
18837 $this->_menu->get_slug(),
18838 array( &$this, '_connect_page_render' )
18839 );
18840 }
18841 } else {
18842 $menus = array( $this->_menu->get_parent_slug() );
18843
18844 if ( $this->_menu->is_override_exact() ) {
18845 // Make sure the current page is matching the activation page.
18846 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18847 return;
18848 }
18849 }
18850
18851 foreach ( $menus as $parent_slug ) {
18852 $hook = $this->_menu->override_submenu_action(
18853 $parent_slug,
18854 $this->_menu->get_raw_slug(),
18855 array( &$this, '_connect_page_render' )
18856 );
18857
18858 if ( false !== $hook ) {
18859 // Found plugin's submenu item.
18860 break;
18861 }
18862 }
18863 }
18864
18865 if ( $this->is_activation_page() ) {
18866 // Clean admin page from distracting content.
18867 self::_clean_admin_content_section();
18868 }
18869
18870 if ( false !== $hook ) {
18871 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18872 $this->_install_with_current_user();
18873 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18874 $this->_install_with_new_user();
18875 }
18876 }
18877 }
18878
18879 /**
18880 * If a plugin was network activated and connected but don't have a network
18881 * level settings, then add an artificial menu item for the Account and other
18882 * Freemius settings.
18883 *
18884 * @author Vova Feldman (@svovaf)
18885 * @since 2.0.0
18886 */
18887 private function add_network_menu_when_missing() {
18888 $this->_logger->entrance();
18889
18890 if ( ! $this->_is_network_active ) {
18891 // Plugin wasn't activated on the network level.
18892 return;
18893 }
18894
18895 if ( ! fs_is_network_admin() ) {
18896 // The context is not the network admin.
18897 return;
18898 }
18899
18900 if ( $this->_menu->has_network_menu() ) {
18901 // Plugin already has a network level menu.
18902 return;
18903 }
18904
18905 if ( $this->is_network_activation_mode() ) {
18906 /**
18907 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
18908 * screen is being shown.
18909 *
18910 * @author Leo Fajardo (@leorw)
18911 */
18912 return;
18913 }
18914
18915 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
18916 if ( $this->is_network_delegated_connection() ) {
18917 // Super-admin delegated the connection to the site admins.
18918 return;
18919 }
18920 }
18921
18922 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
18923
18924 if ( $this->_menu->has_menu() ||
18925 ! $this->is_addon() ||
18926 $this->is_activation_mode()
18927 ) {
18928 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
18929 $this->get_plugin_name(),
18930 $this->get_plugin_name(),
18931 'manage_options',
18932 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
18933 );
18934 }
18935 } else {
18936 $this->_menu->add_subpage_and_update(
18937 $this->_menu->get_parent_slug(),
18938 $this->get_plugin_name(),
18939 $this->get_plugin_name(),
18940 'manage_options',
18941 $this->_menu->get_slug()
18942 );
18943 }
18944 }
18945
18946 /**
18947 * @author Leo Fajardo (@leorw)
18948 * @since 1.2.1
18949 *
18950 * return string
18951 */
18952 function get_top_level_menu_capability() {
18953 global $menu;
18954
18955 $top_level_menu_slug = $this->get_top_level_menu_slug();
18956
18957 foreach ( $menu as $menu_info ) {
18958 /**
18959 * The second element in the menu info array is the capability/role that has access to the menu and the
18960 * third element is the menu slug.
18961 */
18962 if ( $menu_info[2] === $top_level_menu_slug ) {
18963 return $menu_info[1];
18964 }
18965 }
18966
18967 return 'read';
18968 }
18969
18970 /**
18971 * @author Vova Feldman (@svovaf)
18972 * @since 1.0.0
18973 *
18974 * @return string
18975 */
18976 private function get_top_level_menu_slug() {
18977 return ( $this->is_addon() ?
18978 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
18979 $this->_menu->get_top_level_menu_slug() );
18980 }
18981
18982 /**
18983 * @author Vova Feldman (@svovaf)
18984 * @since 1.2.2.7
18985 *
18986 * @return string
18987 */
18988 function get_pricing_cta_label() {
18989 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
18990
18991 if ( $this->is_in_trial_promotion() &&
18992 ! $this->is_paying_or_trial()
18993 ) {
18994 // If running a trial promotion, modify the pricing to load the trial.
18995 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
18996 } else if ( $this->is_paying() ) {
18997 $label = $this->get_text_inline( 'Pricing', 'pricing' );
18998 }
18999
19000 return $label;
19001 }
19002
19003 /**
19004 * @author Vova Feldman (@svovaf)
19005 * @since 1.2.2.7
19006 *
19007 * @return bool
19008 */
19009 function is_pricing_page_visible() {
19010 return (
19011 // Has at least one paid plan.
19012 $this->has_paid_plan() &&
19013 // Didn't ask to hide the pricing page.
19014 $this->is_page_visible( 'pricing' ) &&
19015 // Don't have a valid active license or has more than one plan.
19016 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
19017 );
19018 }
19019
19020 /**
19021 * @author Leo Fajardo (@leorw)
19022 * @since 2.3.0
19023 *
19024 * @param bool $is_activation_mode
19025 *
19026 * @return bool
19027 */
19028 private function should_add_submenu_or_action_links( $is_activation_mode ) {
19029 if ( $this->is_addon() ) {
19030 // No submenu items or action links for add-ons.
19031 return false;
19032 }
19033
19034 if ( $this->show_opt_in_on_themes_page() ) {
19035 if ( ! fs_is_network_admin() ) {
19036 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
19037 return true;
19038 }
19039 } else if ( $is_activation_mode ) {
19040 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
19041 return false;
19042 }
19043
19044 if ( fs_is_network_admin() ) {
19045 /**
19046 * Add submenu items or action links to network level when plugin was network activated and the super
19047 * admin did NOT delegate the connection of all sites to site admins.
19048 */
19049 return (
19050 $this->_is_network_active &&
19051 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
19052 ! $this->is_network_delegated_connection() )
19053 );
19054 }
19055
19056 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
19057 }
19058
19059 /**
19060 * Add default Freemius menu items.
19061 *
19062 * @author Vova Feldman (@svovaf)
19063 * @since 1.0.0
19064 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
19065 */
19066 private function add_submenu_items() {
19067 $this->_logger->entrance();
19068
19069 $is_activation_mode = $this->is_activation_mode();
19070
19071 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
19072
19073 if ( $add_submenu_items ) {
19074 if ( $this->has_affiliate_program() ) {
19075 // Add affiliation page.
19076 $this->add_submenu_item(
19077 $this->get_text_inline( 'Affiliation', 'affiliation' ),
19078 array( &$this, '_affiliation_page_render' ),
19079 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
19080 'manage_options',
19081 'affiliation',
19082 'Freemius::_clean_admin_content_section',
19083 WP_FS__DEFAULT_PRIORITY,
19084 $this->is_submenu_item_visible( 'affiliation' )
19085 );
19086 }
19087 }
19088
19089 if ( $add_submenu_items ||
19090 ( $is_activation_mode &&
19091 $this->is_only_premium() &&
19092 $this->is_admin_page( 'account' ) &&
19093 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
19094 )
19095 ) {
19096 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
19097 $show_account = (
19098 $this->is_submenu_item_visible( 'account' ) &&
19099 /**
19100 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
19101 */
19102 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
19103 );
19104
19105 // Add user account page.
19106 $this->add_submenu_item(
19107 $this->get_text_inline( 'Account', 'account' ),
19108 array( &$this, '_account_page_render' ),
19109 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
19110 'manage_options',
19111 'account',
19112 array( &$this, '_account_page_load' ),
19113 WP_FS__DEFAULT_PRIORITY,
19114 ( $add_submenu_items && $show_account )
19115 );
19116 }
19117 }
19118
19119 if ( $add_submenu_items ) {
19120 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
19121 // Add contact page.
19122 $this->add_submenu_item(
19123 $this->get_text_inline( 'Contact Us', 'contact-us' ),
19124 array( &$this, '_contact_page_render' ),
19125 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
19126 'manage_options',
19127 'contact',
19128 'Freemius::_clean_admin_content_section',
19129 WP_FS__DEFAULT_PRIORITY,
19130 $this->is_submenu_item_visible( 'contact' )
19131 );
19132 }
19133
19134 if ( $this->has_addons() ) {
19135 $this->add_submenu_item(
19136 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
19137 array( &$this, '_addons_page_render' ),
19138 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
19139 'manage_options',
19140 'addons',
19141 array( &$this, '_addons_page_load' ),
19142 WP_FS__LOWEST_PRIORITY - 1,
19143 $this->is_submenu_item_visible( 'addons' )
19144 );
19145 }
19146 }
19147
19148 if ( $add_submenu_items ||
19149 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
19150 ) {
19151 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
19152 $show_pricing = (
19153 $this->is_submenu_item_visible( 'pricing' ) &&
19154 $this->is_pricing_page_visible()
19155 );
19156
19157 $pricing_cta_text = $this->get_pricing_cta_label();
19158 $pricing_class = 'upgrade-mode';
19159 if ( $show_pricing ) {
19160 if ( $this->is_in_trial_promotion() &&
19161 ! $this->is_paying_or_trial()
19162 ) {
19163 // If running a trial promotion, modify the pricing to load the trial.
19164 $pricing_class = 'trial-mode';
19165 } else if ( $this->is_paying() ) {
19166 $pricing_class = '';
19167 }
19168 }
19169
19170 // Add upgrade/pricing page.
19171 $this->add_submenu_item(
19172 $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' ) ),
19173 array( &$this, '_pricing_page_render' ),
19174 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
19175 'manage_options',
19176 'pricing',
19177 'Freemius::_clean_admin_content_section',
19178 WP_FS__LOWEST_PRIORITY,
19179 ( $add_submenu_items && $show_pricing ),
19180 $pricing_class
19181 );
19182 }
19183 }
19184
19185 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
19186 /**
19187 * Add the other menu items if there are any when not in activation mode or license activation is not
19188 * required (license activation is required for registered or anonymous users after activating the
19189 * premium version when the site is not in trial mode or there's no active valid license).
19190 *
19191 * @author Leo Fajardo (@leorw)
19192 * @since 2.2.1
19193 */
19194 if ( 0 < count( $this->_menu_items ) ) {
19195 if ( ! $this->_menu->is_top_level() ) {
19196 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
19197
19198 // Append submenu items right after the plugin's submenu item.
19199 $this->order_sub_submenu_items();
19200 } else {
19201 // Append submenu items.
19202 $this->embed_submenu_items();
19203 }
19204 }
19205 }
19206 }
19207
19208 /**
19209 * Moved the actual submenu item additions to a separated function,
19210 * in order to support sub-submenu items when the plugin's settings
19211 * only have a submenu and not top-level menu item.
19212 *
19213 * @author Vova Feldman (@svovaf)
19214 * @since 1.1.4
19215 */
19216 private function embed_submenu_items() {
19217 $item_template = $this->_menu->is_top_level() ?
19218 '<span class="fs-submenu-item %s %s %s">%s</span>' :
19219 '<span class="fs-submenu-item fs-sub %s %s %s">%s</span>';
19220
19221 $top_level_menu_capability = $this->get_top_level_menu_capability();
19222
19223 ksort( $this->_menu_items );
19224
19225 $is_first_submenu_item = true;
19226
19227 foreach ( $this->_menu_items as $priority => $items ) {
19228 foreach ( $items as $item ) {
19229 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
19230
19231 $menu_item = sprintf(
19232 $item_template,
19233 $this->get_unique_affix(),
19234 $item['menu_slug'],
19235 ! empty( $item['class'] ) ? $item['class'] : '',
19236 $item['menu_title']
19237 );
19238
19239 $top_level_menu_slug = $this->get_top_level_menu_slug();
19240 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
19241
19242 if ( ! isset( $item['url'] ) ) {
19243 $hook = FS_Admin_Menu_Manager::add_subpage(
19244 $item['show_submenu'] ?
19245 $top_level_menu_slug :
19246 '',
19247 $item['page_title'],
19248 $menu_item,
19249 $capability,
19250 $menu_slug,
19251 $item['render_function']
19252 );
19253
19254 if ( false !== $item['before_render_function'] ) {
19255 add_action( "load-$hook", $item['before_render_function'] );
19256 }
19257 } else {
19258 FS_Admin_Menu_Manager::add_subpage(
19259 $item['show_submenu'] ?
19260 $top_level_menu_slug :
19261 '',
19262 $item['page_title'],
19263 $menu_item,
19264 $capability,
19265 $menu_slug,
19266 array( $this, '' )
19267 );
19268 }
19269
19270 if ( $item['show_submenu'] && $is_first_submenu_item ) {
19271 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
19272 /**
19273 * If the top-level menu has been dynamically created, remove the first submenu item that
19274 * WordPress automatically creates when there's no submenu item whose slug matches the
19275 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
19276 *
19277 * Awesome Plugin
19278 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
19279 *
19280 * @author Leo Fajardo (@leorw)
19281 */
19282 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
19283 }
19284
19285 $is_first_submenu_item = false;
19286 }
19287 }
19288 }
19289 }
19290
19291 /**
19292 * Re-order the submenu items so all Freemius added new submenu items
19293 * are added right after the plugin's settings submenu item.
19294 *
19295 * @author Vova Feldman (@svovaf)
19296 * @since 1.1.4
19297 */
19298 private function order_sub_submenu_items() {
19299 global $submenu;
19300
19301 $menu_slug = $this->_menu->get_top_level_menu_slug();
19302
19303 /**
19304 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
19305 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
19306 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
19307 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
19308 *
19309 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
19310 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
19311 * capability needed to access the parent menu as the capability for the submenus that we will add.
19312 */
19313 if ( empty( $submenu[ $menu_slug ] ) ) {
19314 return;
19315 }
19316
19317 $top_level_menu = &$submenu[ $menu_slug ];
19318
19319 $all_submenu_items_after = array();
19320
19321 $found_submenu_item = false;
19322
19323 foreach ( $top_level_menu as $submenu_id => $meta ) {
19324 if ( $found_submenu_item ) {
19325 // Remove all submenu items after the plugin's submenu item.
19326 $all_submenu_items_after[] = $meta;
19327 unset( $top_level_menu[ $submenu_id ] );
19328 }
19329
19330 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
19331 // Found the submenu item, put all below.
19332 $found_submenu_item = true;
19333 continue;
19334 }
19335 }
19336
19337 // Embed all plugin's new submenu items.
19338 $this->embed_submenu_items();
19339
19340 // Start with specially high number to make sure it's appended.
19341 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
19342 foreach ( $all_submenu_items_after as $meta ) {
19343 $top_level_menu[ $i ] = $meta;
19344 $i ++;
19345 }
19346
19347 // Sort submenu items.
19348 ksort( $top_level_menu );
19349 }
19350
19351 /**
19352 * Helper method to return the module's support forum URL.
19353 *
19354 * @author Vova Feldman (@svovaf)
19355 * @since 1.2.2.7
19356 *
19357 * @return string
19358 */
19359 function get_support_forum_url() {
19360 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
19361 }
19362
19363 /**
19364 * Displays the Support Forum link when enabled.
19365 *
19366 * Can be filtered like so:
19367 *
19368 * function _fs_show_support_menu( $is_visible, $menu_id ) {
19369 * if ( 'support' === $menu_id ) {
19370 * return _fs->is_registered();
19371 * }
19372 * return $is_visible;
19373 * }
19374 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
19375 *
19376 */
19377 function _add_default_submenu_items() {
19378 if ( ! $this->is_on() ) {
19379 return;
19380 }
19381
19382 if ( ! $this->is_activation_mode() &&
19383 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
19384 ( ! $this->_is_network_active && is_admin() ) )
19385 ) {
19386 $this->add_submenu_link_item(
19387 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
19388 $this->get_support_forum_url(),
19389 'wp-support-forum',
19390 null,
19391 50,
19392 $this->is_submenu_item_visible( 'support' )
19393 );
19394 }
19395 }
19396
19397 /**
19398 * @author Vova Feldman (@svovaf)
19399 * @since 1.0.1
19400 *
19401 * @param string $menu_title
19402 * @param callable $render_function
19403 * @param bool|string $page_title
19404 * @param string $capability
19405 * @param bool|string $menu_slug
19406 * @param bool|callable $before_render_function
19407 * @param int $priority
19408 * @param bool $show_submenu
19409 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
19410 */
19411 function add_submenu_item(
19412 $menu_title,
19413 $render_function,
19414 $page_title = false,
19415 $capability = 'manage_options',
19416 $menu_slug = false,
19417 $before_render_function = false,
19418 $priority = WP_FS__DEFAULT_PRIORITY,
19419 $show_submenu = true,
19420 $class = ''
19421 ) {
19422 $this->_logger->entrance( 'Title = ' . $menu_title );
19423
19424 if ( $this->is_addon() ) {
19425 $parent_fs = $this->get_parent_instance();
19426
19427 if ( is_object( $parent_fs ) ) {
19428 $parent_fs->add_submenu_item(
19429 $menu_title,
19430 $render_function,
19431 $page_title,
19432 $capability,
19433 $menu_slug,
19434 $before_render_function,
19435 $priority,
19436 $show_submenu,
19437 $class
19438 );
19439
19440 return;
19441 }
19442 }
19443
19444 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19445 $this->_menu_items[ $priority ] = array();
19446 }
19447
19448 $this->_menu_items[ $priority ][] = array(
19449 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
19450 'menu_title' => $menu_title,
19451 'capability' => $capability,
19452 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19453 'render_function' => $render_function,
19454 'before_render_function' => $before_render_function,
19455 'show_submenu' => $show_submenu,
19456 'class' => $class,
19457 );
19458 }
19459
19460 /**
19461 * @author Vova Feldman (@svovaf)
19462 * @since 1.0.1
19463 *
19464 * @param string $menu_title
19465 * @param string $url
19466 * @param bool $menu_slug
19467 * @param string $capability
19468 * @param int $priority
19469 * @param bool $show_submenu
19470 */
19471 function add_submenu_link_item(
19472 $menu_title,
19473 $url,
19474 $menu_slug = false,
19475 $capability = 'read',
19476 $priority = WP_FS__DEFAULT_PRIORITY,
19477 $show_submenu = true
19478 ) {
19479 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
19480
19481 if ( $this->is_addon() ) {
19482 $parent_fs = $this->get_parent_instance();
19483
19484 if ( is_object( $parent_fs ) ) {
19485 $parent_fs->add_submenu_link_item(
19486 $menu_title,
19487 $url,
19488 $menu_slug,
19489 $capability,
19490 $priority,
19491 $show_submenu
19492 );
19493
19494 return;
19495 }
19496 }
19497
19498 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19499 $this->_menu_items[ $priority ] = array();
19500 }
19501
19502 $this->_menu_items[ $priority ][] = array(
19503 'menu_title' => $menu_title,
19504 'capability' => $capability,
19505 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19506 'url' => $url,
19507 'page_title' => $menu_title,
19508 'render_function' => 'fs_dummy',
19509 'before_render_function' => '',
19510 'show_submenu' => $show_submenu,
19511 );
19512 }
19513
19514 #endregion ------------------------------------------------------------------
19515
19516 #--------------------------------------------------------------------------------
19517 #region Admin Notices
19518 #--------------------------------------------------------------------------------
19519
19520 /**
19521 * @author Vova Feldman (@svovaf)
19522 * @since 2.3.1
19523 *
19524 * @param string|string[] $ids
19525 * @param int|null $network_level_or_blog_id
19526 *
19527 * @uses FS_Admin_Notices::remove_sticky()
19528 */
19529 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
19530 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
19531 }
19532
19533 #endregion
19534
19535 #--------------------------------------------------------------------------------
19536 #region Actions / Hooks / Filters
19537 #--------------------------------------------------------------------------------
19538
19539 /**
19540 * @author Vova Feldman (@svovaf)
19541 * @since 1.1.7
19542 *
19543 * @param string $tag
19544 *
19545 * @return string
19546 */
19547 public function get_action_tag( $tag ) {
19548 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
19549 }
19550
19551 /**
19552 * @author Vova Feldman (@svovaf)
19553 * @since 1.2.1.6
19554 *
19555 * @param string $tag
19556 * @param string $slug
19557 * @param bool $is_plugin
19558 *
19559 * @return string
19560 */
19561 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
19562 $action = "fs_{$tag}";
19563
19564 if ( ! empty( $slug ) ) {
19565 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
19566 }
19567
19568 return $action;
19569 }
19570
19571 /**
19572 * Returns a string that can be used to generate a unique action name,
19573 * option name, HTML element ID, or HTML element class.
19574 *
19575 * @author Leo Fajardo (@leorw)
19576 * @since 1.2.2
19577 *
19578 * @return string
19579 */
19580 public function get_unique_affix() {
19581 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
19582 }
19583
19584 /**
19585 * Returns a string that can be used to generate a unique action name,
19586 * option name, HTML element ID, or HTML element class.
19587 *
19588 * @author Vova Feldman (@svovaf)
19589 * @since 1.2.2.5
19590 *
19591 * @param string $slug
19592 * @param bool $is_plugin
19593 *
19594 * @return string
19595 */
19596 static function get_module_unique_affix( $slug, $is_plugin = true ) {
19597 $affix = $slug;
19598
19599 if ( ! $is_plugin ) {
19600 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
19601 }
19602
19603 return $affix;
19604 }
19605
19606 /**
19607 * @author Vova Feldman (@svovaf)
19608 * @since 1.2.1
19609 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19610 * based on the slug for backward compatibility.
19611 *
19612 * @param string $tag
19613 *
19614 * @return string
19615 */
19616 function get_ajax_action( $tag ) {
19617 return self::get_ajax_action_static( $tag, $this->_module_id );
19618 }
19619
19620 /**
19621 * @author Vova Feldman (@svovaf)
19622 * @since 1.2.1.7
19623 *
19624 * @param string $tag
19625 *
19626 * @return string
19627 */
19628 function get_ajax_security( $tag ) {
19629 return wp_create_nonce( $this->get_ajax_action( $tag ) );
19630 }
19631
19632 /**
19633 * @author Vova Feldman (@svovaf)
19634 * @since 1.2.1.7
19635 *
19636 * @param string $tag
19637 */
19638 function check_ajax_referer( $tag ) {
19639 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
19640 }
19641
19642 /**
19643 * @author Vova Feldman (@svovaf)
19644 * @since 1.2.1.6
19645 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19646 * based on the slug for backward compatibility.
19647 *
19648 * @param string $tag
19649 * @param number|null $module_id
19650 *
19651 * @return string
19652 */
19653 static function get_ajax_action_static( $tag, $module_id = null ) {
19654 $action = "fs_{$tag}";
19655
19656 if ( ! empty( $module_id ) ) {
19657 $action .= "_{$module_id}";
19658 }
19659
19660 return $action;
19661 }
19662
19663 /**
19664 * Do action, specific for the current context plugin.
19665 *
19666 * @author Vova Feldman (@svovaf)
19667 * @since 1.0.1
19668 *
19669 * @param string $tag The name of the action to be executed.
19670 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
19671 * functions hooked to the action. Default empty.
19672 *
19673 * @uses do_action()
19674 */
19675 function do_action( $tag, $arg = '' ) {
19676 $args = func_get_args();
19677
19678 $this->_logger->entrance( $tag );
19679
19680 call_user_func_array( 'do_action', array_merge(
19681 array( $this->get_action_tag( $tag ) ),
19682 array_slice( $args, 1 ) )
19683 );
19684 }
19685
19686 /**
19687 * Add action, specific for the current context plugin.
19688 *
19689 * @author Vova Feldman (@svovaf)
19690 * @since 1.0.1
19691 *
19692 * @param string $tag
19693 * @param callable $function_to_add
19694 * @param int $priority
19695 * @param int $accepted_args
19696 *
19697 * @uses add_action()
19698 */
19699 function add_action(
19700 $tag,
19701 $function_to_add,
19702 $priority = WP_FS__DEFAULT_PRIORITY,
19703 $accepted_args = 1
19704 ) {
19705 $this->_logger->entrance( $tag );
19706
19707 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19708 }
19709
19710 /**
19711 * Add AJAX action, specific for the current context plugin.
19712 *
19713 * @author Vova Feldman (@svovaf)
19714 * @since 1.2.1
19715 *
19716 * @param string $tag
19717 * @param callable $function_to_add
19718 * @param int $priority
19719 *
19720 * @uses add_action()
19721 *
19722 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19723 */
19724 function add_ajax_action(
19725 $tag,
19726 $function_to_add,
19727 $priority = WP_FS__DEFAULT_PRIORITY
19728 ) {
19729 $this->_logger->entrance( $tag );
19730
19731 return self::add_ajax_action_static(
19732 $tag,
19733 $function_to_add,
19734 $priority,
19735 $this->_module_id
19736 );
19737 }
19738
19739 /**
19740 * Add AJAX action.
19741 *
19742 * @author Vova Feldman (@svovaf)
19743 * @since 1.2.1.6
19744 *
19745 * @param string $tag
19746 * @param callable $function_to_add
19747 * @param int $priority
19748 * @param number|null $module_id
19749 *
19750 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19751 * @uses add_action()
19752 *
19753 */
19754 static function add_ajax_action_static(
19755 $tag,
19756 $function_to_add,
19757 $priority = WP_FS__DEFAULT_PRIORITY,
19758 $module_id = null
19759 ) {
19760 self::$_static_logger->entrance( $tag );
19761
19762 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
19763 return false;
19764 }
19765
19766 add_action(
19767 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
19768 $function_to_add,
19769 $priority,
19770 0
19771 );
19772
19773 self::$_static_logger->info( "$tag AJAX callback action added." );
19774
19775 return true;
19776 }
19777
19778 /**
19779 * Send a JSON response back to an Ajax request.
19780 *
19781 * @author Vova Feldman (@svovaf)
19782 * @since 1.2.1.5
19783 *
19784 * @param mixed $response
19785 */
19786 static function shoot_ajax_response( $response ) {
19787 wp_send_json( $response );
19788 }
19789
19790 /**
19791 * Send a JSON response back to an Ajax request, indicating success.
19792 *
19793 * @author Vova Feldman (@svovaf)
19794 * @since 1.2.1.5
19795 *
19796 * @param mixed $data Data to encode as JSON, then print and exit.
19797 */
19798 static function shoot_ajax_success( $data = null ) {
19799 wp_send_json_success( $data );
19800 }
19801
19802 /**
19803 * Send a JSON response back to an Ajax request, indicating failure.
19804 *
19805 * @author Vova Feldman (@svovaf)
19806 * @since 1.2.1.5
19807 *
19808 * @param mixed $error Optional error message.
19809 */
19810 static function shoot_ajax_failure( $error = '' ) {
19811 $result = array( 'success' => false );
19812 if ( ! empty( $error ) ) {
19813 $result['error'] = $error;
19814 }
19815
19816 wp_send_json( $result );
19817 }
19818
19819 /**
19820 * Returns an AJAX URL with a special extra param to indicate whether the request was triggered from the network admin or blog admin.
19821 *
19822 * @author Vova Feldman (@svovaf)
19823 * @since 2.5.1
19824 *
19825 * @param string $wrap_with By default, returns the AJAX URL wrapped with single quotes.
19826 *
19827 * @return string
19828 */
19829 static function ajax_url( $wrap_with = "'") {
19830 if ( fs_is_network_admin() ) {
19831 $param_name = '_fs_network_admin';
19832 } else {
19833 $param_name = '_fs_blog_admin';
19834 }
19835
19836 $url = admin_url( 'admin-ajax.php', 'relative' );
19837 $url .= ( false === strpos( $url, '?' ) ) ? '?' : '&';
19838 $url .= "{$param_name}=true";
19839
19840 return "{$wrap_with}{$url}{$wrap_with}";
19841 }
19842
19843 /**
19844 * Apply filter, specific for the current context plugin.
19845 *
19846 * @author Vova Feldman (@svovaf)
19847 * @since 1.0.9
19848 *
19849 * @param string $tag The name of the filter hook.
19850 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
19851 *
19852 * @return mixed The filtered value after all hooked functions are applied to it.
19853 *
19854 * @uses apply_filters()
19855 */
19856 function apply_filters( $tag, $value ) {
19857 $args = func_get_args();
19858
19859 $this->_logger->entrance( $tag );
19860
19861 array_unshift( $args, $this->get_unique_affix() );
19862
19863 return call_user_func_array( 'fs_apply_filter', $args );
19864 }
19865
19866 /**
19867 * Add filter, specific for the current context plugin.
19868 *
19869 * @author Vova Feldman (@svovaf)
19870 * @since 1.0.9
19871 *
19872 * @param string $tag
19873 * @param callable $function_to_add
19874 * @param int $priority
19875 * @param int $accepted_args
19876 *
19877 * @uses add_filter()
19878 */
19879 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
19880 $this->_logger->entrance( $tag );
19881
19882 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19883 }
19884
19885 /**
19886 * Check if has filter.
19887 *
19888 * @author Vova Feldman (@svovaf)
19889 * @since 1.1.4
19890 *
19891 * @param string $tag
19892 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
19893 *
19894 * @return false|int
19895 *
19896 * @uses has_filter()
19897 */
19898 function has_filter( $tag, $function_to_check = false ) {
19899 $this->_logger->entrance( $tag );
19900
19901 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
19902 }
19903
19904 #endregion
19905
19906 /**
19907 * Override default i18n text phrases.
19908 *
19909 * @author Vova Feldman (@svovaf)
19910 * @since 1.1.6
19911 *
19912 * @param string[] string $key_value
19913 *
19914 * @uses fs_override_i18n()
19915 */
19916 function override_i18n( $key_value ) {
19917 fs_override_i18n( $key_value, $this->_slug );
19918 }
19919
19920 /* Account Page
19921 ------------------------------------------------------------------------------------------------------------------*/
19922 /**
19923 * Update site information.
19924 *
19925 * @author Vova Feldman (@svovaf)
19926 * @since 1.0.1
19927 *
19928 * @param bool $store Flush to Database if true.
19929 * @param null|int $network_level_or_blog_id Since 2.0.0
19930 * @param \FS_Site $site Since 2.0.0
19931 */
19932 private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null, $is_backup = false ) {
19933 $this->_logger->entrance();
19934
19935 if ( is_null( $site ) ) {
19936 $site = $this->_site;
19937 }
19938
19939 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
19940 $this->_logger->error( "Empty install ID, can't store site." );
19941
19942 return;
19943 }
19944
19945 $site_clone = clone $site;
19946
19947 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id, $is_backup );
19948
19949 if (
19950 ! $is_backup &&
19951 is_object( $this->_user ) && $this->_user->id != $site->user_id
19952 ) {
19953 $this->sync_user_by_current_install( $site->user_id );
19954
19955 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
19956
19957 if ( empty( $prev_stored_user_id ) &&
19958 is_object($this->_user) && $this->_user->id != $site->user_id
19959 ) {
19960 /**
19961 * Store the current user ID as the previous user ID so that the previous user can be used
19962 * as the install's owner while the new owner's details are not yet available.
19963 *
19964 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
19965 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
19966 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
19967 * into `replica`.
19968 *
19969 * @author Leo Fajardo (@leorw)
19970 */
19971 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
19972 }
19973 }
19974
19975 $sites[ $this->_slug ] = $site_clone;
19976
19977 $this->set_account_option(
19978 ( $is_backup ? 'prev_' : '' ) . 'sites',
19979 $sites,
19980 $store,
19981 $network_level_or_blog_id
19982 );
19983 }
19984
19985 /**
19986 * 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).
19987 *
19988 * @author Leo Fajardo (@leorw)
19989 * @since 2.5.0
19990 */
19991 private function back_up_site() {
19992 $this->_logger->entrance();
19993
19994 $site_clone = clone $this->_site;
19995
19996 $this->_store_site( true, null, $site_clone, true );
19997 }
19998
19999 /**
20000 * Update plugin's plans information.
20001 *
20002 * @author Vova Feldman (@svovaf)
20003 * @since 1.0.2
20004 *
20005 * @param bool $store Flush to Database if true.
20006 */
20007 private function _store_plans( $store = true ) {
20008 $this->_logger->entrance();
20009
20010 $plans = self::get_all_plans( $this->_module_type );
20011
20012 // Copy plans.
20013 $encrypted_plans = array();
20014 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
20015 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
20016 }
20017
20018 $plans[ $this->_slug ] = $encrypted_plans;
20019
20020 $this->set_account_option( 'plans', $plans, $store );
20021 }
20022
20023 /**
20024 * Update user's plugin licenses.
20025 *
20026 * @author Vova Feldman (@svovaf)
20027 * @since 1.0.5
20028 *
20029 * @param bool $store
20030 * @param number|bool $module_id
20031 * @param FS_Plugin_License[] $licenses
20032 */
20033 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
20034 $this->_logger->entrance();
20035
20036 $all_licenses = self::get_all_licenses();
20037
20038 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
20039 $module_id = $this->_module_id;
20040
20041 $user_licenses = is_array( $this->_licenses ) ?
20042 $this->_licenses :
20043 array();
20044
20045 if ( empty( $user_licenses ) ) {
20046 // If the context user doesn't have any license, don't update the licenses collection.
20047 return;
20048 }
20049
20050 $new_user_licenses_map = array();
20051 foreach ( $user_licenses as $user_license ) {
20052 $new_user_licenses_map[ $user_license->id ] = $user_license;
20053 }
20054
20055 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
20056
20057 // Update user licenses.
20058 $licenses_to_update_count = count( $new_user_licenses_map );
20059 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
20060 if ( 0 === $licenses_to_update_count ) {
20061 break;
20062 }
20063
20064 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
20065 // Update license.
20066 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
20067 unset( $new_user_licenses_map[ $license->id ] );
20068
20069 $licenses_to_update_count --;
20070 }
20071 }
20072
20073 if ( ! empty( $new_user_licenses_map ) ) {
20074 // Add new licenses.
20075 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
20076 }
20077
20078 $licenses = $all_licenses[ $module_id ];
20079 }
20080
20081 if ( ! isset( $all_licenses[ $module_id ] ) ) {
20082 $all_licenses[ $module_id ] = array();
20083 }
20084
20085 $all_licenses[ $module_id ] = $licenses;
20086
20087 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
20088 }
20089
20090 /**
20091 * Update user information.
20092 *
20093 * @author Vova Feldman (@svovaf)
20094 * @since 1.0.1
20095 *
20096 * @param bool $store Flush to Database if true.
20097 */
20098 private function _store_user( $store = true ) {
20099 $this->_logger->entrance();
20100
20101 if ( empty( $this->_user->id ) ) {
20102 $this->_logger->error( "Empty user ID, can't store user." );
20103
20104 return;
20105 }
20106
20107 $users = self::get_all_users();
20108 $users[ $this->_user->id ] = $this->_user;
20109 self::$_accounts->set_option( 'users', $users, $store );
20110 }
20111
20112 /**
20113 * Update new updates information.
20114 *
20115 * @author Vova Feldman (@svovaf)
20116 * @since 1.0.4
20117 *
20118 * @param FS_Plugin_Tag|null $update
20119 * @param bool $store Flush to Database if true.
20120 * @param bool|number $plugin_id
20121 */
20122 private function _store_update( $update, $store = true, $plugin_id = false ) {
20123 $this->_logger->entrance();
20124
20125 if ( $update instanceof FS_Plugin_Tag ) {
20126 $update->updated = time();
20127 }
20128
20129 if ( ! is_numeric( $plugin_id ) ) {
20130 $plugin_id = $this->_plugin->id;
20131 }
20132
20133 $updates = self::get_all_updates();
20134 $updates[ $plugin_id ] = $update;
20135 self::$_accounts->set_option( 'updates', $updates, $store );
20136 }
20137
20138 /**
20139 * Update new updates information.
20140 *
20141 * @author Vova Feldman (@svovaf)
20142 * @since 1.0.6
20143 *
20144 * @param FS_Plugin[] $plugin_addons
20145 * @param bool $store Flush to Database if true.
20146 */
20147 private function _store_addons( $plugin_addons, $store = true ) {
20148 $this->_logger->entrance();
20149
20150 $addons = self::get_all_addons();
20151 $addons[ $this->_plugin->id ] = $plugin_addons;
20152 self::$_accounts->set_option( 'addons', $addons, $store );
20153 }
20154
20155 /**
20156 * Delete plugin's associated add-ons.
20157 *
20158 * @author Vova Feldman (@svovaf)
20159 * @since 1.0.8
20160 *
20161 * @param bool $store
20162 *
20163 * @return bool
20164 */
20165 private function _delete_account_addons( $store = true ) {
20166 $all_addons = self::get_all_account_addons();
20167
20168 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
20169 return false;
20170 }
20171
20172 unset( $all_addons[ $this->_plugin->id ] );
20173
20174 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20175
20176 return true;
20177 }
20178
20179 /**
20180 * Update account add-ons list.
20181 *
20182 * @author Vova Feldman (@svovaf)
20183 * @since 1.0.6
20184 *
20185 * @param FS_Plugin[] $addons
20186 * @param bool $store Flush to Database if true.
20187 */
20188 private function _store_account_addons( $addons, $store = true ) {
20189 $this->_logger->entrance();
20190
20191 $all_addons = self::get_all_account_addons();
20192 $all_addons[ $this->_plugin->id ] = $addons;
20193 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20194 }
20195
20196 /**
20197 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
20198 * the valid user licenses will be fetched again and the account add-ons may be updated.
20199 *
20200 * @author Leo Fajardo (@leorw)
20201 * @since 2.2.4
20202 */
20203 private function purge_valid_user_licenses_cache() {
20204 if ( ! $this->is_registered() ) {
20205 return;
20206 }
20207
20208 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
20209 }
20210
20211 /**
20212 * @author Leo Fajardo (@leorw)
20213 * @since 2.3.0
20214 *
20215 * @param array $all_licenses
20216 * @param number|null $site_license_id
20217 * @param bool $include_parent_licenses
20218 *
20219 * @return array
20220 */
20221 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
20222 $foreign_licenses = array(
20223 'ids' => array(),
20224 'license_keys' => array()
20225 );
20226
20227 $parent_license_ids_map = array();
20228
20229 foreach ( $all_licenses as $license ) {
20230 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
20231 continue;
20232 }
20233
20234 $foreign_licenses['ids'][] = $license->id;
20235 $foreign_licenses['license_keys'][] = $license->secret_key;
20236
20237 if (
20238 $include_parent_licenses &&
20239 is_object( $this->_license ) &&
20240 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
20241 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
20242 ) {
20243 /**
20244 * Include the parent license's info only if it has not been included before since child licenses
20245 * can have the same parent license.
20246 */
20247 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
20248 $foreign_licenses['license_keys'][] = $license->secret_key;
20249
20250 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
20251 }
20252 }
20253
20254 if ( empty( $foreign_licenses['ids'] ) ) {
20255 $foreign_licenses = array();
20256 }
20257
20258 return $foreign_licenses;
20259 }
20260
20261 /**
20262 * @author Leo Fajardo (@leorw)
20263 * @since 2.3.0
20264 *
20265 * @return string
20266 */
20267 private function get_valid_user_licenses_endpoint() {
20268 $user_licenses_endpoint = '/licenses.json?type=active' .
20269 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
20270
20271 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
20272
20273 if ( ! empty ( $foreign_licenses ) ) {
20274 $foreign_licenses = array(
20275 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20276 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20277 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20278 );
20279
20280 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20281 }
20282
20283 return $user_licenses_endpoint;
20284 }
20285
20286 /**
20287 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
20288 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
20289 * method filters out non–add-on product IDs and stores the add-on IDs.
20290 *
20291 * @author Leo Fajardo (@leorw)
20292 * @since 2.2.4
20293 *
20294 * @return stdClass[] array
20295 */
20296 private function fetch_valid_user_licenses() {
20297 $this->_logger->entrance();
20298
20299 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
20300
20301 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
20302 ! is_array( $result->licenses )
20303 ) {
20304 return array();
20305 }
20306
20307 return $result->licenses;
20308 }
20309
20310 /**
20311 * @author Leo Fajardo (@leorw)
20312 * @since 2.2.4
20313 *
20314 * @return number[] Account add-on IDs.
20315 */
20316 function get_updated_account_addons() {
20317 $addons = $this->get_addons();
20318 if ( empty( $addons ) ) {
20319 return array();
20320 }
20321
20322 $account_addons = $this->get_account_addons();
20323 if ( ! is_array( $account_addons ) ) {
20324 $account_addons = array();
20325 }
20326
20327 $user_licenses = $this->is_registered() ?
20328 $this->fetch_valid_user_licenses() :
20329 array();
20330
20331 if ( empty( $user_licenses ) ) {
20332 return $account_addons;
20333 }
20334
20335 $addon_ids = array();
20336 foreach ( $addons as $addon ) {
20337 $addon_ids[] = $addon->id;
20338 }
20339
20340 $license_product_ids = array();
20341
20342 foreach ( $user_licenses as $license ) {
20343 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
20344 $license_product_ids = array_merge( $license_product_ids, $license->products );
20345 } else {
20346 $license_product_ids[] = $license->plugin_id;
20347 }
20348 }
20349
20350 // Filter out non–add-on IDs.
20351 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
20352 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
20353 $this->_store_account_addons( array_unique( $new_account_addons ) );
20354 }
20355
20356 return $new_account_addons;
20357 }
20358
20359 /**
20360 * Store account params in the Database.
20361 *
20362 * @author Vova Feldman (@svovaf)
20363 * @since 1.0.1
20364 *
20365 * @param null|int $blog_id Since 2.0.0
20366 */
20367 private function _store_account( $blog_id = null ) {
20368 $this->_logger->entrance();
20369
20370 $this->_store_site( false, $blog_id );
20371 $this->_store_user( false );
20372 $this->_store_plans( false );
20373 $this->_store_licenses( false );
20374
20375 self::$_accounts->store( $blog_id );
20376 }
20377
20378 /**
20379 * Sync user's information.
20380 *
20381 * @author Vova Feldman (@svovaf)
20382 * @since 1.0.3
20383 * @uses FS_Api
20384 */
20385 private function _handle_account_user_sync() {
20386 $this->_logger->entrance();
20387
20388 $api = $this->get_api_user_scope();
20389
20390 // Get user's information.
20391 $user = $api->get( '/', true );
20392
20393 if ( isset( $user->id ) ) {
20394 $this->_user->first = $user->first;
20395 $this->_user->last = $user->last;
20396 $this->_user->email = $user->email;
20397
20398 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
20399
20400 if ( $user->is_verified &&
20401 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
20402 ) {
20403 $this->_user->is_verified = true;
20404
20405 $this->do_action( 'account_email_verified', $user->email );
20406
20407 $this->_admin_notices->add(
20408 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
20409 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
20410 'success',
20411 // Make admin sticky if account menu item is invisible,
20412 // since the page will be auto redirected to the plugin's
20413 // main settings page, and the non-sticky message
20414 // will disappear.
20415 ! $is_menu_item_account_visible,
20416 'email_verified'
20417 );
20418 }
20419
20420 // Flush user details to DB.
20421 $this->_store_user();
20422
20423 $this->do_action( 'after_account_user_sync', $user );
20424
20425 /**
20426 * If account menu item is hidden, redirect to plugin's main settings page.
20427 *
20428 * @author Vova Feldman (@svovaf)
20429 * @since 1.1.6
20430 *
20431 * @link https://github.com/Freemius/wordpress-sdk/issues/6
20432 */
20433 if ( ! $is_menu_item_account_visible ) {
20434 fs_redirect( $this->_get_admin_page_url() );
20435 }
20436 }
20437 }
20438
20439 /**
20440 * @author Vova Feldman (@svovaf)
20441 * @since 1.0.9
20442 * @uses FS_Api
20443 *
20444 * @param number|bool $license_id
20445 *
20446 * @return FS_Subscription|object|bool
20447 */
20448 private function _fetch_site_license_subscription( $license_id = false ) {
20449 $this->_logger->entrance();
20450 $api = $this->get_api_site_scope();
20451
20452 if ( ! is_numeric( $license_id ) ) {
20453 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
20454 $this->_license->parent_license_id :
20455 $this->_license->id;
20456 }
20457
20458 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
20459
20460 return ! isset( $result->error ) ?
20461 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
20462 new FS_Subscription( $result->subscriptions[0] ) :
20463 false
20464 ) :
20465 $result;
20466 }
20467
20468 /**
20469 * @author Vova Feldman (@svovaf)
20470 * @since 1.0.4
20471 * @uses FS_Api
20472 *
20473 * @param number|bool $plan_id
20474 *
20475 * @return FS_Plugin_Plan|object
20476 */
20477 private function _fetch_site_plan( $plan_id = false ) {
20478 $this->_logger->entrance();
20479 $api = $this->get_api_site_scope();
20480
20481 if ( ! is_numeric( $plan_id ) ) {
20482 $plan_id = $this->_site->plan_id;
20483 }
20484
20485 $plan = $api->get( "/plans/{$plan_id}.json", true );
20486
20487 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
20488 }
20489
20490 /**
20491 * @author Vova Feldman (@svovaf)
20492 * @since 1.0.5
20493 * @uses FS_Api
20494 *
20495 * @return FS_Plugin_Plan[]|object
20496 */
20497 private function _fetch_plugin_plans() {
20498 $this->_logger->entrance();
20499 $api = $this->get_current_or_network_user_api_scope();
20500
20501 /**
20502 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
20503 */
20504 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
20505
20506 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
20507 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
20508 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
20509 }
20510
20511 $result = $result->plans;
20512 }
20513
20514 return $result;
20515 }
20516
20517 /**
20518 * @author Vova Feldman (@svovaf)
20519 * @since 2.0.0
20520 *
20521 * @param number $plan_id
20522 *
20523 * @return \FS_Plugin_Plan|object
20524 */
20525 private function fetch_plan_by_id( $plan_id ) {
20526 $this->_logger->entrance();
20527 $api = $this->get_current_or_network_user_api_scope();
20528
20529 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
20530
20531 return $this->is_api_result_entity( $result ) ?
20532 new FS_Plugin_Plan( $result ) :
20533 $result;
20534 }
20535
20536 /**
20537 * @author Vova Feldman (@svovaf)
20538 * @since 1.0.5
20539 * @uses FS_Api
20540 *
20541 * @param number|bool $plugin_id
20542 * @param number|bool $site_license_id
20543 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
20544 * @param number|null $blog_id
20545 *
20546 * @return FS_Plugin_License[]|object
20547 */
20548 private function _fetch_licenses(
20549 $plugin_id = false,
20550 $site_license_id = false,
20551 $foreign_licenses = array(),
20552 $blog_id = null
20553 ) {
20554 $this->_logger->entrance();
20555
20556 $api = $this->get_api_user_scope();
20557
20558 if ( ! is_numeric( $plugin_id ) ) {
20559 $plugin_id = $this->_plugin->id;
20560 }
20561
20562 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
20563 if ( ! empty ( $foreign_licenses ) ) {
20564 $foreign_licenses = array(
20565 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20566 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20567 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20568 );
20569
20570 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20571 }
20572
20573 $result = $api->get( $user_licenses_endpoint, true );
20574
20575 $is_site_license_synced = false;
20576
20577 $api_errors = array();
20578
20579 if ( $this->is_api_result_object( $result, 'licenses' ) &&
20580 is_array( $result->licenses )
20581 ) {
20582 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
20583 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
20584
20585 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
20586 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
20587 }
20588 }
20589
20590 $result = $result->licenses;
20591 } else {
20592 $api_errors[] = $result;
20593 $result = array();
20594 }
20595
20596 if ( ! $is_site_license_synced ) {
20597 if ( ! is_null( $blog_id ) ) {
20598 /**
20599 * If blog ID is not null, the request is for syncing of the license of a single site via the
20600 * network-level "Account" page.
20601 *
20602 * @author Leo Fajardo (@leorw)
20603 */
20604 $this->switch_to_blog( $blog_id );
20605 }
20606
20607 $api = $this->get_api_site_scope();
20608
20609 if ( is_numeric( $site_license_id ) ) {
20610 // Try to retrieve a foreign license that is linked to the install.
20611 $api_result = $api->call( '/licenses.json?is_enriched=true' );
20612
20613 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
20614 is_array( $api_result->licenses )
20615 ) {
20616 $licenses = $api_result->licenses;
20617
20618 if ( ! empty( $licenses ) ) {
20619 $result[] = new FS_Plugin_License( $licenses[0] );
20620 }
20621 } else {
20622 $api_errors[] = $api_result;
20623 }
20624 } else if (
20625 is_object( $this->_license ) &&
20626 /**
20627 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
20628 * the FS instance that does the syncing is the parent FS instance.
20629 *
20630 * @author Leo Fajardo (@leorw)
20631 * @since 2.3.0
20632 */
20633 $this->_license->plugin_id == $plugin_id
20634 ) {
20635 $is_license_in_result = false;
20636 if ( ! empty( $result ) ) {
20637 foreach ( $result as $license ) {
20638 if ( $license->id == $this->_license->id ) {
20639 $is_license_in_result = true;
20640 break;
20641 }
20642 }
20643 }
20644
20645 if ( ! $is_license_in_result ) {
20646 // Fetch foreign license by ID and license key.
20647 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
20648 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
20649
20650 if ( $this->is_api_result_entity( $license ) ) {
20651 $result[] = new FS_Plugin_License( $license );
20652 } else {
20653 $api_errors[] = $license;
20654 }
20655 }
20656 }
20657
20658 if ( ! is_null( $blog_id ) ) {
20659 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20660 }
20661 }
20662
20663 if ( is_array( $result ) && 0 < count( $result ) ) {
20664 // If found at least one license, return license collection even if there are errors.
20665 return $result;
20666 }
20667
20668 if ( ! empty( $api_errors ) ) {
20669 // If found any errors and no licenses, return first error.
20670 return $api_errors[0];
20671 }
20672
20673 // Fallback to empty licenses list.
20674 return $result;
20675 }
20676
20677 /**
20678 * @author Vova Feldman (@svovaf)
20679 * @since 2.0.0
20680 *
20681 * @param number $license_id
20682 * @param string $license_key
20683 *
20684 * @return \FS_Plugin_License|object
20685 */
20686 private function fetch_license_by_key( $license_id, $license_key ) {
20687 $this->_logger->entrance();
20688
20689 $api = $this->get_current_or_network_user_api_scope();
20690
20691 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
20692
20693 return $this->is_api_result_entity( $result ) ?
20694 new FS_Plugin_License( $result ) :
20695 $result;
20696 }
20697
20698 /**
20699 * @author Vova Feldman (@svovaf)
20700 * @since 1.2.0
20701 * @uses FS_Api
20702 *
20703 * @param number|bool $plugin_id
20704 * @param bool $flush
20705 *
20706 * @return FS_Payment[]|object
20707 */
20708 function _fetch_payments( $plugin_id = false, $flush = false ) {
20709 $this->_logger->entrance();
20710
20711 $api = $this->get_api_user_scope();
20712
20713 if ( ! is_numeric( $plugin_id ) ) {
20714 $plugin_id = $this->_plugin->id;
20715 }
20716
20717 $include_bundles = (
20718 is_object( $this->_plugin ) &&
20719 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
20720 );
20721
20722 $result = $api->get(
20723 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
20724 $flush
20725 );
20726
20727 if ( ! isset( $result->error ) ) {
20728 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
20729 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
20730 }
20731 $result = $result->payments;
20732 }
20733
20734 return $result;
20735 }
20736
20737 /**
20738 * @author Vova Feldman (@svovaf)
20739 * @since 1.2.1.5
20740 * @uses FS_Api
20741 *
20742 * @param bool $flush
20743 *
20744 * @return \FS_Billing|mixed
20745 */
20746 function _fetch_billing( $flush = false ) {
20747 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
20748
20749 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
20750
20751 if ( $this->is_api_result_entity( $billing ) ) {
20752 $billing = new FS_Billing( $billing );
20753 }
20754
20755 return $billing;
20756 }
20757
20758 /**
20759 * @author Vova Feldman (@svovaf)
20760 * @since 1.0.5
20761 *
20762 * @param FS_Plugin_License[] $licenses
20763 * @param number $module_id
20764 */
20765 private function _update_licenses( $licenses, $module_id ) {
20766 $this->_logger->entrance();
20767
20768 if ( is_array( $licenses ) ) {
20769 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
20770 $licenses[ $i ]->updated = time();
20771 }
20772 }
20773
20774 $this->_store_licenses( true, $module_id, $licenses );
20775 }
20776
20777 /**
20778 * @author Vova Feldman (@svovaf)
20779 * @since 1.0.4
20780 *
20781 * @param bool|number $plugin_id
20782 * @param bool $flush Since 1.1.7.3
20783 * @param int $expiration Since 1.2.2.7
20784 * @param bool|string $newer_than Since 2.2.1
20785 *
20786 * @return object|false New plugin tag info if exist.
20787 */
20788 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
20789 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
20790
20791 if ( ! is_object( $latest_tag ) ) {
20792 return false;
20793 }
20794
20795 $plugin_version = $this->get_plugin_version();
20796
20797 // Check if version is actually newer.
20798 $has_new_version =
20799 // If it's an non-installed add-on then always return latest.
20800 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
20801 // Compare versions.
20802 version_compare( $plugin_version, $latest_tag->version, '<' );
20803
20804 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
20805
20806 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
20807
20808 $this->_storage->beta_data = array(
20809 'is_beta' => $is_latest_version_beta,
20810 'version' => $latest_tag->version
20811 );
20812
20813 return $has_new_version ? $latest_tag : false;
20814 }
20815
20816 /**
20817 * @author Vova Feldman (@svovaf)
20818 * @since 1.0.5
20819 *
20820 * @param bool|number $plugin_id
20821 * @param bool $flush Since 1.1.7.3
20822 * @param int $expiration Since 1.2.2.7
20823 * @param bool|string $newer_than Since 2.2.1
20824 *
20825 * @return bool|FS_Plugin_Tag
20826 */
20827 function get_update( $plugin_id = false, $flush = true, $expiration = FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION, $newer_than = false ) {
20828 $this->_logger->entrance();
20829
20830 if ( ! is_numeric( $plugin_id ) ) {
20831 $plugin_id = $this->_plugin->id;
20832 }
20833
20834 $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than );
20835 $updates = $this->get_all_updates();
20836
20837 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
20838 }
20839
20840 /**
20841 * Check if site assigned with active license.
20842 *
20843 * @author Vova Feldman (@svovaf)
20844 * @since 1.0.6
20845 *
20846 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
20847 */
20848 function has_active_license() {
20849 return (
20850 is_object( $this->_license ) &&
20851 is_numeric( $this->_license->id ) &&
20852 ! $this->_license->is_expired()
20853 );
20854 }
20855
20856 /**
20857 * Check if site assigned with active & valid (not expired) license.
20858 *
20859 * @author Vova Feldman (@svovaf)
20860 * @since 1.2.1
20861 *
20862 * @param bool $check_expiration
20863 */
20864 function has_active_valid_license( $check_expiration = true ) {
20865 return self::is_active_valid_license( $this->_license, $check_expiration );
20866 }
20867
20868 /**
20869 * @author Leo Fajardo (@leorw)
20870 * @since 2.3.1
20871 */
20872 function is_data_debug_mode() {
20873 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
20874 return false;
20875 }
20876
20877 $fs = $this->is_addon() ?
20878 $this->get_parent_instance() :
20879 $this;
20880
20881 if ( $fs->is_network_active() && fs_is_network_admin() ) {
20882 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
20883 } else {
20884 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
20885 }
20886
20887 return ( 'true' === $is_developer_license_debug_mode );
20888 }
20889
20890 /**
20891 * @author Leo Fajardo (@leorw)
20892 * @since 2.3.1
20893 */
20894 function _set_data_debug_mode() {
20895 if ( ! $this->is_whitelabeled( true ) ) {
20896 return;
20897 }
20898
20899 $license_or_user_key = fs_request_get_raw( 'license_or_user_key' );
20900
20901 $transient_value = ( ! empty( $license_or_user_key ) ) ?
20902 'true' :
20903 'false';
20904
20905 if ( 'true' === $transient_value ) {
20906 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
20907 'last_license_key' :
20908 'last_license_user_key'
20909 );
20910
20911 if ( md5( $license_or_user_key ) !== $stored_key ) {
20912 $this->shoot_ajax_failure( sprintf(
20913 '%s... %s',
20914 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
20915 $this->get_text_inline(
20916 'seems like the key you entered doesn\'t match our records.',
20917 'developer-or-license-not-found'
20918 )
20919 ) );
20920 }
20921 }
20922
20923 if ( $this->is_network_active() && fs_is_network_admin() ) {
20924 set_site_transient(
20925 "fs_{$this->get_id()}_data_debug_mode",
20926 $transient_value,
20927 WP_FS__TIME_24_HOURS_IN_SEC / 24
20928 );
20929 } else {
20930 set_transient(
20931 "fs_{$this->get_id()}_data_debug_mode",
20932 $transient_value,
20933 WP_FS__TIME_24_HOURS_IN_SEC / 24
20934 );
20935 }
20936
20937 if ( 'true' === $transient_value ) {
20938 $this->_admin_notices->add_sticky(
20939 $this->get_text_inline(
20940 '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.',
20941 'data_debug_mode_enabled'
20942 ),
20943 'data_debug_mode_enabled'
20944 );
20945 }
20946
20947 $this->shoot_ajax_success();
20948 }
20949
20950 /**
20951 * Check if a given license is active & valid (not expired).
20952 *
20953 * @author Vova Feldman (@svovaf)
20954 * @since 2.1.3
20955 *
20956 * @param FS_Plugin_License $license
20957 * @param bool $check_expiration
20958 *
20959 * @return bool
20960 */
20961 private static function is_active_valid_license( $license, $check_expiration = true ) {
20962 return (
20963 is_object( $license ) &&
20964 FS_Plugin_License::is_valid_id( $license->id ) &&
20965 $license->is_active() &&
20966 ( ! $check_expiration || $license->is_valid() )
20967 );
20968 }
20969
20970 /**
20971 * Checks if there's any site that is associated with an active & valid license.
20972 * This logic is used to determine if the admin can download the premium code base from a network level admin.
20973 *
20974 * @author Vova Feldman (@svovaf)
20975 * @since 2.1.3
20976 *
20977 * @return bool
20978 */
20979 function has_any_active_valid_license() {
20980 if ( ! fs_is_network_admin() ) {
20981 return $this->has_active_valid_license();
20982 }
20983
20984 $installs = $this->get_blog_install_map();
20985 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
20986
20987 foreach ( $installs as $blog_id => $install ) {
20988 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20989 continue;
20990 }
20991
20992 foreach ( $all_plugin_licenses as $license ) {
20993 if ( $license->id == $install->license_id ) {
20994 if ( self::is_active_valid_license( $license ) ) {
20995 return true;
20996 }
20997 }
20998 }
20999 }
21000
21001 return false;
21002 }
21003
21004 /**
21005 * Check if site assigned with license with enabled features.
21006 *
21007 * @author Vova Feldman (@svovaf)
21008 * @since 1.0.6
21009 *
21010 * @return bool
21011 */
21012 function has_features_enabled_license() {
21013 return (
21014 is_object( $this->_license ) &&
21015 is_numeric( $this->_license->id ) &&
21016 $this->_license->is_features_enabled()
21017 );
21018 }
21019
21020 /**
21021 * Checks if the product is activated with a bundle license.
21022 *
21023 * @author Leo Fajardo (@leorw)
21024 * @since 2.4.0
21025 *
21026 * @return bool
21027 */
21028 function is_activated_with_bundle_license() {
21029 if ( ! $this->has_features_enabled_license() ) {
21030 return false;
21031 }
21032
21033 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
21034 }
21035
21036 /**
21037 * Check if user is a trial or have feature enabled license.
21038 *
21039 * @author Vova Feldman (@svovaf)
21040 * @since 1.1.7
21041 *
21042 * @return bool
21043 */
21044 function can_use_premium_code() {
21045 return $this->is_trial() || $this->has_features_enabled_license();
21046 }
21047
21048 /**
21049 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
21050 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
21051 * the context user is authenticated.
21052 *
21053 * @author Leo Fajardo (@leorw)
21054 * @since 1.2.2
21055 *
21056 * @return bool
21057 */
21058 function is_user_admin() {
21059 /**
21060 * Require a super-admin when network activated, running from the network level OR if
21061 * running from the site level but not delegated the opt-in.
21062 *
21063 * @author Vova Feldman (@svovaf)
21064 * @since 2.0.0
21065 */
21066 if ( $this->_is_network_active &&
21067 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
21068 ) {
21069 return is_super_admin();
21070 }
21071
21072 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
21073 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
21074 }
21075
21076 /**
21077 * Sync site's plan.
21078 *
21079 * @author Vova Feldman (@svovaf)
21080 * @since 1.0.3
21081 *
21082 * @uses FS_Api
21083 *
21084 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
21085 * the admin.
21086 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
21087 * network-level "Account" page.
21088 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
21089 * `_sync_plugin_license` method in order to switch to the previous blog when sending
21090 * updates for a single site in case `execute_cron` has switched to a different blog.
21091 */
21092 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
21093 $this->_logger->entrance();
21094
21095 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
21096
21097 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
21098
21099 if ( $is_addon_sync ) {
21100 $this->_sync_addon_license( $plugin_id, $background );
21101 } else {
21102 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
21103 }
21104
21105 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
21106 }
21107
21108 /**
21109 * Sync plugin's add-on license.
21110 *
21111 * @author Vova Feldman (@svovaf)
21112 * @since 1.0.6
21113 * @uses FS_Api
21114 *
21115 * @param number $addon_id
21116 * @param bool $background
21117 */
21118 private function _sync_addon_license( $addon_id, $background ) {
21119 $this->_logger->entrance();
21120
21121 if ( $this->is_addon_activated( $addon_id ) ) {
21122 // If already installed, use add-on sync.
21123 $fs_addon = self::get_instance_by_id( $addon_id );
21124
21125 if (
21126 // Add-on is network activated and network integrated.
21127 $fs_addon->is_network_active() ||
21128 // Background sync cron.
21129 self::is_cron() ||
21130 // Add-on is not network activated or not network integrated.
21131 ! fs_is_network_admin()
21132 ) {
21133 $fs_addon->_sync_license( $background );
21134
21135 return;
21136 }
21137 }
21138
21139 // Validate add-on exists.
21140 $addon = $this->get_addon( $addon_id );
21141
21142 if ( ! is_object( $addon ) ) {
21143 return;
21144 }
21145
21146 // Add add-on into account add-ons.
21147 $account_addons = $this->get_account_addons();
21148 if ( ! is_array( $account_addons ) ) {
21149 $account_addons = array();
21150 }
21151 $account_addons[] = $addon->id;
21152 $account_addons = array_unique( $account_addons );
21153 $this->_store_account_addons( $account_addons );
21154
21155 // Load add-on licenses.
21156 $licenses = $this->_fetch_licenses( $addon->id );
21157
21158 // Sync add-on licenses.
21159 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
21160 $this->_update_licenses( $licenses, $addon->id );
21161
21162 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
21163 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
21164
21165 if ( ! isset( $plans_result->error ) ) {
21166 $plans = array();
21167 foreach ( $plans_result->plans as $plan ) {
21168 $plans[] = new FS_Plugin_Plan( $plan );
21169 }
21170
21171 $this->_admin_notices->add_sticky(
21172 sprintf(
21173 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
21174 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
21175 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
21176 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
21177 $addon->title
21178 ) . ' ' . $this->get_latest_download_link(
21179 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
21180 $addon_id
21181 ),
21182 'addon_plan_upgraded_' . $addon->slug,
21183 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
21184 );
21185 }
21186 }
21187 }
21188 }
21189
21190 /**
21191 * Sync site's plugin plan.
21192 *
21193 * @author Vova Feldman (@svovaf)
21194 * @since 1.0.6
21195 * @uses FS_Api
21196 *
21197 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
21198 * @param bool $send_installs_update Since 2.0.0
21199 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
21200 * syncing its license from the network-level "Account" page (e.g.: after
21201 * activating a license only for the single install).
21202 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
21203 * can be used here to switch to the previous blog in case `execute_cron`
21204 * has switched to a different blog.
21205 */
21206 private function _sync_plugin_license(
21207 $background = false,
21208 $send_installs_update = true,
21209 $is_context_single_site = false,
21210 $current_blog_id = null
21211 ) {
21212 $this->_logger->entrance();
21213
21214 $plan_change = 'none';
21215
21216 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
21217
21218 if ( ! $send_installs_update ) {
21219 $site = $this->_site;
21220 } else {
21221 /**
21222 * Sync site info.
21223 *
21224 * @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.
21225 */
21226 if ( $is_site_level_sync ) {
21227 /**
21228 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
21229 *
21230 * @author Leo Fajardo (@leorw)
21231 * @since 2.2.3
21232 */
21233 if ( is_numeric( $current_blog_id ) ) {
21234 $this->switch_to_blog( $current_blog_id );
21235 }
21236
21237 $result = $this->send_install_update( array(), true, true );
21238 $is_valid = $this->is_api_result_entity( $result );
21239 } else {
21240 $result = $this->send_installs_update( array(), true, true );
21241 $is_valid = $this->is_api_result_object( $result, 'installs' );
21242 }
21243
21244 if ( ! $is_valid ) {
21245 if ( $is_context_single_site ) {
21246 // Switch back to the main blog so that the following logic will have the right entities.
21247 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21248 }
21249
21250 // Show API message only if not background sync or if paying customer.
21251 if ( ! $background || $this->is_paying() ) {
21252 // Try to ping API to see if not blocked.
21253 if ( FS_Api::is_blocked( $result ) ) {
21254 /**
21255 * @author Vova Feldman (@svovaf)
21256 * @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.
21257 */
21258 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
21259 // Add notice immediately if not a background sync.
21260 $add_notice = ( ! $background );
21261
21262 if ( ! $add_notice ) {
21263 $counter = (int) get_transient( '_fs_api_connection_retry_counter' );
21264
21265 // We only want to add the notice after 3 consecutive failures.
21266 $add_notice = ( 3 <= $counter );
21267
21268 if ( ! $add_notice ) {
21269 /**
21270 * 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.
21271 *
21272 * 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.
21273 */
21274 set_transient( '_fs_api_connection_retry_counter', $counter + 1, WP_FS__TIME_WEEK_IN_SEC );
21275 }
21276 }
21277
21278 // Add notice instantly for not-background sync and only after 3 failed attempts for background sync.
21279 if ( $add_notice ) {
21280 self::$_global_admin_notices->add(
21281 $this->generate_api_blocked_notice_message_from_result( $result ),
21282 '',
21283 'error',
21284 $background,
21285 'api_blocked'
21286 );
21287
21288 add_action( 'admin_footer', array( 'Freemius', '_add_api_connectivity_notice_handler_js' ) );
21289
21290 // Notice was just shown, reset connectivity counter.
21291 delete_transient( '_fs_api_connection_retry_counter' );
21292 }
21293 }
21294 } else if ( is_object( $result ) ) {
21295 // Authentication params are broken.
21296 $this->_admin_notices->add(
21297 $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 ),
21298 '',
21299 'error'
21300 );
21301 }
21302 }
21303
21304 // No reason to continue with license sync while there are API issues.
21305 return;
21306 }
21307
21308 // API is working now. Delete the transient and start afresh.
21309 delete_transient('_fs_api_connection_retry_counter');
21310
21311 if ( $is_site_level_sync ) {
21312 $site = new FS_Site( $result );
21313 } else {
21314 // Map site addresses to their blog IDs.
21315 $address_to_blog_map = $this->get_address_to_blog_map();
21316
21317 // Find the current context install.
21318 $site = null;
21319 foreach ( $result->installs as $install ) {
21320 if ( $install->id == $this->_site->id ) {
21321 $site = new FS_Site( $install );
21322 } else {
21323 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
21324 $blog_id = $address_to_blog_map[ $address ];
21325
21326 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
21327 }
21328 }
21329 }
21330
21331 // Sync plans.
21332 $this->_sync_plans();
21333 }
21334
21335 // Remove sticky API connectivity message.
21336 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
21337
21338 if ( ! $this->has_paid_plan() ) {
21339 $this->_site = $site;
21340 $this->_store_site(
21341 true,
21342 $is_site_level_sync ?
21343 null :
21344 $this->get_network_install_blog_id()
21345 );
21346 } else {
21347 $context_blog_id = 0;
21348
21349 if ( $is_context_single_site ) {
21350 $context_blog_id = get_current_blog_id();
21351
21352 // Switch back to the main blog in order to properly sync the license.
21353 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21354 }
21355
21356 /**
21357 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
21358 * associated with that ID is not included in the user's licenses collection.
21359 * Save previous value to manage remote license renewals.
21360 */
21361 $was_license_expired_before_sync = is_object( $this->_license ) && $this->_license->is_expired();
21362 $this->_sync_licenses(
21363 $site->license_id,
21364 ( $is_context_single_site ?
21365 $context_blog_id :
21366 null
21367 )
21368 );
21369
21370 if ( $is_context_single_site ) {
21371 $this->switch_to_blog( $context_blog_id );
21372 }
21373
21374 // Check if plan / license changed.
21375 if ( $site->plan_id != $this->_site->plan_id ||
21376 // Check if trial started.
21377 $site->trial_plan_id != $this->_site->trial_plan_id ||
21378 $site->trial_ends != $this->_site->trial_ends ||
21379 // Check if license changed.
21380 $site->license_id != $this->_site->license_id
21381 ) {
21382 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
21383 // New trial started.
21384 $this->_site = $site;
21385 $plan_change = 'trial_started';
21386
21387 // For trial with subscription use-case.
21388 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
21389
21390 if ( is_object( $new_license ) && $new_license->is_valid() ) {
21391 $this->_site = $site;
21392 $this->_update_site_license( $new_license );
21393 $this->_store_licenses();
21394
21395 $this->_sync_site_subscription( $this->_license );
21396 }
21397 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
21398 // Was in trial, but now trial expired and no license ID.
21399 // New trial started.
21400 $this->_site = $site;
21401 $plan_change = 'trial_expired';
21402 } else {
21403 $is_free = $this->is_free_plan();
21404
21405 // Make sure license exist and not expired.
21406 $new_license = is_null( $site->license_id ) ?
21407 null :
21408 $this->_get_license_by_id( $site->license_id );
21409
21410 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
21411 // License cancelled.
21412 $this->_site = $site;
21413 $this->_update_site_license( $new_license );
21414 $this->_store_licenses();
21415
21416 $plan_change = 'cancelled';
21417 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
21418 // The license is expired, so ignore upgrade method.
21419 $this->_site = $site;
21420 } else {
21421 // License changed.
21422 $this->_site = $site;
21423
21424 /**
21425 * IMPORTANT:
21426 * 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.
21427 *
21428 * @author Vova Feldman (@svovaf)
21429 * @since 2.0.0
21430 */
21431 $this->_update_site_license( $new_license );
21432
21433 if ( ! $is_context_single_site &&
21434 fs_is_network_admin() &&
21435 $this->_is_network_active &&
21436 $new_license->quota > 1 &&
21437 get_blog_count() > 1
21438 ) {
21439 // See if license can activated on all sites.
21440 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
21441 if ( ! fs_request_get_bool( 'auto_install' ) ) {
21442 // Open the license activation dialog box on the account page.
21443 add_action( 'admin_footer', array(
21444 &$this,
21445 '_open_license_activation_dialog_box'
21446 ) );
21447 }
21448 }
21449 }
21450
21451 $this->_store_licenses();
21452
21453 $plan_change = $is_free ?
21454 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
21455 ( is_object( $new_license ) ?
21456 'changed' :
21457 'downgraded' );
21458 }
21459 }
21460
21461 // Store updated site info.
21462 $this->_store_site(
21463 true,
21464 $is_site_level_sync ?
21465 null :
21466 $this->get_network_install_blog_id()
21467 );
21468 } else {
21469 if ( ! is_object( $this->_license ) ) {
21470 $this->maybe_update_whitelabel_flag(
21471 FS_Plugin_License::is_valid_id( $site->license_id ) ?
21472 $this->get_license_by_id( $site->license_id ) :
21473 null
21474 );
21475 } else {
21476 $this->maybe_update_whitelabel_flag( $this->_license );
21477
21478 if ( $this->_license->is_expired() ) {
21479 if ( ! $this->has_features_enabled_license() ) {
21480 $this->_deactivate_license();
21481 $plan_change = 'downgraded';
21482 } else {
21483 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
21484
21485 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
21486 /**
21487 * Show the expired license notice every 14 days.
21488 *
21489 * @author Leo Fajardo (@leorw)
21490 * @since 2.3.1
21491 */
21492 $plan_change = 'expired';
21493 }
21494 }
21495 } else if ( $was_license_expired_before_sync ) {
21496 /**
21497 * If license was expired but it is not anymore.
21498 *
21499 *
21500 * @author Daniele Alessandra (@danielealessandra)
21501 */
21502 $plan_change = 'extended';
21503 }
21504 }
21505
21506 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
21507 $this->_sync_site_subscription( $this->_license );
21508 }
21509 }
21510
21511 if ( ! $this->is_addon() &&
21512 $this->_site->is_beta() !== $site->is_beta()
21513 ) {
21514 // Beta flag updated.
21515 $this->_site = $site;
21516
21517 $this->_store_site(
21518 true,
21519 $is_site_level_sync ?
21520 null :
21521 $this->get_network_install_blog_id()
21522 );
21523 }
21524
21525 if ( $this->is_addon() || $this->has_addons() ) {
21526 /**
21527 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21528 * an updated valid user licenses collection will be fetched from the server which is used to also
21529 * update the account add-ons (add-ons the user has licenses for).
21530 *
21531 * @author Leo Fajardo (@leorw)
21532 * @since 2.2.4
21533 */
21534 $this->purge_valid_user_licenses_cache();
21535 }
21536 }
21537
21538 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21539
21540 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
21541 switch ( $plan_change ) {
21542 case 'none':
21543 if ( ! $background && is_admin() ) {
21544 $plan = $this->is_trial() ?
21545 $this->get_trial_plan() :
21546 $this->get_plan();
21547
21548 if ( $plan->is_free() ) {
21549 $this->_admin_notices->add(
21550 sprintf(
21551 $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' ),
21552 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
21553 ) . ' ' . sprintf(
21554 '<a href="%s">%s</a>',
21555 $this->contact_url(
21556 'bug',
21557 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' ),
21558 strtoupper( $plan->name )
21559 )
21560 ),
21561 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21562 ),
21563 $hmm_text
21564 );
21565 }
21566 }
21567 break;
21568 case 'upgraded':
21569 case 'activated':
21570 $this->add_after_plan_activation_or_upgrade_instructions_notice( 'upgraded' === $plan_change );
21571
21572 $this->_admin_notices->remove_sticky( array(
21573 'trial_started',
21574 'trial_promotion',
21575 'trial_expired',
21576 'activation_complete',
21577 'license_expired',
21578 ) );
21579 break;
21580 case 'extended':
21581 $this->_admin_notices->remove_sticky( array(
21582 'trial_expired',
21583 'license_expired',
21584 ) );
21585 break;
21586 case 'changed':
21587 $this->_admin_notices->add_sticky(
21588 sprintf(
21589 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
21590 $this->get_plan_title()
21591 ),
21592 'plan_changed'
21593 );
21594
21595 $this->_admin_notices->remove_sticky( array(
21596 'trial_started',
21597 'trial_promotion',
21598 'trial_expired',
21599 'activation_complete',
21600 ) );
21601 break;
21602 case 'downgraded':
21603 $this->_admin_notices->add_sticky(
21604 ($this->has_free_plan() ?
21605 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 ) :
21606 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21607 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) ) ),
21608 'license_expired',
21609 $hmm_text
21610 );
21611 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21612 break;
21613 case 'cancelled':
21614 $this->_admin_notices->add(
21615 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
21616 sprintf(
21617 '<a href="%s">%s</a>',
21618 $this->contact_url( 'bug' ),
21619 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21620 ),
21621 $hmm_text,
21622 'error'
21623 );
21624 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21625 break;
21626 case 'expired':
21627 $this->_admin_notices->add_sticky(
21628 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 ),
21629 'license_expired',
21630 $hmm_text
21631 );
21632
21633 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
21634
21635 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21636 break;
21637 case 'trial_started':
21638 $this->add_complete_upgrade_instructions_notice(
21639 sprintf(
21640 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
21641 '<i>' . $this->get_plugin_name() . '</i>'
21642 ),
21643 'trial_started',
21644 $this->get_trial_plan()->title
21645 );
21646
21647 $this->_admin_notices->remove_sticky( array(
21648 'trial_promotion',
21649 ) );
21650 break;
21651 case 'trial_expired':
21652 $this->_admin_notices->add_sticky(
21653 ($this->has_free_plan() ?
21654 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
21655 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21656 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))),
21657 'trial_expired',
21658 $hmm_text
21659 );
21660 $this->_admin_notices->remove_sticky( array(
21661 'trial_started',
21662 'trial_promotion',
21663 'plan_upgraded',
21664 ) );
21665 break;
21666 }
21667 }
21668
21669 if ( 'none' !== $plan_change ) {
21670 if (
21671 ! is_object( $this->_license ) ||
21672 ! $this->_license->is_whitelabeled
21673 ) {
21674 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
21675 }
21676
21677 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
21678 }
21679 }
21680
21681 /**
21682 * @author Leo Fajardo (@leorw)
21683 * @since 2.5.4
21684 *
21685 * @param mixed $result
21686 *
21687 * @return string
21688 */
21689 private function generate_api_blocked_notice_message_from_result( $result ) {
21690 $api_domains = $this->apply_filters( 'api_domains', array(
21691 'api.freemius.com',
21692 'wp.freemius.com',
21693 ) );
21694
21695 $api_domains_list_items = '';
21696
21697 foreach( $api_domains as $api_domain ) {
21698 $api_domains_list_items .= "<li>{$api_domain}</li>";
21699 }
21700
21701 $error_message = sprintf(
21702 $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' ),
21703 $this->get_plugin_name(),
21704 "<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>"
21705 );
21706
21707 $error_message =
21708 "<div>{$error_message}</div>" .
21709 '<div class="fs-api-request-error-details" style="display: none">' .
21710 '<strong>' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . '</strong><br>' .
21711 $result->error->message .
21712 '</div>';
21713
21714 return $error_message;
21715 }
21716
21717 /**
21718 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
21719 *
21720 * @author Vova Feldman (@svovaf)
21721 * @since 2.0.0
21722 */
21723 public function _open_license_activation_dialog_box() {
21724 $vars = array( 'license_id' => $this->_site->license_id );
21725 fs_require_once_template( 'js/open-license-activation.php', $vars );
21726 }
21727
21728 /**
21729 * @author Vova Feldman (@svovaf)
21730 * @since 1.0.5
21731 *
21732 * @param bool $background
21733 * @param FS_Plugin_License|null $premium_license
21734 */
21735 protected function _activate_license( $background = false, $premium_license = null ) {
21736 $this->_logger->entrance();
21737
21738 if ( is_null( $premium_license ) ) {
21739 $license_id = fs_request_get( 'license_id' );
21740
21741 if ( is_object( $this->_site ) &&
21742 FS_Plugin_License::is_valid_id( $license_id ) &&
21743 $license_id == $this->_site->license_id
21744 ) {
21745 // License is already activated.
21746 return;
21747 }
21748
21749 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
21750 $this->_get_license_by_id( $license_id ) :
21751 $this->_get_available_premium_license();
21752 }
21753
21754 if ( ! is_object( $premium_license ) ) {
21755 return;
21756 }
21757
21758 if ( ! is_object( $this->_site ) ) {
21759 // Not yet opted-in.
21760 $user = $this->get_current_or_network_user();
21761 if ( ! is_object( $user ) ) {
21762 $user = self::_get_user_by_id( $premium_license->user_id );
21763 }
21764
21765 if ( is_object( $user ) ) {
21766 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
21767 } else {
21768 $this->opt_in(
21769 false,
21770 false,
21771 false,
21772 $premium_license->secret_key
21773 );
21774
21775 return;
21776 }
21777 }
21778
21779
21780 /**
21781 * If the premium license is already associated with the install, just
21782 * update the license reference (activation is not required).
21783 *
21784 * @since 1.1.9
21785 */
21786 if ( $premium_license->id == $this->_site->license_id ) {
21787 // License is already activated.
21788 $this->_update_site_license( $premium_license );
21789 $this->_store_account();
21790
21791 return;
21792 }
21793
21794 if ( $this->_site->user_id != $premium_license->user_id ) {
21795 $api_request_params = array( 'license_key' => $premium_license->secret_key );
21796 } else {
21797 $api_request_params = array();
21798 }
21799
21800 $api = $this->get_api_site_scope();
21801 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
21802
21803 if ( ! $this->is_api_result_entity( $license ) ) {
21804 if ( ! $background ) {
21805 $this->_admin_notices->add( sprintf(
21806 '%s %s',
21807 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
21808 ( is_object( $license ) && isset( $license->error ) ?
21809 $license->error->message :
21810 sprintf( '%s<br><code>%s</code>',
21811 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
21812 var_export( $license, true )
21813 )
21814 )
21815 ),
21816 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
21817 'error'
21818 );
21819 }
21820
21821 return;
21822 }
21823
21824 $premium_license = new FS_Plugin_License( $license );
21825
21826 // Updated site plan.
21827 $site = $this->get_api_site_scope()->get( '/', true );
21828 if ( $this->is_api_result_entity( $site ) ) {
21829 $this->_site = new FS_Site( $site );
21830 }
21831 $this->_update_site_license( $premium_license );
21832
21833 $this->_store_account();
21834
21835 if ( $this->is_addon() || $this->has_addons() ) {
21836 /**
21837 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21838 * an updated valid user licenses collection will be fetched from the server which is used to also
21839 * update the account add-ons (add-ons the user has licenses for).
21840 *
21841 * @author Leo Fajardo (@leorw)
21842 * @since 2.2.4
21843 */
21844 $this->purge_valid_user_licenses_cache();
21845 }
21846
21847 if ( ! $background ) {
21848 $this->add_complete_upgrade_instructions_notice(
21849 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ),
21850 'license_activated'
21851 );
21852 }
21853
21854 $this->_admin_notices->remove_sticky( array(
21855 'trial_promotion',
21856 'license_expired',
21857 ) );
21858 }
21859
21860 /**
21861 * @author Vova Feldman (@svovaf)
21862 * @since 1.0.5
21863 *
21864 * @param bool $show_notice
21865 */
21866 protected function _deactivate_license( $show_notice = true ) {
21867 $this->_logger->entrance();
21868
21869 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21870
21871 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
21872 $this->_admin_notices->add(
21873 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() ),
21874 $hmm_text
21875 );
21876
21877 return;
21878 }
21879
21880 $api = $this->get_api_site_scope();
21881 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
21882
21883 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
21884 }
21885
21886 /**
21887 * @author Leo Fajardo (@leorw)
21888 * @since 2.2.1
21889 *
21890 * @param FS_Plugin_License $license
21891 * @param bool|string $hmm_text
21892 * @param bool $show_notice
21893 */
21894 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
21895 if ( isset( $license->error ) ) {
21896 $this->_admin_notices->add(
21897 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
21898 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
21899 $hmm_text,
21900 'error'
21901 );
21902
21903 return;
21904 }
21905
21906 // Update license cache.
21907 if ( is_array( $this->_licenses ) ) {
21908 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
21909 if ( $license->id == $this->_licenses[ $i ]->id ) {
21910 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
21911 }
21912 }
21913 }
21914
21915 // Update site plan to default.
21916 $this->_sync_plans();
21917 $this->_site->plan_id = $this->_plans[0]->id;
21918 // Unlink license from site.
21919 $this->_update_site_license( null );
21920
21921 $this->_store_account();
21922
21923 if ( $show_notice ) {
21924 $this->_admin_notices->add(
21925 sprintf( $this->is_only_premium() ?
21926 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
21927 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
21928 $this->get_plan_title()
21929 ),
21930 $this->get_text_inline( 'O.K', 'ok' )
21931 );
21932 }
21933
21934 $this->_admin_notices->remove_sticky( array(
21935 'plan_upgraded',
21936 'license_activated',
21937 ) );
21938 }
21939
21940 /**
21941 * Site plan downgrade.
21942 *
21943 * @author Vova Feldman (@svovaf)
21944 * @since 1.0.4
21945 *
21946 * @return object
21947 *
21948 * @uses FS_Api
21949 */
21950 private function _downgrade_site() {
21951 $this->_logger->entrance();
21952
21953 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
21954
21955 $api = $this->get_api_site_scope();
21956 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
21957
21958 $plan_downgraded = false;
21959 $plan = false;
21960 if ( $this->is_api_result_entity( $site ) ) {
21961 $prev_plan_id = $this->_site->plan_id;
21962
21963 // Update new site plan id.
21964 $this->_site->plan_id = $site->plan_id;
21965
21966 $plan = $this->get_plan();
21967 $subscription = $this->_sync_site_subscription( $this->_license );
21968
21969 // Plan downgraded if plan was changed or subscription was cancelled.
21970 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
21971 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
21972 } else {
21973 // handle different error cases.
21974 $this->handle_license_deactivation_result(
21975 $site,
21976 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
21977 );
21978 }
21979
21980 if ( ! $plan_downgraded ) {
21981 return (object) array(
21982 'error' => (object) array(
21983 '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' )
21984 )
21985 );
21986 }
21987
21988 // Remove previous sticky message about upgrade (if exist).
21989 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21990
21991 $this->_admin_notices->add(
21992 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
21993 $plan->title,
21994 human_time_diff( time(), strtotime( $this->_license->expiration ) )
21995 )
21996 );
21997
21998 // Store site updates.
21999 $this->_store_site();
22000
22001 if ( $deactivate_license &&
22002 ! FS_Plugin_License::is_valid_id( $site->license_id )
22003 ) {
22004 if ( $this->_site->is_localhost() ) {
22005 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
22006 } else {
22007 $this->_license->activated = max( 0, $this->_license->activated - 1 );
22008 }
22009
22010 // Handle successful license deactivation result.
22011 $this->handle_license_deactivation_result( $this->_license );
22012 }
22013
22014 return $site;
22015 }
22016
22017 /**
22018 * @author Vova Feldman (@svovaf)
22019 * @since 1.1.8.1
22020 *
22021 * @param bool|string $plan_name
22022 *
22023 * @return bool If trial was successfully started.
22024 */
22025 function start_trial( $plan_name = false ) {
22026 $this->_logger->entrance();
22027
22028 // Alias.
22029 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
22030
22031 if ( $this->is_trial() ) {
22032 // Already in trial mode.
22033 $this->_admin_notices->add(
22034 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
22035 $oops_text,
22036 'error'
22037 );
22038
22039 return false;
22040 }
22041
22042 if ( $this->_site->is_trial_utilized() ) {
22043 // Trial was already utilized.
22044 $this->_admin_notices->add(
22045 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
22046 $oops_text,
22047 'error'
22048 );
22049
22050 return false;
22051 }
22052
22053 if ( false !== $plan_name ) {
22054 $plan = $this->get_plan_by_name( $plan_name );
22055
22056 if ( false === $plan ) {
22057 // Plan doesn't exist.
22058 $this->_admin_notices->add(
22059 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
22060 $oops_text,
22061 'error'
22062 );
22063
22064 return false;
22065 }
22066
22067 if ( ! $plan->has_trial() ) {
22068 // Plan doesn't exist.
22069 $this->_admin_notices->add(
22070 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
22071 $oops_text,
22072 'error'
22073 );
22074
22075 return false;
22076 }
22077 } else {
22078 if ( ! $this->has_trial_plan() ) {
22079 // None of the plans have a trial.
22080 $this->_admin_notices->add(
22081 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
22082 $oops_text,
22083 'error'
22084 );
22085
22086 return false;
22087 }
22088
22089 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
22090
22091 $plan = $plans_with_trial[0];
22092 }
22093
22094 $api = $this->get_api_site_scope();
22095 $plan = $api->call( "plans/{$plan->id}/trials.json", 'post' );
22096
22097 if ( ! $this->is_api_result_entity( $plan ) ) {
22098 // Some API error while trying to start the trial.
22099 $this->_admin_notices->add(
22100 $this->get_api_error_message( $plan ),
22101 $oops_text,
22102 'error'
22103 );
22104
22105 return false;
22106 }
22107
22108 // Sync license.
22109 $this->_sync_license();
22110
22111 return $this->is_trial();
22112 }
22113
22114 /**
22115 * Cancel site trial.
22116 *
22117 * @author Vova Feldman (@svovaf)
22118 * @since 1.0.9
22119 *
22120 * @return object
22121 *
22122 * @uses FS_Api
22123 */
22124 private function _cancel_trial() {
22125 $this->_logger->entrance();
22126
22127 if ( ! $this->is_trial() ) {
22128 return (object) array(
22129 'error' => (object) array(
22130 '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' )
22131 )
22132 );
22133 }
22134
22135 $trial_plan = $this->get_trial_plan();
22136
22137 $api = $this->get_api_site_scope();
22138 $site = $api->call( 'trials.json', 'delete' );
22139
22140 $trial_cancelled = false;
22141
22142 if ( $this->is_api_result_entity( $site ) ) {
22143 $prev_trial_ends = $this->_site->trial_ends;
22144
22145 if ( $this->is_paid_trial() ) {
22146 $this->_license->expiration = $site->trial_ends;
22147 $this->_license->is_cancelled = true;
22148 $this->_update_site_license( $this->_license );
22149 $this->_store_licenses();
22150
22151 // Clear subscription reference.
22152 $this->_sync_site_subscription( null );
22153 }
22154
22155 // Update site info.
22156 $this->_site = new FS_Site( $site );
22157
22158 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
22159 } else {
22160 // @todo handle different error cases.
22161 }
22162
22163 if ( ! $trial_cancelled ) {
22164 return (object) array(
22165 'error' => (object) array(
22166 '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' )
22167 )
22168 );
22169 }
22170
22171 // Remove previous sticky messages about upgrade or trial (if exist).
22172 $this->_admin_notices->remove_sticky( array(
22173 'trial_started',
22174 'trial_promotion',
22175 'plan_upgraded',
22176 ) );
22177
22178 // Store site updates.
22179 $this->_store_site();
22180
22181 if ( ! $this->is_addon() ||
22182 ! $this->deactivate_premium_only_addon_without_license( true )
22183 ) {
22184 $this->_admin_notices->add(
22185 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
22186 );
22187 }
22188
22189 return $site;
22190 }
22191
22192 /**
22193 * @author Vova Feldman (@svovaf)
22194 * @since 1.0.6
22195 *
22196 * @param bool|number $plugin_id
22197 *
22198 * @return bool
22199 */
22200 private function _is_addon_id( $plugin_id ) {
22201 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
22202 }
22203
22204 /**
22205 * Check if user eligible to download premium version updates.
22206 *
22207 * @author Vova Feldman (@svovaf)
22208 * @since 1.0.6
22209 *
22210 * @return bool
22211 */
22212 private function _can_download_premium() {
22213 return $this->has_any_active_valid_license() ||
22214 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
22215 }
22216
22217 /**
22218 *
22219 * @author Vova Feldman (@svovaf)
22220 * @since 1.0.6
22221 *
22222 * @param bool|number $addon_id
22223 * @param string $type "json" or "zip"
22224 *
22225 * @return string
22226 */
22227 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
22228
22229 $is_addon = $this->_is_addon_id( $addon_id );
22230
22231 $is_premium = null;
22232 if ( ! $is_addon ) {
22233 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
22234 } else if ( $this->is_addon_activated( $addon_id ) ) {
22235 $fs_addon = self::get_instance_by_id( $addon_id );
22236 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
22237 }
22238
22239 // If add-on, then append add-on ID.
22240 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
22241 '/updates/latest.' . $type;
22242
22243 // If add-on and not yet activated, try to fetch based on server licensing.
22244 if ( is_bool( $is_premium ) ) {
22245 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
22246 }
22247
22248 if ( $this->has_secret_key() ) {
22249 $endpoint = add_query_arg( 'type', 'all', $endpoint );
22250 } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) {
22251 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
22252 }
22253
22254 return $endpoint;
22255 }
22256
22257 /**
22258 * @author Vova Feldman (@svovaf)
22259 * @since 1.0.4
22260 *
22261 * @param bool|number $addon_id
22262 * @param bool $flush Since 1.1.7.3
22263 * @param int $expiration Since 1.2.2.7
22264 * @param bool|string $newer_than Since 2.2.1
22265 * @param bool|string $fetch_readme Since 2.2.1
22266 *
22267 * @return object|false Plugin latest tag info.
22268 */
22269 function _fetch_latest_version(
22270 $addon_id = false,
22271 $flush = true,
22272 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22273 $newer_than = false,
22274 $fetch_readme = true
22275 ) {
22276 $this->_logger->entrance();
22277
22278 if ( $this->is_unresolved_clone( true ) ) {
22279 return false;
22280 }
22281
22282 $switch_to_blog_id = null;
22283
22284 /**
22285 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
22286 * @since 1.1.7.4 Also check updates for add-ons.
22287 */
22288 if (
22289 ( ! $this->is_registered() || ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed() ) &&
22290 ! $this->_is_addon_id( $addon_id )
22291 ) {
22292 if ( ! is_multisite() ) {
22293 return false;
22294 }
22295
22296 $installs_map = $this->get_blog_install_map();
22297
22298 foreach ( $installs_map as $blog_id => $install ) {
22299 if ( ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed( $blog_id ) ) {
22300 continue;
22301 }
22302
22303 /**
22304 * @var FS_Site $install
22305 */
22306 if ( $install->is_trial() ) {
22307 $switch_to_blog_id = $blog_id;
22308 break;
22309 }
22310
22311 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
22312 $license = $this->get_license_by_id( $install->license_id );
22313
22314 if ( is_object( $license ) && $license->is_features_enabled() ) {
22315 $switch_to_blog_id = $blog_id;
22316 break;
22317 }
22318 }
22319 }
22320
22321 if ( is_null( $switch_to_blog_id ) ) {
22322 return false;
22323 }
22324 }
22325
22326 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
22327 get_current_blog_id() :
22328 0;
22329
22330 if ( is_numeric( $switch_to_blog_id ) ) {
22331 $this->switch_to_blog( $switch_to_blog_id );
22332 }
22333
22334 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
22335
22336 if ( ! empty( $newer_than ) ) {
22337 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
22338 }
22339
22340 if ( true === $fetch_readme ) {
22341 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
22342 }
22343
22344 $tag = $this->get_api_site_or_plugin_scope()->get(
22345 $latest_version_endpoint,
22346 $flush,
22347 $expiration
22348 );
22349
22350 if ( is_numeric( $switch_to_blog_id ) ) {
22351 $this->switch_to_blog( $current_blog_id );
22352 }
22353
22354 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
22355
22356 $this->_logger->departure( 'Latest version ' . $latest_version );
22357
22358 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
22359 }
22360
22361 #----------------------------------------------------------------------------------
22362 #region Download Plugin
22363 #----------------------------------------------------------------------------------
22364
22365 /**
22366 * Download latest plugin version, based on plan.
22367 *
22368 * Not like _download_latest(), this will redirect the page
22369 * to secure download url to prevent dual download (from FS to WP server,
22370 * and then from WP server to the client / browser).
22371 *
22372 * @author Vova Feldman (@svovaf)
22373 * @since 1.0.9
22374 *
22375 * @param bool|number $plugin_id
22376 *
22377 * @uses FS_Api
22378 * @uses wp_redirect()
22379 */
22380 private function download_latest_directly( $plugin_id = false ) {
22381 $this->_logger->entrance();
22382
22383 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
22384 }
22385
22386 /**
22387 * Get latest plugin FS API download URL.
22388 *
22389 * @author Vova Feldman (@svovaf)
22390 * @since 1.0.9
22391 *
22392 * @param bool|number $plugin_id
22393 *
22394 * @return string
22395 */
22396 private function get_latest_download_api_url( $plugin_id = false ) {
22397 $this->_logger->entrance();
22398
22399 $download_api_url = $this->get_api_site_scope()->get_signed_url(
22400 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
22401 );
22402
22403 return str_replace( 'http:', 'https:', $download_api_url );
22404 }
22405
22406 /**
22407 * Get payment invoice URL.
22408 *
22409 * @author Vova Feldman (@svovaf)
22410 * @since 1.2.0
22411 *
22412 * @param bool|number $payment_id
22413 *
22414 * @return string
22415 */
22416 function _get_invoice_api_url( $payment_id = false ) {
22417 $this->_logger->entrance();
22418
22419 $url = $this->get_api_user_scope()->get_signed_url(
22420 "/payments/{$payment_id}/invoice.pdf"
22421 );
22422
22423 if ( ! fs_starts_with( $url, 'https://' ) ) {
22424 // Always use HTTPS for invoices.
22425 $url = 'https' . substr( $url, 4 );
22426 }
22427
22428 return $url;
22429 }
22430
22431 /**
22432 * Get latest plugin download link.
22433 *
22434 * @author Vova Feldman (@svovaf)
22435 * @since 1.0.9
22436 *
22437 * @param string $label
22438 * @param bool|number $plugin_id
22439 *
22440 * @return string
22441 */
22442 private function get_latest_download_link( $label, $plugin_id = false ) {
22443 return sprintf(
22444 '<a target="_blank" rel="noopener" href="%s">%s</a>',
22445 $this->_get_latest_download_local_url( $plugin_id ),
22446 $label
22447 );
22448 }
22449
22450 /**
22451 * Get latest plugin download local URL.
22452 *
22453 * @author Vova Feldman (@svovaf)
22454 * @since 1.0.9
22455 *
22456 * @param bool|number $plugin_id
22457 *
22458 * @return string
22459 */
22460 function _get_latest_download_local_url( $plugin_id = false ) {
22461 // Add timestamp to protect from caching.
22462 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
22463
22464 if ( ! empty( $plugin_id ) ) {
22465 $params['plugin_id'] = $plugin_id;
22466 } else if ( $this->is_addon() ) {
22467 $params['plugin_id'] = $this->get_id();
22468 }
22469
22470 $fs = $this->is_addon() ?
22471 $this->get_parent_instance() :
22472 $this;
22473
22474 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
22475 }
22476
22477 #endregion Download Plugin ------------------------------------------------------------------
22478
22479 /**
22480 * @author Vova Feldman (@svovaf)
22481 * @since 1.0.4
22482 *
22483 * @uses FS_Api
22484 *
22485 * @param bool $background Hints the method if it's a background updates check. If false, it means that
22486 * was initiated by the admin.
22487 * @param bool|number $plugin_id
22488 * @param bool $flush Since 1.1.7.3
22489 * @param int $expiration Since 1.2.2.7
22490 * @param bool|string $newer_than Since 2.2.1
22491 */
22492 private function check_updates(
22493 $background = false,
22494 $plugin_id = false,
22495 $flush = true,
22496 $expiration = FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION,
22497 $newer_than = false
22498 ) {
22499 $this->_logger->entrance();
22500
22501 // Check if there's a newer version for download.
22502 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than );
22503
22504 $update = null;
22505 if ( is_object( $new_version ) ) {
22506 $update = new FS_Plugin_Tag( $new_version );
22507
22508 if ( ! $background ) {
22509 $this->_admin_notices->add(
22510 sprintf(
22511 /* translators: %s: Numeric version number (e.g. '2.1.9' */
22512 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
22513 $update->version,
22514 sprintf(
22515 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22516 $this->get_account_url( 'download_latest' ),
22517 sprintf(
22518 /* translators: %s: plan name (e.g. latest "Professional" version) */
22519 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
22520 $this->get_plan_title()
22521 )
22522 )
22523 ),
22524 $this->get_text_inline( 'New', 'new' ) . '!'
22525 );
22526 }
22527 } else if ( false === $new_version && ! $background ) {
22528 $this->_admin_notices->add(
22529 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
22530 $this->get_text_inline( 'You are all good!', 'you-are-good' )
22531 );
22532 }
22533
22534 $this->_store_update( $update, true, $plugin_id );
22535 }
22536
22537 /**
22538 * @author Vova Feldman (@svovaf)
22539 * @since 1.0.4
22540 *
22541 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
22542 *
22543 * @return FS_Plugin[]
22544 *
22545 * @uses FS_Api
22546 */
22547 private function sync_addons( $flush = false ) {
22548 $this->_logger->entrance();
22549
22550 $api = $this->get_api_site_or_plugin_scope();
22551
22552 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
22553
22554 /**
22555 * @since 1.2.1
22556 *
22557 * If there's a cached version of the add-ons and not asking
22558 * for a flush, just use the currently stored add-ons.
22559 */
22560 if ( ! $flush && $api->is_cached( $path ) ) {
22561 $addons = self::get_all_addons();
22562
22563 return isset( $addons[ $this->_plugin->id ] ) ?
22564 $addons[ $this->_plugin->id ] :
22565 array();
22566 }
22567
22568 $result = $api->get( $path, $flush );
22569
22570 $addons = array();
22571 if ( $this->is_api_result_object( $result, 'plugins' ) &&
22572 is_array( $result->plugins )
22573 ) {
22574 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
22575 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
22576 }
22577
22578 $this->_store_addons( $addons, true );
22579 }
22580
22581 return $addons;
22582 }
22583
22584 /**
22585 * Handle user email update.
22586 *
22587 * @author Vova Feldman (@svovaf)
22588 * @since 1.0.3
22589 * @uses FS_Api
22590 *
22591 * @param string $new_email
22592 *
22593 * @return object
22594 */
22595 private function update_email( $new_email ) {
22596 $this->_logger->entrance();
22597
22598 $api = $this->get_api_user_scope();
22599 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
22600 'email' => $new_email,
22601 'after_email_confirm_url' => $this->_get_admin_page_url(
22602 'account',
22603 array( 'fs_action' => 'sync_user' )
22604 ),
22605 ) );
22606
22607 if ( ! isset( $user->error ) ) {
22608 $this->_user->email = $user->email;
22609 $this->_user->is_verified = $user->is_verified;
22610 $this->_store_user();
22611 } else {
22612 // handle different error cases.
22613 }
22614
22615 return $user;
22616 }
22617
22618 #----------------------------------------------------------------------------------
22619 #region API Error Handling
22620 #----------------------------------------------------------------------------------
22621
22622 /**
22623 * @author Vova Feldman (@svovaf)
22624 * @since 1.1.1
22625 *
22626 * @param mixed $result
22627 *
22628 * @return bool Is API result contains an error.
22629 */
22630 private function is_api_error( $result ) {
22631 return FS_Api::is_api_error( $result );
22632 }
22633
22634 /**
22635 * Checks if given API result is a non-empty and not an error object.
22636 *
22637 * @author Vova Feldman (@svovaf)
22638 * @since 1.2.1.5
22639 *
22640 * @param mixed $result
22641 * @param string|null $required_property Optional property we want to verify that is set.
22642 *
22643 * @return bool
22644 */
22645 function is_api_result_object( $result, $required_property = null ) {
22646 return FS_Api::is_api_result_object( $result, $required_property );
22647 }
22648
22649 /**
22650 * Checks if given API result is a non-empty entity object with non-empty ID.
22651 *
22652 * @author Vova Feldman (@svovaf)
22653 * @since 1.2.1.5
22654 *
22655 * @param mixed $result
22656 *
22657 * @return bool
22658 */
22659 private function is_api_result_entity( $result ) {
22660 return FS_Api::is_api_result_entity( $result );
22661 }
22662
22663 #endregion
22664
22665 /**
22666 * Make sure a given argument is an array of a specific type.
22667 *
22668 * @author Vova Feldman (@svovaf)
22669 * @since 1.2.1.5
22670 *
22671 * @param mixed $array
22672 * @param string $class
22673 *
22674 * @return bool
22675 */
22676 private function is_array_instanceof( $array, $class ) {
22677 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
22678 }
22679
22680 /**
22681 * Start install ownership change.
22682 *
22683 * @author Vova Feldman (@svovaf)
22684 * @since 1.1.1
22685 * @uses FS_Api
22686 *
22687 * @param string $candidate_email
22688 * @param string $transfer_type
22689 *
22690 * @return bool Is ownership change successfully initiated.
22691 */
22692 private function init_change_owner( $candidate_email, $transfer_type ) {
22693 $this->_logger->entrance();
22694
22695 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
22696 $install_ids = array();
22697
22698 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
22699 $install = $install_info['install'];
22700
22701 if ( $this->_user->id != $install->user_id ) {
22702 // Skip add-on installs that are not owned by the parent product's install's owner.
22703 continue;
22704 }
22705
22706 $install_ids[ $slug ] = $install->id;
22707 }
22708
22709 $api = $this->get_api_site_scope();
22710 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
22711 'email' => $candidate_email,
22712 'transfer_type' => $transfer_type,
22713 'install_ids' => implode( ',', array_values( $install_ids ) ),
22714 'after_confirm_url' => $this->_get_admin_page_url(
22715 'account',
22716 array( 'fs_action' => 'change_owner' )
22717 ),
22718 ) );
22719
22720 return ! $this->is_api_error( $result );
22721 }
22722
22723 /**
22724 * Handle install ownership change.
22725 *
22726 * @author Vova Feldman (@svovaf)
22727 * @since 1.1.1
22728 * @uses FS_Api
22729 *
22730 * @return bool Was ownership change successfully complete.
22731 */
22732 private function complete_change_owner() {
22733 $this->_logger->entrance();
22734
22735 $install_ids = fs_request_get( 'install_ids' );
22736
22737 if ( ! empty( $install_ids ) ) {
22738 $install_ids = explode( ',', $install_ids );
22739
22740 foreach ( $install_ids as $key => $install_id ) {
22741 if ( ! FS_Site::is_valid_id( $install_id ) ) {
22742 unset( $install_ids[ $key ] );
22743 }
22744 }
22745 }
22746
22747 if ( ! is_array( $install_ids ) ) {
22748 $install_ids = array();
22749 }
22750
22751 $user = new FS_User();
22752 $user->id = fs_request_get( 'user_id' );
22753 $user->public_key = fs_request_get_raw( 'user_public_key' );
22754 $user->secret_key = fs_request_get_raw( 'user_secret_key' );
22755
22756 $prev_user = $this->_user;
22757 $this->_user = $user;
22758
22759 $result = $this->get_api_user_scope( true )->get(
22760 "/installs.json?install_ids=" . implode( ',', $install_ids )
22761 );
22762
22763 $current_blog_sites = self::get_all_sites( $this->get_module_type() );
22764
22765 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22766 $site_id_slug_map = array();
22767
22768 foreach ( $current_blog_sites as $slug => $site ) {
22769 $site_id_slug_map[ $site->id ] = $slug;
22770 }
22771
22772 foreach ( $result->installs as $install ) {
22773 $site = new FS_Site( $install );
22774
22775 if ( ! isset( $site_id_slug_map[ $install->id ] ) ) {
22776 continue;
22777 }
22778
22779 $current_blog_sites[ $site_id_slug_map[ $install->id ] ] = clone $site;
22780
22781 if ( $this->_site->id == $site->id ) {
22782 $this->_site = $site;
22783 }
22784 }
22785 }
22786
22787 // Validate install's user and given user.
22788 if ( $user->id != $this->_site->user_id ) {
22789 $this->_user = $prev_user;
22790
22791 return false;
22792 }
22793
22794 $this->set_account_option( 'sites', $current_blog_sites, true );
22795
22796 // Fetch new user information.
22797 $user_result = $this->get_api_user_scope( true )->get();
22798 $user = new FS_User( $user_result );
22799 $this->_user = $user;
22800
22801 $this->_set_account( $user, $this->_site );
22802
22803 $remove_user = true;
22804 $all_modules_sites = self::get_all_modules_sites();
22805
22806 foreach ( $all_modules_sites as $sites_by_module_type ) {
22807 foreach ( $sites_by_module_type as $sites_by_slug ) {
22808 foreach ( $sites_by_slug as $site ) {
22809 if ( $prev_user->id == $site->user_id ) {
22810 $remove_user = false;
22811 break;
22812 }
22813 }
22814
22815 if ( ! $remove_user ) {
22816 break;
22817 }
22818 }
22819
22820 if ( ! $remove_user ) {
22821 break;
22822 }
22823 }
22824
22825 if ( $remove_user ) {
22826 $users = self::get_all_users();
22827
22828 if ( isset( $users[ $prev_user->id ] ) ) {
22829 unset( $users[ $prev_user->id ] );
22830 } else {
22831 // If the prev user wasn't found by the key, iterate over the users collection.
22832 foreach ( $users as $key => $user ) {
22833 if ( $user->id == $prev_user->id ) {
22834 unset( $users[ $key ] );
22835 break;
22836 }
22837 }
22838 }
22839
22840 $this->set_account_option( 'users', $users, true );
22841 }
22842
22843 return true;
22844 }
22845
22846 /**
22847 * Completes ownership change by license.
22848 *
22849 * @author Leo Fajardo (@leorw)
22850 * @since 2.3.2
22851 *
22852 * @param number $user_id
22853 * @param array[string]number $install_ids_by_slug_map
22854 *
22855 */
22856 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
22857 $this->_logger->entrance();
22858
22859 $this->sync_user_by_current_install( $user_id );
22860
22861 $result = $this->get_api_user_scope( true )->get(
22862 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
22863 );
22864
22865 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22866 $sites = self::get_all_sites( $this->get_module_type() );
22867 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
22868
22869 foreach ( $result->installs as $install ) {
22870 $site = new FS_Site( $install );
22871
22872 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
22873 }
22874
22875 $this->set_account_option( 'sites', $sites, true );
22876 }
22877 }
22878
22879 /**
22880 * Handle user name update.
22881 *
22882 * @author Vova Feldman (@svovaf)
22883 * @since 1.0.9
22884 * @uses FS_Api
22885 *
22886 * @return object
22887 */
22888 private function update_user_name() {
22889 $this->_logger->entrance();
22890 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
22891
22892 $api = $this->get_api_user_scope();
22893 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
22894 'name' => $name,
22895 ) );
22896
22897 if ( ! isset( $user->error ) ) {
22898 $this->_user->first = $user->first;
22899 $this->_user->last = $user->last;
22900 $this->_store_user();
22901 } else {
22902 // handle different error cases.
22903
22904 }
22905
22906 return $user;
22907 }
22908
22909 /**
22910 * Verify user email.
22911 *
22912 * @author Vova Feldman (@svovaf)
22913 * @since 1.0.3
22914 * @uses FS_Api
22915 */
22916 private function verify_email() {
22917 $this->_handle_account_user_sync();
22918
22919 if ( $this->_user->is_verified() ) {
22920 return;
22921 }
22922
22923 $api = $this->get_api_site_scope();
22924 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
22925 'after_email_confirm_url' => $this->_get_admin_page_url(
22926 'account',
22927 array( 'fs_action' => 'sync_user' )
22928 )
22929 ) );
22930
22931 if ( ! isset( $result->error ) ) {
22932 $this->_admin_notices->add( sprintf(
22933 $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' ),
22934 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
22935 ) );
22936 } else {
22937 // handle different error cases.
22938
22939 }
22940 }
22941
22942 /**
22943 * @author Vova Feldman (@svovaf)
22944 * @since 1.1.2
22945 *
22946 * @param array $params
22947 * @param bool|null $network
22948 *
22949 * @return string
22950 */
22951 function get_activation_url( $params = array(), $network = null ) {
22952 if ( $this->is_addon() && $this->has_free_plan() ) {
22953 /**
22954 * @author Vova Feldman (@svovaf)
22955 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
22956 */
22957 return $this->get_parent_instance()->get_activation_url( $params );
22958 }
22959
22960 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
22961 }
22962
22963 /**
22964 * @author Vova Feldman (@svovaf)
22965 * @since 1.2.1.5
22966 *
22967 * @param array $params
22968 *
22969 * @return string
22970 */
22971 function get_reconnect_url( $params = array() ) {
22972 $params['fs_action'] = 'reset_anonymous_mode';
22973 $params['fs_unique_affix'] = $this->get_unique_affix();
22974
22975 return $this->get_activation_url( $params );
22976 }
22977
22978 /**
22979 * Get the URL of the page that should be loaded after the user connect
22980 * or skip in the opt-in screen.
22981 *
22982 * @author Vova Feldman (@svovaf)
22983 * @since 1.1.3
22984 *
22985 * @param string $filter Filter name.
22986 * @param array $params Since 1.2.2.7
22987 * @param bool|null $network
22988 *
22989 * @return string
22990 */
22991 function get_after_activation_url( $filter, $params = array(), $network = null ) {
22992 if ( $this->show_opt_in_on_themes_page() &&
22993 ( fs_request_has( 'pending_activation' ) ||
22994 // For cases when the first time path is set, even though it's a WP.org theme.
22995 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
22996 ) {
22997 $first_time_path = '';
22998 } else {
22999 $first_time_path = $this->_menu->get_first_time_path(
23000 fs_is_network_admin() && $this->_is_network_active
23001 );
23002 }
23003
23004 if ( $this->_is_network_active &&
23005 fs_is_network_admin() &&
23006 ! $this->_menu->has_network_menu() &&
23007 $this->is_network_registered()
23008 ) {
23009 $target_url = $this->get_account_url();
23010 } else {
23011 // Default plugin's page.
23012 $target_url = $this->_get_admin_page_url( '', array(), $network );
23013 }
23014
23015 return add_query_arg( $params, $this->apply_filters(
23016 $filter,
23017 empty( $first_time_path ) ?
23018 $target_url :
23019 $first_time_path
23020 ) );
23021 }
23022
23023 /**
23024 * Handle account page updates / edits / actions.
23025 *
23026 * @author Vova Feldman (@svovaf)
23027 * @since 1.0.2
23028 *
23029 */
23030 private function _handle_account_edits() {
23031 if ( ! $this->is_user_admin() ) {
23032 return;
23033 }
23034
23035 $action = fs_get_action();
23036
23037 if ( empty( $action ) ) {
23038 return;
23039 }
23040
23041 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
23042 $install_id = fs_request_get( 'install_id', '' );
23043
23044 // Alias.
23045 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
23046
23047 $is_network_action = $this->is_network_level_action();
23048 $blog_id = $this->is_network_level_site_specific_action();
23049 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
23050
23051 if ( is_numeric( $blog_id ) ) {
23052 $this->switch_to_blog( $blog_id );
23053 } else {
23054 $blog_id = '';
23055 }
23056
23057 switch ( $action ) {
23058 case 'opt_in':
23059 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23060
23061 if ( $is_parent_plugin_action ) {
23062 if ( $is_network_action && ! empty( $blog_id ) ) {
23063 if ( ! $this->is_registered() ) {
23064 $this->install_with_user(
23065 $this->get_network_user(),
23066 false,
23067 false,
23068 false,
23069 false
23070 );
23071
23072 $this->_admin_notices->add(
23073 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
23074 $this->get_text_inline( 'Awesome', 'awesome' )
23075 );
23076 }
23077 }
23078 }
23079 break;
23080
23081 case 'toggle_tracking':
23082 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23083
23084 if ( $is_parent_plugin_action ) {
23085 if ( $is_network_action && ! empty( $blog_id ) ) {
23086 if ( $this->is_registered( true ) ) {
23087 if ( $this->is_tracking_prohibited( $blog_id ) ) {
23088 if ( $this->toggle_site_tracking( true, $blog_id ) ) {
23089 $this->_admin_notices->add(
23090 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>" ),
23091 $this->get_text_inline( 'Thank you!', 'thank-you' )
23092 );
23093 }
23094 } else {
23095 if ( $this->toggle_site_tracking( false, $blog_id ) ) {
23096 $install = $this->get_install_by_blog_id( $blog_id );
23097
23098 $this->_admin_notices->add(
23099 sprintf(
23100 $this->get_text_inline( 'Diagnostic data will no longer be sent from %s to %s.', 'opted-out-successfully' ),
23101 self::get_unfiltered_site_url( $blog_id, true ),
23102 "<b>{$this->get_plugin_title()}</b>"
23103 )
23104 );
23105 }
23106 }
23107 }
23108 }
23109 }
23110
23111 break;
23112
23113 case 'delete_account':
23114 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23115
23116 $is_network_deletion = $is_network_action && empty( $blog_id );
23117
23118 if ( $is_parent_plugin_action ) {
23119 // Delete add-on installs if have any.
23120 $installed_addons = $this->get_installed_addons();
23121 foreach ( $installed_addons as $fs_addon ) {
23122 if ( $is_network_deletion ) {
23123 $fs_addon->delete_network_account_event();
23124 } else {
23125 $fs_addon->delete_account_event();
23126 }
23127 }
23128
23129 if ( $is_network_deletion ) {
23130 $this->delete_network_account_event();
23131 } else {
23132 $this->delete_account_event();
23133 }
23134
23135 // Clear user and site.
23136 $this->_site = null;
23137 $this->_user = null;
23138
23139 $this->maybe_set_slug_and_network_menu_exists_flag();
23140
23141 fs_redirect( $this->get_activation_url() );
23142 } else {
23143 if ( $this->is_addon_activated( $plugin_id ) ) {
23144 $fs_addon = self::get_instance_by_id( $plugin_id );
23145
23146 if ( $is_network_deletion ) {
23147 $fs_addon->delete_network_account_event();
23148 } else {
23149 $fs_addon->delete_account_event();
23150 }
23151
23152 fs_redirect( $this->_get_admin_page_url( 'account' ) );
23153 }
23154 }
23155
23156 return;
23157
23158 case 'downgrade_account':
23159 if ( is_numeric( $blog_id ) ) {
23160 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23161 } else {
23162 check_admin_referer( $action );
23163 }
23164
23165 $switch_to_network_install_blog_after_cancellation = (
23166 is_numeric( $blog_id ) &&
23167 $plugin_id == $this->get_id() &&
23168 ! $this->is_trial()
23169 );
23170
23171 $result = $this->cancel_subscription_or_trial( $plugin_id );
23172 if ( $this->is_api_error( $result ) ) {
23173 $this->_admin_notices->add(
23174 $result->error->message,
23175 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23176 'error'
23177 );
23178 }
23179
23180 if ( $switch_to_network_install_blog_after_cancellation ) {
23181 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23182 }
23183
23184 return;
23185
23186 case 'activate_license':
23187 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23188
23189 $fs = $this;
23190 if ( $plugin_id != $this->get_id() ) {
23191 $fs = $this->is_addon_activated( $plugin_id ) ?
23192 self::get_instance_by_id( $plugin_id ) :
23193 null;
23194 }
23195
23196 if ( is_object( $fs ) ) {
23197 $fs->_activate_license();
23198
23199 /**
23200 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
23201 *
23202 * @author Leo Fajardo (@leorw)
23203 * @since 2.4.0
23204 */
23205 unset( $_REQUEST['plugin_id'] );
23206
23207 if ( $this->is_bundle_license_auto_activation_enabled() ) {
23208 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
23209 }
23210 }
23211
23212 return;
23213
23214 case 'deactivate_license':
23215 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23216
23217 if ( $plugin_id == $this->get_id() ) {
23218 $this->_deactivate_license();
23219
23220 if ( $this->is_only_premium() ) {
23221 // Clear user and site.
23222 $this->_site = null;
23223 $this->_user = null;
23224
23225 if ( ! $is_network_action ) {
23226 fs_redirect( $this->get_activation_url() );
23227 } else if ( is_numeric( $blog_id ) ) {
23228 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23229 }
23230 }
23231 } else {
23232 if ( $this->is_addon_activated( $plugin_id ) ) {
23233 $fs_addon = self::get_instance_by_id( $plugin_id );
23234 $fs_addon->_deactivate_license();
23235 }
23236 }
23237
23238 return;
23239
23240 case 'check_updates':
23241 check_admin_referer( $action );
23242 $this->check_updates();
23243
23244 return;
23245
23246 case 'change_owner':
23247 $state = fs_request_get( 'state', 'init' );
23248 switch ( $state ) {
23249 case 'init':
23250 // The nonce is injected by the error handler in `_email_address_update_ajax_handler` function.
23251 check_admin_referer( 'change_owner' );
23252
23253 $candidate_email = fs_request_get( 'candidate_email' );
23254 $transfer_type = fs_request_get( 'transfer_type' );
23255
23256 if ( $this->init_change_owner( $candidate_email, $transfer_type ) ) {
23257 if ( 'transfer' === $transfer_type ) {
23258 $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>' ) );
23259 } else {
23260 $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>' ) );
23261 }
23262 }
23263 break;
23264 case 'owner_confirmed':
23265 // We cannot (or need not to) check the nonce and referer here, because the link comes from the email sent by our API.
23266 $candidate_email = fs_request_get( 'candidate_email', '' );
23267
23268 if ( ! is_email($candidate_email ) ) {
23269 return;
23270 }
23271
23272 $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>' ) );
23273 break;
23274 case 'candidate_confirmed':
23275 // We do not need to validate the authenticity of this request here, because the `complete_change_owner` does that for us through API calls.
23276 if ( $this->complete_change_owner() ) {
23277 $this->_admin_notices->add_sticky(
23278 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
23279 'ownership_changed',
23280 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
23281 );
23282 } else {
23283 // @todo Handle failed ownership change message.
23284 }
23285 break;
23286 }
23287
23288 return;
23289
23290 case 'update_user_name':
23291 check_admin_referer( 'update_user_name' );
23292
23293 $result = $this->update_user_name();
23294
23295 if ( isset( $result->error ) ) {
23296 $this->_admin_notices->add(
23297 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
23298 $oops_text,
23299 'error'
23300 );
23301 } else {
23302 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
23303 }
23304
23305 return;
23306
23307 #region Actions that might be called from external links (e.g. email)
23308
23309 /**
23310 * !!IMPORTANT!!: We cannot check for a valid nonce in this region, because the links could be coming from emails.
23311 */
23312
23313 case 'cancel_trial':
23314 $result = $this->cancel_subscription_or_trial( $plugin_id );
23315 if ( $this->is_api_error( $result ) ) {
23316 $this->_admin_notices->add(
23317 $result->error->message,
23318 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23319 'error'
23320 );
23321 }
23322
23323 return;
23324
23325 case 'verify_email':
23326 $this->verify_email();
23327
23328 return;
23329
23330 case 'sync_user':
23331 $this->_handle_account_user_sync();
23332
23333 return;
23334
23335 case $this->get_unique_affix() . '_sync_license':
23336 $this->_sync_license();
23337
23338 return;
23339
23340 case 'download_latest':
23341 $this->download_latest_directly( $plugin_id );
23342
23343 return;
23344
23345 #endregion
23346 }
23347
23348 if ( WP_FS__IS_POST_REQUEST ) {
23349 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
23350 foreach ( $properties as $p ) {
23351 if ( 'update_' . $p === $action ) {
23352 check_admin_referer( $action );
23353
23354 $this->_logger->log( $action );
23355
23356 $site_property = substr( $p, strlen( 'site_' ) );
23357 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
23358 $this->get_site()->{$site_property} = $site_property_value;
23359
23360 // Store account after modification.
23361 $this->_store_site();
23362
23363 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
23364
23365 $this->_admin_notices->add( sprintf(
23366 /* translators: %s: User's account property (e.g. email address, name) */
23367 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
23368 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
23369 ) );
23370
23371 return;
23372 }
23373 }
23374 }
23375 }
23376
23377 /**
23378 * Adds CSS classes for the body tag in the admin.
23379 *
23380 * @param string $classes Space-separated string of class names.
23381 *
23382 * @return string $classes FS Admin body tag class names.
23383 */
23384 public function fs_addons_body_class( $classes ) {
23385 $classes .= ' plugins-php';
23386 return $classes;
23387 }
23388
23389 /**
23390 * Account page resources load.
23391 *
23392 * @author Vova Feldman (@svovaf)
23393 * @since 1.0.6
23394 */
23395 function _account_page_load() {
23396 $this->_logger->entrance();
23397
23398 $this->_logger->info( var_export( $_REQUEST, true ) );
23399
23400 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
23401
23402 if ( $this->has_addons() ) {
23403 wp_enqueue_script( 'plugin-install' );
23404 add_thickbox();
23405 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23406 }
23407
23408 if ( $this->has_paid_plan() &&
23409 ! $this->has_any_license() &&
23410 ! $this->is_sync_executed() &&
23411 $this->is_tracking_allowed()
23412 ) {
23413 /**
23414 * If no licenses found and no sync job was executed during the last 24 hours,
23415 * just execute the sync job right away (blocking execution).
23416 *
23417 * @since 1.1.7.3
23418 */
23419 $this->run_manual_sync();
23420 }
23421
23422 $this->_handle_account_edits();
23423
23424 if (
23425 is_object( $this->_license ) &&
23426 $this->_license->user_id == $this->_user->id &&
23427 ! $this->is_whitelabeled( true )
23428 ) {
23429 $this->_admin_notices->add(
23430 sprintf(
23431 $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' ),
23432 sprintf(
23433 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
23434 $this->get_text_inline( 'Click here', 'click-here' )
23435 )
23436 ),
23437 '',
23438 'success',
23439 false,
23440 'license_not_whitelabeled'
23441 );
23442 }
23443
23444 $this->do_action( 'account_page_load_before_departure' );
23445 }
23446
23447 /**
23448 * Renders the "Affiliation" page.
23449 *
23450 * @author Leo Fajardo (@leorw)
23451 * @since 1.2.3
23452 */
23453 function _affiliation_page_render() {
23454 $this->_logger->entrance();
23455
23456 $this->fetch_affiliate_and_terms();
23457
23458 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
23459
23460 $is_bundle_context = $this->has_bundle_context();
23461
23462 $plugin_title = $this->get_plugin_title();
23463
23464 if ( $is_bundle_context ) {
23465 $plugin_title = $this->plugin_affiliate_terms->plugin_title;
23466
23467 // Add the suffix "Bundle" only if the word is not present in the title itself.
23468 if ( false === mb_stripos( $plugin_title, fs_text_inline( 'Bundle', 'bundle' ) ) ) {
23469 $plugin_title = $this->apply_filters(
23470 'formatted_bundle_title',
23471 $plugin_title . ' ' . fs_text_inline( 'Bundle', 'bundle' )
23472 );
23473 }
23474 }
23475
23476 $vars = array(
23477 'id' => $this->_module_id,
23478 'plugin_title' => $plugin_title,
23479 );
23480 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
23481 }
23482
23483
23484 /**
23485 * Render account page.
23486 *
23487 * @author Vova Feldman (@svovaf)
23488 * @since 1.0.0
23489 */
23490 function _account_page_render() {
23491 $this->_logger->entrance();
23492
23493 $template = 'account.php';
23494 $vars = array( 'id' => $this->_module_id );
23495
23496 /**
23497 * Added filter to the template to allow developers wrapping the template
23498 * in custom HTML (e.g. within a wizard/tabs).
23499 *
23500 * @author Vova Feldman (@svovaf)
23501 * @since 1.2.1.6
23502 */
23503 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
23504 }
23505
23506 /**
23507 * Render account connect page.
23508 *
23509 * @author Vova Feldman (@svovaf)
23510 * @since 1.0.7
23511 */
23512 function _connect_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/connect.php', fs_get_template( 'connect.php', $vars ) );
23525 }
23526
23527 /**
23528 * Load required resources before add-ons page render.
23529 *
23530 * @author Vova Feldman (@svovaf)
23531 * @since 1.0.6
23532 */
23533 function _addons_page_load() {
23534 $this->_logger->entrance();
23535
23536 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
23537
23538 wp_enqueue_script( 'plugin-install' );
23539 add_thickbox();
23540 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23541
23542 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
23543 $this->_admin_notices->add(
23544 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>' ),
23545 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
23546 'update-nag'
23547 );
23548 }
23549 }
23550
23551 /**
23552 * Render add-ons page.
23553 *
23554 * @author Vova Feldman (@svovaf)
23555 * @since 1.0.6
23556 */
23557 function _addons_page_render() {
23558 $this->_logger->entrance();
23559
23560 $vars = array( 'id' => $this->_module_id );
23561
23562 /**
23563 * Added filter to the template to allow developers wrapping the template
23564 * in custom HTML (e.g. within a wizard/tabs).
23565 *
23566 * @author Vova Feldman (@svovaf)
23567 * @since 1.2.1.6
23568 */
23569 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
23570 }
23571
23572 /* Pricing & Upgrade
23573 ------------------------------------------------------------------------------------------------------------------*/
23574 /**
23575 * Render pricing page.
23576 *
23577 * @author Vova Feldman (@svovaf)
23578 * @since 1.0.0
23579 */
23580 function _pricing_page_render() {
23581 $this->_logger->entrance();
23582
23583 $vars = array( 'id' => $this->_module_id );
23584
23585 if ( 'true' === fs_request_get( 'checkout', false ) ) {
23586 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
23587 } else {
23588 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
23589 }
23590 }
23591
23592 /**
23593 * @author Leo Fajardo (@leorw)
23594 * @since 2.3.1
23595 */
23596 function _maybe_add_pricing_ajax_handler() {
23597 if ( ! $this->should_use_external_pricing() ) {
23598 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
23599 }
23600 }
23601
23602 /**
23603 * @author Leo Fajardo (@leorw)
23604 * @since 2.3.1
23605 */
23606 function _fs_pricing_ajax_action_handler() {
23607 $this->check_ajax_referer( 'pricing_ajax_action' );
23608
23609 $result = null;
23610 $pricing_action = fs_request_get( 'pricing_action' );
23611
23612 switch ( $pricing_action ) {
23613 case 'fetch_pricing_data':
23614 $params = array(
23615 'is_enriched' => true,
23616 'trial' => fs_request_get_bool( 'trial' ),
23617 'sandbox' => fs_request_get_raw( 'sandbox' ),
23618 's_ctx_type' => fs_request_get_raw( 's_ctx_type' ),
23619 's_ctx_id' => fs_request_get_raw( 's_ctx_id' ),
23620 's_ctx_ts' => fs_request_get_raw( 's_ctx_ts' ),
23621 's_ctx_secure' => fs_request_get_raw( 's_ctx_secure' ),
23622 );
23623
23624 $bundle_id = $this->get_bundle_id();
23625 $bundle_public_key = $this->get_bundle_public_key();
23626
23627 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
23628
23629 if ( ! $has_bundle_context ) {
23630 $api = $this->get_api_plugin_scope();
23631 } else {
23632 $api = FS_Api::instance(
23633 $bundle_id,
23634 'plugin',
23635 $bundle_id,
23636 $bundle_public_key,
23637 ! $this->is_live(),
23638 false,
23639 $this->get_sdk_version()
23640 );
23641
23642 $params['plugin_id'] = $this->get_id();
23643 $params['plugin_public_key'] = $this->get_public_key();
23644 }
23645
23646 $result = $api->get( 'pricing.json?' . http_build_query( $params ) );
23647 break;
23648 case 'start_trial':
23649 $result = $this->opt_in(
23650 false,
23651 false,
23652 false,
23653 false,
23654 false,
23655 fs_request_get( 'plan_id' )
23656 );
23657 }
23658
23659 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
23660 $this->_logger->api_error( $result );
23661
23662 self::shoot_ajax_failure(
23663 isset( $result->error ) ?
23664 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
23665 var_export( $result, true )
23666 );
23667 }
23668
23669 $this->shoot_ajax_success( $result );
23670 }
23671
23672 #----------------------------------------------------------------------------------
23673 #region Contact Us
23674 #----------------------------------------------------------------------------------
23675
23676 /**
23677 * Render contact-us page.
23678 *
23679 * @author Vova Feldman (@svovaf)
23680 * @since 1.0.3
23681 */
23682 function _contact_page_render() {
23683 $this->_logger->entrance();
23684
23685 $vars = array( 'id' => $this->_module_id );
23686
23687 /**
23688 * Added filter to the template to allow developers wrapping the template
23689 * in custom HTML (e.g. within a wizard/tabs).
23690 *
23691 * @author Vova Feldman (@svovaf)
23692 * @since 2.1.3
23693 */
23694 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
23695 }
23696
23697 #endregion ------------------------------------------------------------------------
23698
23699 /**
23700 * Hide all admin notices to prevent distractions.
23701 *
23702 * @author Vova Feldman (@svovaf)
23703 * @since 1.0.3
23704 *
23705 * @uses remove_all_actions()
23706 */
23707 private static function _hide_admin_notices() {
23708 remove_all_actions( 'admin_notices' );
23709 remove_all_actions( 'network_admin_notices' );
23710 remove_all_actions( 'all_admin_notices' );
23711 remove_all_actions( 'user_admin_notices' );
23712 }
23713
23714 static function _clean_admin_content_section_hook() {
23715 $hide_admin_notices = true;
23716
23717 if ( fs_request_is_action( 'allow_clone_resolution_notice' ) ) {
23718 check_admin_referer( 'fs_allow_clone_resolution_notice' );
23719
23720 $hide_admin_notices = false;
23721 }
23722
23723 if ( $hide_admin_notices ) {
23724 self::_hide_admin_notices();
23725 }
23726
23727 // Hide footer.
23728 echo '<style>#wpfooter { display: none !important; }</style>';
23729 }
23730
23731 /**
23732 * Attach to admin_head hook to hide all admin notices.
23733 *
23734 * @author Vova Feldman (@svovaf)
23735 * @since 1.0.3
23736 */
23737 static function _clean_admin_content_section() {
23738 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
23739 }
23740
23741 /* CSS & JavaScript
23742 ------------------------------------------------------------------------------------------------------------------*/
23743 /* function _enqueue_script($handle, $src) {
23744 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
23745
23746 $this->_logger->entrance( 'script = ' . $url );
23747
23748 wp_enqueue_script( $handle, $url );
23749 }*/
23750
23751 /* SDK
23752 ------------------------------------------------------------------------------------------------------------------*/
23753 private $_user_api;
23754
23755 /**
23756 *
23757 * @author Vova Feldman (@svovaf)
23758 * @since 1.0.2
23759 *
23760 * @param bool $flush
23761 *
23762 * @return FS_Api
23763 */
23764 function get_api_user_scope( $flush = false ) {
23765 if ( ! isset( $this->_user_api ) || $flush ) {
23766 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
23767 }
23768
23769 return $this->_user_api;
23770 }
23771
23772 /**
23773 * @author Vova Feldman (@svovaf)
23774 * @since 2.0.0
23775 *
23776 * @param \FS_User $user
23777 *
23778 * @return \FS_Api
23779 */
23780 private function get_api_user_scope_by_user( FS_User $user ) {
23781 return FS_Api::instance(
23782 $this->_module_id,
23783 'user',
23784 $user->id,
23785 $user->public_key,
23786 ! $this->is_live(),
23787 $user->secret_key,
23788 $this->get_sdk_version()
23789 );
23790 }
23791
23792 /**
23793 *
23794 * @author Leo Fajardo (@leorw)
23795 * @since 2.0.0
23796 *
23797 * @param bool $flush
23798 *
23799 * @return FS_Api
23800 */
23801 private function get_current_or_network_user_api_scope( $flush = false ) {
23802 if ( ! $this->_is_network_active ||
23803 ( isset( $this->_user ) && $this->_user instanceof FS_User )
23804 ) {
23805 return $this->get_api_user_scope( $flush );
23806 }
23807
23808 $user = $this->get_current_or_network_user();
23809
23810 $this->_user_api = FS_Api::instance(
23811 $this->_module_id,
23812 'user',
23813 $user->id,
23814 $user->public_key,
23815 ! $this->is_live(),
23816 $user->secret_key,
23817 $this->get_sdk_version()
23818 );
23819
23820 return $this->_user_api;
23821 }
23822
23823 private $_site_api;
23824
23825 /**
23826 *
23827 * @author Vova Feldman (@svovaf)
23828 * @since 1.0.2
23829 *
23830 * @param bool $flush
23831 *
23832 * @return FS_Api
23833 */
23834 private function get_api_site_scope( $flush = false ) {
23835 if ( ! isset( $this->_site_api ) || $flush ) {
23836 $this->_site_api = FS_Api::instance(
23837 $this->_module_id,
23838 'install',
23839 $this->_site->id,
23840 $this->_site->public_key,
23841 ! $this->is_live(),
23842 $this->_site->secret_key,
23843 $this->get_sdk_version(),
23844 self::get_unfiltered_site_url()
23845 );
23846 }
23847
23848 return $this->_site_api;
23849 }
23850
23851 /**
23852 * @author Leo Fajardo (@leorw)
23853 * @since 2.5.0
23854 *
23855 * @param string $path
23856 * @param string $method
23857 * @param array $params
23858 * @param bool $flush_instance
23859 *
23860 * @return array|mixed|string|void
23861 * @throws Freemius_Exception
23862 */
23863 private function api_site_call( $path, $method = 'GET', $params = array(), $flush_instance = false ) {
23864 $result = $this->get_api_site_scope( $flush_instance )->call( $path, $method, $params );
23865
23866 /**
23867 * 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.
23868 *
23869 * @author Leo Fajardo (@leorw)
23870 * @since 2.5.0
23871 */
23872 if (
23873 $this->is_registered() &&
23874 FS_Api::is_api_result_entity( $result ) &&
23875 isset( $result->url )
23876 ) {
23877 $stored_local_url = trailingslashit( $this->_site->url );
23878 $stored_remote_url = trailingslashit( $result->url );
23879
23880 if ( $stored_local_url !== $stored_remote_url ) {
23881 $this->_site->url = $result->url;
23882 $this->_store_site();
23883 }
23884
23885 if ( fs_strip_url_protocol( $stored_remote_url ) !== self::get_unfiltered_site_url( null, true, true ) ) {
23886 FS_Clone_Manager::instance()->maybe_run_clone_resolution();
23887 }
23888 }
23889
23890 return $result;
23891 }
23892
23893 private $_plugin_api;
23894
23895 /**
23896 * Get plugin public API scope.
23897 *
23898 * @author Vova Feldman (@svovaf)
23899 * @since 1.0.7
23900 *
23901 * @return FS_Api
23902 */
23903 function get_api_plugin_scope() {
23904 if ( ! isset( $this->_plugin_api ) ) {
23905 $this->_plugin_api = FS_Api::instance(
23906 $this->_module_id,
23907 'plugin',
23908 $this->_plugin->id,
23909 $this->_plugin->public_key,
23910 ! $this->is_live(),
23911 false,
23912 $this->get_sdk_version()
23913 );
23914 }
23915
23916 return $this->_plugin_api;
23917 }
23918
23919 /**
23920 * Get bundle public API scope.
23921 *
23922 * @author Vova Feldman (@svovaf)
23923 * @since 2.3.1
23924 *
23925 * @return FS_Api
23926 */
23927 function get_api_bundle_scope() {
23928 return FS_Api::instance(
23929 $this->get_bundle_id(),
23930 'plugin',
23931 $this->get_bundle_id(),
23932 $this->get_bundle_public_key(),
23933 ! $this->is_live(),
23934 false,
23935 $this->get_sdk_version()
23936 );
23937 }
23938
23939 /**
23940 * Get site API scope object (fallback to public plugin scope when not registered).
23941 *
23942 * @author Vova Feldman (@svovaf)
23943 * @since 1.0.7
23944 *
23945 * @return FS_Api
23946 */
23947 function get_api_site_or_plugin_scope() {
23948 return $this->is_registered() ?
23949 $this->get_api_site_scope() :
23950 $this->get_api_plugin_scope();
23951 }
23952
23953 /**
23954 * @author Leo Fajardo (@leorw)
23955 * @since 2.2.3.1
23956 *
23957 * @param object $result
23958 */
23959 private function maybe_modify_api_curl_error_message( $result ) {
23960 if (
23961 'cUrlMissing' !== $result->error->type &&
23962 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
23963 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
23964 ) {
23965 return;
23966 }
23967
23968 $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' ) .
23969 ' ' .
23970 $this->esc_html_inline(
23971 sprintf(
23972 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
23973 implode(
23974 ', ',
23975 $this->apply_filters( 'api_domains', array(
23976 'api.freemius.com',
23977 'wp.freemius.com'
23978 ) )
23979 )
23980 ),
23981 'connectivity-whitelist'
23982 ) .
23983 ' ' .
23984 sprintf(
23985 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
23986 $this->get_module_type()
23987 );
23988 }
23989
23990 /**
23991 * Show trial promotional notice (if any trial exist).
23992 *
23993 * @author Vova Feldman (@svovaf)
23994 * @since 1.0.9
23995 *
23996 * @param FS_Plugin_Plan[] $plans
23997 */
23998 function _check_for_trial_plans( $plans ) {
23999 /**
24000 * 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.
24001 *
24002 * @author Vova Feldman (@svovaf)
24003 * @since 2.1.2
24004 */
24005 if ( ! is_array( $plans ) && is_object( $plans ) ) {
24006 $plans = array( $plans );
24007 }
24008
24009 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
24010 $plans = array();
24011 }
24012
24013 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
24014 }
24015
24016 /**
24017 * During trial promotion the "upgrade" submenu item turns to
24018 * "start trial" to encourage the trial. Since we want to keep
24019 * the same menu item handler and there's no robust way to
24020 * add new arguments to the menu item link's querystring,
24021 * use JavaScript to find the menu item and update the href of
24022 * the link.
24023 *
24024 * @author Vova Feldman (@svovaf)
24025 * @since 1.2.1.5
24026 */
24027 function _fix_start_trial_menu_item_url() {
24028 $template_args = array( 'id' => $this->_module_id );
24029 fs_require_template( 'add-trial-to-pricing.php', $template_args );
24030 }
24031
24032 /**
24033 * Check if module is currently in a trial promotion mode.
24034 *
24035 * @author Vova Feldman (@svovaf)
24036 * @since 1.2.2.7
24037 *
24038 * @return bool
24039 */
24040 function is_in_trial_promotion() {
24041 return $this->_admin_notices->has_sticky( 'trial_promotion' );
24042 }
24043
24044 /**
24045 * Show trial promotional notice (if any trial exist).
24046 *
24047 * @author Vova Feldman (@svovaf)
24048 * @since 1.0.9
24049 *
24050 * @return bool If trial notice added.
24051 */
24052 function _add_trial_notice() {
24053 if ( ! $this->is_user_admin() ) {
24054 return false;
24055 }
24056
24057 if ( ! $this->is_user_in_admin() ) {
24058 return false;
24059 }
24060
24061 if ( $this->_is_network_active ) {
24062 if ( fs_is_network_admin() ) {
24063 // Network level trial is disabled at the moment.
24064 return false;
24065 }
24066
24067 if ( ! $this->is_delegated_connection() ) {
24068 // Only delegated sites should support trials.
24069 return false;
24070 }
24071 }
24072
24073 // Check if trial message is already shown.
24074 if ( $this->is_in_trial_promotion() ) {
24075 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
24076
24077 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
24078
24079 return false;
24080 }
24081
24082 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
24083 // Don't show trial if running the premium code, unless running in DEV mode.
24084 return false;
24085 }
24086
24087 if ( ! $this->has_trial_plan() ) {
24088 // No plans with trial.
24089 return false;
24090 }
24091
24092 if ( ! $this->apply_filters( 'show_trial', true ) ) {
24093 // Developer explicitly asked not to show the trial promo.
24094 return false;
24095 }
24096
24097 if ( $this->is_registered() ) {
24098 // Check if trial already utilized.
24099 if ( $this->_site->is_trial_utilized() ) {
24100 return false;
24101 }
24102
24103 if ( $this->is_paying_or_trial() ) {
24104 // Don't show trial if paying or already in trial.
24105 return false;
24106 }
24107 }
24108
24109 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24110 // If not yet opted-in/skipped, or pending activation, don't show trial.
24111 return false;
24112 }
24113
24114 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
24115 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
24116
24117 // Show promotion if never shown before and 24 hours after initial activation with FS.
24118 if ( ! $was_promotion_shown_before &&
24119 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
24120 ) {
24121 return false;
24122 }
24123
24124 // OR if promotion was shown before, try showing it every 30 days.
24125 if ( $was_promotion_shown_before &&
24126 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
24127 ) {
24128 return false;
24129 }
24130
24131 $trial_period = $this->_trial_days;
24132 $require_payment = $this->_is_trial_require_payment;
24133 $trial_url = $this->get_trial_url();
24134 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
24135
24136 if ( $this->is_registered() ) {
24137 // If opted-in, override trial with up to date data from API.
24138 $trial_plans = FS_Plan_Manager::instance()->get_visible_trial_plans( $this->_plans );
24139 $trial_plans_count = count( $trial_plans );
24140
24141 if ( 0 === $trial_plans_count ) {
24142 // If there's no plans with a trial just exit.
24143 return false;
24144 }
24145
24146 /**
24147 * @var FS_Plugin_Plan $paid_plan
24148 */
24149 $paid_plan = $trial_plans[0];
24150 $require_payment = $paid_plan->is_require_subscription;
24151 $trial_period = $paid_plan->trial_period;
24152
24153 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
24154
24155 if ( $total_paid_plans !== $trial_plans_count ) {
24156 // Not all paid plans have a trial - generate a string of those that have it.
24157 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
24158 $plans_string .= sprintf(
24159 ' <a href="%s">%s</a>',
24160 $trial_url,
24161 $trial_plans[ $i ]->title
24162 );
24163
24164 if ( $i < $trial_plans_count - 2 ) {
24165 $plans_string .= ', ';
24166 } else if ( $i == $trial_plans_count - 2 ) {
24167 $plans_string .= ' and ';
24168 }
24169 }
24170 }
24171 }
24172
24173 $message = sprintf(
24174 $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' ),
24175 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
24176 $plans_string,
24177 $trial_period
24178 );
24179
24180 // "No Credit-Card Required" or "No Commitment for N Days".
24181 $cc_string = $require_payment ?
24182 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
24183 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
24184
24185
24186 // Start trial button.
24187 $button = ' ' . sprintf(
24188 '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24189 $trial_url,
24190 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
24191 );
24192
24193 $this->_admin_notices->add_sticky(
24194 $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
24195 'trial_promotion',
24196 '',
24197 'promotion'
24198 );
24199
24200 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
24201
24202 return true;
24203 }
24204
24205 /**
24206 * Lets users/customers know that the product has an affiliate program.
24207 *
24208 * @author Leo Fajardo (@leorw)
24209 * @since 1.2.2.11
24210 *
24211 * @return bool Returns true if the notice has been added.
24212 */
24213 function _add_affiliate_program_notice() {
24214 if ( ! $this->is_user_admin() ) {
24215 return false;
24216 }
24217
24218 if ( ! $this->is_user_in_admin() ) {
24219 return false;
24220 }
24221
24222 // Check if the notice is already shown.
24223 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
24224 return false;
24225 }
24226
24227 if (
24228 // Product has no affiliate program.
24229 ! $this->has_affiliate_program() ||
24230 // User has applied for an affiliate account.
24231 ! empty( $this->_storage->affiliate_application_data )
24232 ) {
24233 return false;
24234 }
24235
24236 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
24237 // Developer explicitly asked not to show the notice about the affiliate program.
24238 return false;
24239 }
24240
24241 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24242 // If not yet opted in/skipped, or pending activation, don't show the notice.
24243 return false;
24244 }
24245
24246 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
24247 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
24248
24249 /**
24250 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
24251 * activation with FS.
24252 */
24253 if ( $was_notice_shown_before ||
24254 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
24255 ) {
24256 return false;
24257 }
24258
24259 if ( ! $this->is_paying() &&
24260 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
24261 ) {
24262 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
24263 return false;
24264 }
24265
24266 $message = sprintf(
24267 $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' ),
24268 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
24269 $this->get_module_label( true )
24270 );
24271
24272 // HTML code for the "Learn more..." button.
24273 $button = ' ' . sprintf(
24274 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24275 $this->_get_admin_page_url( 'affiliation' ),
24276 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
24277 );
24278
24279 $this->_admin_notices->add_sticky(
24280 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
24281 'affiliate_program',
24282 '',
24283 'promotion'
24284 );
24285
24286 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
24287
24288 return true;
24289 }
24290
24291 /**
24292 * @author Vova Feldman (@svovaf)
24293 * @since 1.2.1.5
24294 */
24295 function _enqueue_common_css() {
24296 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
24297 // Add basic CSS for admin-notices and menu-item colors.
24298 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
24299 }
24300 }
24301
24302 /**
24303 * @author Leo Fajardo (@leorw)
24304 * @since 1.2.2
24305 */
24306 function _show_theme_activation_optin_dialog() {
24307 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
24308
24309 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
24310 }
24311
24312 /**
24313 * @author Leo Fajardo (@leorw)
24314 * @since 1.2.2
24315 */
24316 function _add_fs_theme_activation_dialog() {
24317 global $pagenow;
24318
24319 if ( 'themes.php' !== $pagenow ) {
24320 return;
24321 }
24322
24323 $vars = array( 'id' => $this->_module_id );
24324 fs_require_once_template( 'connect.php', $vars );
24325 }
24326
24327 /* Action Links
24328 ------------------------------------------------------------------------------------------------------------------*/
24329 private $_action_links_hooked = false;
24330 private $_action_links = array();
24331
24332 /**
24333 * Hook to plugin action links filter.
24334 *
24335 * @author Vova Feldman (@svovaf)
24336 * @since 1.0.0
24337 */
24338 private function hook_plugin_action_links() {
24339 $this->_logger->entrance();
24340
24341 $this->_action_links_hooked = true;
24342
24343 $this->_logger->log( 'Adding action links hooks.' );
24344
24345 // Add action link to settings page.
24346 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
24347 &$this,
24348 '_modify_plugin_action_links_hook'
24349 ), WP_FS__DEFAULT_PRIORITY, 2 );
24350 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
24351 &$this,
24352 '_modify_plugin_action_links_hook'
24353 ), WP_FS__DEFAULT_PRIORITY, 2 );
24354 }
24355
24356 /**
24357 * Add plugin action link.
24358 *
24359 * @author Vova Feldman (@svovaf)
24360 * @since 1.0.0
24361 *
24362 * @param $label
24363 * @param $url
24364 * @param bool $external
24365 * @param int $priority
24366 * @param bool $key
24367 */
24368 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
24369 $this->_logger->entrance();
24370
24371 if ( ! isset( $this->_action_links[ $priority ] ) ) {
24372 $this->_action_links[ $priority ] = array();
24373 }
24374
24375 if ( false === $key ) {
24376 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
24377 }
24378
24379 $this->_action_links[ $priority ][] = array(
24380 'label' => $label,
24381 'href' => $url,
24382 'key' => $key,
24383 'external' => $external
24384 );
24385 }
24386
24387 /**
24388 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
24389 *
24390 * @author Vova Feldman (@svovaf)
24391 * @since 1.0.0
24392 */
24393 function _add_upgrade_action_link() {
24394 $this->_logger->entrance();
24395
24396 $is_activation_mode = $this->is_activation_mode();
24397
24398 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
24399
24400 /**
24401 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
24402 * and "Add-Ons" menus should be added.
24403 *
24404 * @author Leo Fajardo (@leorw)
24405 * @since 2.3.0
24406 */
24407 $add_upgrade_link = (
24408 $add_action_links ||
24409 ( $is_activation_mode && $this->is_only_premium() )
24410 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
24411
24412 $add_addons_link = ( $add_action_links && $this->has_addons() );
24413
24414 if ( ! $add_upgrade_link && ! $add_addons_link ) {
24415 return;
24416 }
24417
24418 if (
24419 $add_upgrade_link &&
24420 $this->is_pricing_page_visible() &&
24421 $this->is_submenu_item_visible( 'pricing' )
24422 ) {
24423 $this->add_plugin_action_link(
24424 $this->get_text_inline( 'Upgrade', 'upgrade' ),
24425 $this->get_upgrade_url(),
24426 false,
24427 7,
24428 'upgrade'
24429 );
24430 }
24431
24432 if (
24433 $add_addons_link &&
24434 $this->has_addons() &&
24435 $this->is_submenu_item_visible( 'addons' )
24436 ) {
24437 $this->add_plugin_action_link(
24438 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
24439 $this->_get_admin_page_url( 'addons' ),
24440 false,
24441 9,
24442 'addons'
24443 );
24444 }
24445 }
24446
24447 /**
24448 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
24449 *
24450 * @author Leo Fajardo (@leorw)
24451 * @since 1.1.9
24452 */
24453 function _add_license_action_link() {
24454 $this->_logger->entrance();
24455
24456 if ( ! self::is_ajax() ) {
24457 // Inject license activation dialog UI and client side code.
24458 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
24459 }
24460
24461 $link_text = $this->is_free_plan() ?
24462 $this->get_text_inline( 'Activate License', 'activate-license' ) :
24463 $this->get_text_inline( 'Change License', 'change-license' );
24464
24465 $this->add_plugin_action_link(
24466 $link_text,
24467 '#',
24468 false,
24469 11,
24470 ( 'activate-license ' . $this->get_unique_affix() )
24471 );
24472 }
24473
24474 /**
24475 * @author Leo Fajardo (@leorw)
24476 * @since 2.0.2
24477 */
24478 function _add_premium_version_upgrade_selection_action() {
24479 $this->_logger->entrance();
24480
24481 if ( ! self::is_ajax() ) {
24482 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
24483 }
24484 }
24485
24486 /**
24487 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
24488 *
24489 * @author Leo Fajardo (@leorw)
24490 * @since 1.2.1.5
24491 */
24492 function _add_tracking_links() {
24493 if ( ! current_user_can( 'manage_options' ) ) {
24494 return;
24495 }
24496
24497 $this->_logger->entrance();
24498
24499 if ( $this->is_only_premium() && $this->is_free_plan() ) {
24500 // 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.
24501 return;
24502 }
24503
24504 if (
24505 $this->is_addon() &&
24506 ! $this->is_only_premium()
24507 ) {
24508 $parent = $this->get_parent_instance();
24509
24510 if ( is_object( $parent ) && $parent->is_anonymous() ) {
24511 return;
24512 }
24513 }
24514
24515 if ( fs_is_network_admin() ) {
24516 if ( ! $this->_is_network_active ) {
24517 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
24518 return;
24519 } else if ( $this->is_network_delegated_connection() ) {
24520 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
24521 return;
24522 }
24523 } else {
24524 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
24525 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
24526 return;
24527 }
24528 }
24529
24530 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
24531 if ( ! $this->is_registered() && $this->is_anonymous() ) {
24532 $this->connect_again();
24533
24534 return;
24535 }
24536 }
24537
24538 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
24539 ( $this->is_theme() && ! self::is_themes_page() )
24540 ) {
24541 // Only show tracking links on the plugins and themes pages.
24542 return;
24543 }
24544
24545 if (
24546 $this->is_activation_mode() &&
24547 $this->is_premium() &&
24548 ! $this->is_registered()
24549 ) {
24550 // If not yet registered and running the premium code base, a license activation link will already be shown.
24551 return;
24552 }
24553
24554 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24555 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
24556 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
24557 return;
24558 }
24559 }
24560
24561 if ( $this->add_ajax_action( 'toggle_permission_tracking', array( &$this, '_toggle_permission_tracking_callback' ) ) ) {
24562 return;
24563 }
24564
24565 $link_text_id = '';
24566 $url = '#';
24567
24568 if ( $this->is_registered( true ) ) {
24569 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24570 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
24571 } else {
24572 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24573 }
24574 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
24575 /**
24576 * Show opt-in link only if skipped or in activation mode.
24577 */
24578 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24579
24580 $params = ! $this->is_anonymous() ?
24581 array() :
24582 array(
24583 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
24584 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
24585 );
24586
24587 $url = $this->get_activation_url( $params );
24588 }
24589
24590 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
24591
24592 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
24593 $this->add_plugin_action_link(
24594 $link_text_id,
24595 $url,
24596 false,
24597 13,
24598 "opt-in-or-opt-out {$this->_slug}"
24599 );
24600 }
24601 }
24602
24603 /**
24604 * Get the URL of the page that should be loaded right after the plugin activation.
24605 *
24606 * @author Vova Feldman (@svovaf)
24607 * @since 1.1.7.4
24608 *
24609 * @return string
24610 */
24611 function get_after_plugin_activation_redirect_url() {
24612 $url = false;
24613
24614 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
24615 $first_time_path = $this->_menu->get_first_time_path(
24616 fs_is_network_admin() && $this->_is_network_active
24617 );
24618
24619 if ( $this->is_activation_mode() ) {
24620 $url = $this->get_activation_url();
24621 } else if ( ! empty( $first_time_path ) ) {
24622 $url = $first_time_path;
24623 } else {
24624 $page = '';
24625 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
24626 if ( $this->is_network_registered() ) {
24627 $page = 'account';
24628 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
24629 $this->maybe_set_slug_and_network_menu_exists_flag();
24630 }
24631 }
24632
24633 $url = $this->_get_admin_page_url( $page );
24634 }
24635 } else {
24636 $plugin_fs = false;
24637
24638 if ( $this->is_parent_plugin_installed() ) {
24639 $plugin_fs = self::get_parent_instance();
24640 }
24641
24642 if ( is_object( $plugin_fs ) ) {
24643 if ( ! $plugin_fs->is_registered() ) {
24644 // Forward to parent plugin connect when parent not registered.
24645 $url = $plugin_fs->get_activation_url();
24646 } else {
24647 // Forward to account page.
24648 $url = $plugin_fs->_get_admin_page_url( 'account' );
24649 }
24650 }
24651 }
24652
24653 return $url;
24654 }
24655
24656 /**
24657 * Forward page to activation page.
24658 *
24659 * @author Vova Feldman (@svovaf)
24660 * @since 1.0.3
24661 */
24662 function _redirect_on_activation_hook() {
24663 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
24664 $url = $this->get_after_plugin_activation_redirect_url();
24665
24666 if ( is_string( $url ) ) {
24667 fs_redirect( $url );
24668 }
24669 }
24670 }
24671
24672 /**
24673 * Modify plugin's page action links collection.
24674 *
24675 * @author Vova Feldman (@svovaf)
24676 * @since 1.0.0
24677 *
24678 * @param array $links
24679 * @param $file
24680 *
24681 * @return array
24682 */
24683 function _modify_plugin_action_links_hook( $links, $file ) {
24684 $this->_logger->entrance();
24685
24686 $passed_deactivate = false;
24687 $deactivate_link = '';
24688 $before_deactivate = array();
24689 $after_deactivate = array();
24690 foreach ( $links as $key => $link ) {
24691 if ( 'deactivate' === $key ) {
24692 $deactivate_link = $link;
24693 $passed_deactivate = true;
24694 continue;
24695 }
24696
24697 if ( ! $passed_deactivate ) {
24698 $before_deactivate[ $key ] = $link;
24699 } else {
24700 $after_deactivate[ $key ] = $link;
24701 }
24702 }
24703
24704 ksort( $this->_action_links );
24705
24706 foreach ( $this->_action_links as $new_links ) {
24707 foreach ( $new_links as $link ) {
24708 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
24709 }
24710 }
24711
24712 if ( ! empty( $deactivate_link ) ) {
24713 /**
24714 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
24715 *
24716 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
24717 */
24718 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
24719
24720 // Append deactivation link.
24721 $before_deactivate['deactivate'] = $deactivate_link;
24722 }
24723
24724 return array_merge( $before_deactivate, $after_deactivate );
24725 }
24726
24727 /**
24728 * Adds admin message.
24729 *
24730 * @author Vova Feldman (@svovaf)
24731 * @since 1.0.4
24732 *
24733 * @param string $message
24734 * @param string $title
24735 * @param string $type
24736 */
24737 function add_admin_message( $message, $title = '', $type = 'success' ) {
24738 $this->_admin_notices->add( $message, $title, $type );
24739 }
24740
24741 /**
24742 * Adds sticky admin message.
24743 *
24744 * @author Vova Feldman (@svovaf)
24745 * @since 1.1.0
24746 *
24747 * @param string $message
24748 * @param string $id
24749 * @param string $title
24750 * @param string $type
24751 */
24752 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
24753 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
24754 }
24755
24756 /**
24757 * Check if the paid version of the module is installed.
24758 *
24759 * @author Vova Feldman (@svovaf)
24760 * @since 2.2.0
24761 *
24762 * @return bool
24763 */
24764 private function is_premium_version_installed() {
24765 $premium_plugin_basename = $this->premium_plugin_basename();
24766
24767 if ( $this->is_theme() ) {
24768 return $this->can_activate_theme( $this->get_premium_slug() );
24769 }
24770
24771 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_plugin_basename ) );
24772 }
24773
24774 /**
24775 * Helper function that returns the final steps for the upgrade completion.
24776 *
24777 * If the module is already running the premium code, returns an empty string.
24778 *
24779 * @author Vova Feldman (@svovaf)
24780 * @since 1.2.1
24781 *
24782 * @param string $plan_title
24783 *
24784 * @return string
24785 */
24786 private function get_complete_upgrade_instructions( $plan_title = '' ) {
24787 $this->_logger->entrance();
24788
24789 $activate_license_string = $this->get_license_network_activation_notice();
24790
24791 if ( ! $this->has_premium_version() || $this->is_premium() ) {
24792 return '' . $activate_license_string;
24793 }
24794
24795 if ( empty( $plan_title ) ) {
24796 $plan_title = $this->get_plan_title();
24797 }
24798
24799 if ( $this->is_premium_version_installed() ) {
24800 /**
24801 * If the premium version is already installed, instead of showing the installation instructions,
24802 * tell the current user to activate it.
24803 *
24804 * @author Leo Fajardo (@leorw)
24805 * @since 2.2.1
24806 */
24807 $premium_theme_slug_or_plugin_basename = $this->is_theme() ?
24808 $this->get_premium_slug() :
24809 $this->premium_plugin_basename();
24810
24811 return sprintf(
24812 /* translators: %1$s: Product title; %2$s: Plan title */
24813 $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' ),
24814 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24815 $plan_title,
24816 sprintf(
24817 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
24818 ( $this->is_theme() ?
24819 wp_nonce_url( 'themes.php?action=activate&amp;stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) :
24820 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ),
24821 esc_html( sprintf(
24822 /* translators: %s: Plan title */
24823 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
24824 $plan_title
24825 ) )
24826 )
24827 );
24828 } else {
24829 // @since 1.2.1.5 The free version is auto deactivated.
24830 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
24831 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
24832 '';
24833
24834 return sprintf(
24835 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
24836 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
24837 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
24838 $this->get_latest_download_link( sprintf(
24839 /* translators: %s: Plan title */
24840 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
24841 $plan_title
24842 ) ),
24843 $deactivation_step,
24844 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
24845 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/wp-' . $this->_module_type . '-upload' ),
24846 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
24847 );
24848 }
24849 }
24850
24851 /**
24852 * @author Leo Fajardo (@leorw)
24853 * @since 2.5.3
24854 *
24855 * @param string $message_before_the_instructions
24856 * @param string $message_id
24857 * @param string $plan_title
24858 */
24859 private function add_complete_upgrade_instructions_notice(
24860 $message_before_the_instructions,
24861 $message_id,
24862 $plan_title = ''
24863 ) {
24864 $this->_admin_notices->add_sticky(
24865 $message_before_the_instructions .
24866 $this->get_complete_upgrade_instructions( $plan_title ),
24867 $message_id,
24868 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
24869 );
24870 }
24871
24872 /**
24873 * @author Leo Fajardo (@leorw)
24874 * @since 2.5.3
24875 *
24876 * @param bool $is_upgrade
24877 */
24878 private function add_after_plan_activation_or_upgrade_instructions_notice( $is_upgrade = true ) {
24879 $this->add_complete_upgrade_instructions_notice(
24880 $is_upgrade ?
24881 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) :
24882 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ),
24883 'plan_upgraded'
24884 );
24885 }
24886
24887 /**
24888 * @author Leo Fajardo (@leorw)
24889 * @since 2.1.0
24890 *
24891 * @param string $url
24892 * @param array $request
24893 */
24894 private static function enrich_request_for_debug( &$url, &$request ) {
24895 if ( WP_FS__DEBUG_SDK || isset( $_COOKIE['XDEBUG_SESSION'] ) ) {
24896 $url = add_query_arg( 'XDEBUG_SESSION_START', rand( 0, 9999999 ), $url );
24897 $url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url );
24898
24899 $request['cookies'] = array(
24900 new WP_Http_Cookie( array(
24901 'name' => 'XDEBUG_SESSION',
24902 'value' => 'PHPSTORM',
24903 ) )
24904 );
24905 }
24906 }
24907
24908 /**
24909 * @author Leo Fajardo (@leorw)
24910 * @since 2.1.0
24911 *
24912 * @param string $url
24913 * @param array $request
24914 * @param int $success_cache_expiration
24915 * @param int $failure_cache_expiration
24916 * @param bool $maybe_enrich_request_for_debug
24917 *
24918 * @return WP_Error|array
24919 */
24920 static function safe_remote_post(
24921 &$url,
24922 $request,
24923 $success_cache_expiration = 0,
24924 $failure_cache_expiration = 0,
24925 $maybe_enrich_request_for_debug = true
24926 ) {
24927 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
24928
24929 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
24930
24931 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
24932 get_transient( $cache_key ) :
24933 false;
24934
24935 if ( false === $response ) {
24936 if ( $maybe_enrich_request_for_debug ) {
24937 self::enrich_request_for_debug( $url, $request );
24938 }
24939
24940 if ( ! isset( $request['method'] ) ) {
24941 $request['method'] = 'POST';
24942 }
24943
24944 $response = FS_Api::remote_request( $url, $request );
24945
24946 if (
24947 'https://' === substr( $url, 0, 8 ) &&
24948 FS_Api::is_ssl_error_response( $response )
24949 ) {
24950 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
24951 $url = 'http://' . substr( $url, 8 );
24952
24953 $request['timeout'] = 15;
24954 $response = FS_Api::remote_request( $url, $request );
24955 }
24956
24957 if ( false !== $cache_key ) {
24958 set_transient(
24959 $cache_key,
24960 $response,
24961 ( ( $response instanceof WP_Error ) ?
24962 $failure_cache_expiration :
24963 $success_cache_expiration )
24964 );
24965 }
24966 }
24967
24968 return $response;
24969 }
24970
24971 /**
24972 * This method is used to enrich the after upgrade notice instructions when the upgraded
24973 * license cannot be activated network wide (license quota isn't large enough).
24974 *
24975 * @author Vova Feldman (@svovaf)
24976 * @since 2.0.0
24977 *
24978 * @return string
24979 */
24980 private function get_license_network_activation_notice() {
24981 if ( ! $this->_is_network_active ) {
24982 // Module isn't network level activated.
24983 return '';
24984 }
24985
24986 if ( ! fs_is_network_admin() ) {
24987 // Not network level admin.
24988 return '';
24989 }
24990
24991 if ( get_blog_count() == 1 ) {
24992 // There's only a single site in the network so if there's a context license it was already activated.
24993 return '';
24994 }
24995
24996 if ( ! is_object( $this->_license ) ) {
24997 // No context license.
24998 return '';
24999 }
25000
25001 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
25002 // 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).
25003 return '';
25004 }
25005
25006 if ( $this->can_activate_license_on_network( $this->_license ) ) {
25007 // 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).
25008 return '';
25009 }
25010
25011 return sprintf(
25012 $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' ),
25013 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
25014 '</a>'
25015 );
25016 }
25017
25018 /**
25019 * @author Vova Feldman (@svovaf)
25020 * @since 1.2.1.7
25021 *
25022 * @param string $key
25023 *
25024 * @return string
25025 */
25026 function get_text( $key ) {
25027 return fs_text( $key, $this->_slug );
25028 }
25029
25030 /**
25031 * @author Vova Feldman (@svovaf)
25032 * @since 1.2.3
25033 *
25034 * @param string $text Translatable string.
25035 * @param string $key String key for overrides.
25036 *
25037 * @return string
25038 */
25039 function get_text_inline( $text, $key = '' ) {
25040 return _fs_text_inline( $text, $key, $this->_slug );
25041 }
25042
25043 /**
25044 * @author Vova Feldman (@svovaf)
25045 * @since 1.2.3
25046 *
25047 * @param string $text Translatable string.
25048 * @param string $context Context information for the translators.
25049 * @param string $key String key for overrides.
25050 *
25051 * @return string
25052 */
25053 function get_text_x_inline( $text, $context, $key ) {
25054 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
25055 }
25056
25057 /**
25058 * @author Vova Feldman (@svovaf)
25059 * @since 1.2.3
25060 *
25061 * @param string $text Translatable string.
25062 * @param string $key String key for overrides.
25063 *
25064 * @return string
25065 */
25066 function esc_html_inline( $text, $key ) {
25067 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
25068 }
25069
25070 #----------------------------------------------------------------------------------
25071 #region Versioning
25072 #----------------------------------------------------------------------------------
25073
25074 /**
25075 * Check if Freemius in SDK upgrade mode.
25076 *
25077 * @author Vova Feldman (@svovaf)
25078 * @since 1.0.9
25079 *
25080 * @return bool
25081 */
25082 function is_sdk_upgrade_mode() {
25083 return isset( $this->_storage->sdk_upgrade_mode ) ?
25084 $this->_storage->sdk_upgrade_mode :
25085 false;
25086 }
25087
25088 /**
25089 * Turn SDK upgrade mode off.
25090 *
25091 * @author Vova Feldman (@svovaf)
25092 * @since 1.0.9
25093 */
25094 function set_sdk_upgrade_complete() {
25095 $this->_storage->sdk_upgrade_mode = false;
25096 }
25097
25098 /**
25099 * Check if plugin upgrade mode.
25100 *
25101 * @author Vova Feldman (@svovaf)
25102 * @since 1.0.9
25103 *
25104 * @return bool
25105 */
25106 function is_plugin_upgrade_mode() {
25107 return isset( $this->_storage->plugin_upgrade_mode ) ?
25108 $this->_storage->plugin_upgrade_mode :
25109 false;
25110 }
25111
25112 /**
25113 * Turn plugin upgrade mode off.
25114 *
25115 * @author Vova Feldman (@svovaf)
25116 * @since 1.0.9
25117 */
25118 function set_plugin_upgrade_complete() {
25119 $this->_storage->plugin_upgrade_mode = false;
25120
25121 $license_migration = ! empty( $this->_storage->license_migration ) ?
25122 $this->_storage->license_migration :
25123 array();
25124
25125 $license_migration['is_migrating'] = false;
25126
25127 $this->_storage->license_migration = $license_migration;
25128 }
25129
25130 #endregion
25131
25132 #----------------------------------------------------------------------------------
25133 #region Permissions
25134 #----------------------------------------------------------------------------------
25135
25136 /**
25137 * Check if specific permission requested.
25138 *
25139 * @author Vova Feldman (@svovaf)
25140 * @since 1.1.6
25141 *
25142 * @param string $permission
25143 *
25144 * @return bool
25145 */
25146 function is_permission_requested( $permission ) {
25147 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
25148 }
25149
25150 #endregion
25151
25152 #----------------------------------------------------------------------------------
25153 #region Auto Activation
25154 #----------------------------------------------------------------------------------
25155
25156 /**
25157 * Hints the SDK if running an auto-installation.
25158 *
25159 * @var bool
25160 */
25161 private $_isAutoInstall = false;
25162
25163 /**
25164 * After upgrade callback to install and auto activate a plugin.
25165 * This code will only be executed on explicit request from the user,
25166 * following the practice Jetpack are using with their theme installations.
25167 *
25168 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
25169 *
25170 * @author Vova Feldman (@svovaf)
25171 * @since 1.2.1.7
25172 */
25173 function _install_premium_version_ajax_action() {
25174 $this->_logger->entrance();
25175
25176 $this->check_ajax_referer( 'install_premium_version' );
25177
25178 if ( ! $this->is_registered() ) {
25179 // Not registered.
25180 self::shoot_ajax_failure( array(
25181 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
25182 'code' => 'premium_installed',
25183 ) );
25184 }
25185
25186 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
25187
25188 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25189 // Invalid ID.
25190 self::shoot_ajax_failure( array(
25191 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25192 'code' => 'invalid_module_id',
25193 ) );
25194 }
25195
25196 if ( $plugin_id == $this->get_id() ) {
25197 if ( $this->is_premium() ) {
25198 // Already using the premium code version.
25199 self::shoot_ajax_failure( array(
25200 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
25201 'code' => 'premium_installed',
25202 ) );
25203 }
25204 if ( ! $this->can_use_premium_code() ) {
25205 // Don't have access to the premium code.
25206 self::shoot_ajax_failure( array(
25207 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
25208 'code' => 'invalid_license',
25209 ) );
25210 }
25211 if ( ! $this->has_release_on_freemius() ) {
25212 // Plugin is a serviceware, no premium code version.
25213 self::shoot_ajax_failure( array(
25214 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
25215 'code' => 'premium_version_missing',
25216 ) );
25217 }
25218 } else {
25219 $addon = $this->get_addon( $plugin_id );
25220
25221 if ( ! is_object( $addon ) ) {
25222 // Invalid add-on ID.
25223 self::shoot_ajax_failure( array(
25224 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25225 'code' => 'invalid_module_id',
25226 ) );
25227 }
25228
25229 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25230 // Premium add-on version is already activated.
25231 self::shoot_ajax_failure( array(
25232 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
25233 'code' => 'premium_installed',
25234 ) );
25235 }
25236 }
25237
25238 $this->_isAutoInstall = true;
25239
25240 // Try to install and activate.
25241 $updater = FS_Plugin_Updater::instance( $this );
25242 $result = $updater->install_and_activate_plugin( $plugin_id );
25243
25244 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
25245 self::shoot_ajax_failure( array(
25246 'message' => $result['message'],
25247 'code' => $result['code'],
25248 ) );
25249 }
25250
25251 self::shoot_ajax_success( $result );
25252 }
25253
25254 /**
25255 * Displays module activation dialog box after a successful upgrade
25256 * where the user explicitly requested to auto download and install
25257 * the premium version.
25258 *
25259 * @author Vova Feldman (@svovaf)
25260 * @since 1.2.1.7
25261 */
25262 function _add_auto_installation_dialog_box() {
25263 $this->_logger->entrance();
25264
25265 if ( ! $this->is_registered() ) {
25266 // Not registered.
25267 return;
25268 }
25269
25270 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
25271
25272 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25273 // Invalid module ID.
25274 return;
25275 }
25276
25277 if ( $plugin_id == $this->get_id() ) {
25278 if ( $this->is_premium() ) {
25279 // Already using the premium code version.
25280 return;
25281 }
25282 if ( ! $this->can_use_premium_code() ) {
25283 // Don't have access to the premium code.
25284 return;
25285 }
25286 if ( ! $this->has_release_on_freemius() ) {
25287 // Plugin is a serviceware, no premium code version.
25288 return;
25289 }
25290 } else {
25291 $addon = $this->get_addon( $plugin_id );
25292
25293 if ( ! is_object( $addon ) ) {
25294 // Invalid add-on ID.
25295 return;
25296 }
25297
25298 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25299 // Premium add-on version is already activated.
25300 return;
25301 }
25302 }
25303
25304 $vars = array(
25305 'id' => $this->_module_id,
25306 'target_module_id' => $plugin_id,
25307 'slug' => $this->_slug,
25308 );
25309
25310 fs_require_template( 'auto-installation.php', $vars );
25311 }
25312
25313 #endregion
25314
25315 #--------------------------------------------------------------------------------
25316 #region Tabs Integration
25317 #--------------------------------------------------------------------------------
25318
25319 #region Module's Original Tabs
25320
25321 /**
25322 * Inject a JavaScript logic to capture the theme tabs HTML.
25323 *
25324 * @author Vova Feldman (@svovaf)
25325 * @since 1.2.2.7
25326 */
25327 function _tabs_capture() {
25328 $this->_logger->entrance();
25329
25330 if (
25331 ! $this->is_product_settings_page() ||
25332 ! $this->should_page_include_tabs() ||
25333 ! $this->is_matching_url( $this->main_menu_url() )
25334 ) {
25335 return;
25336 }
25337
25338 $params = array(
25339 'id' => $this->_module_id,
25340 );
25341
25342 fs_require_once_template( 'tabs-capture-js.php', $params );
25343 }
25344
25345 /**
25346 * Cache theme's tabs HTML for a week. The cache will also be set as expired
25347 * after version and type (free/premium) changes, in addition to the week period.
25348 *
25349 * @author Vova Feldman (@svovaf)
25350 * @since 1.2.2.7
25351 */
25352 function _store_tabs_ajax_action() {
25353 $this->_logger->entrance();
25354
25355 $this->check_ajax_referer( 'store_tabs' );
25356
25357 // Init filesystem if not yet initiated.
25358 WP_Filesystem();
25359
25360 // Get POST body HTML data.
25361 global $wp_filesystem;
25362 $tabs_html = $wp_filesystem->get_contents( "php://input" );
25363
25364 if ( is_string( $tabs_html ) ) {
25365 $tabs_html = trim( $tabs_html );
25366 }
25367
25368 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
25369 self::shoot_ajax_failure();
25370 }
25371
25372 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25373
25374 self::shoot_ajax_success();
25375 }
25376
25377 /**
25378 * Cache theme's settings page custom styles. The cache will also be set as expired
25379 * after version and type (free/premium) changes, in addition to the week period.
25380 *
25381 * @author Vova Feldman (@svovaf)
25382 * @since 1.2.2.7
25383 */
25384 function _store_tabs_styles() {
25385 $this->_logger->entrance();
25386
25387 if (
25388 ! $this->is_product_settings_page() ||
25389 ! $this->should_page_include_tabs() ||
25390 ! $this->is_matching_url( $this->main_menu_url() )
25391 ) {
25392 return;
25393 }
25394
25395 $wp_styles = wp_styles();
25396
25397 $theme_styles_url = get_template_directory_uri();
25398
25399 $stylesheets = array();
25400 foreach ( $wp_styles->queue as $handler ) {
25401 if ( fs_starts_with( $handler, 'fs_' ) ) {
25402 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
25403 continue;
25404 }
25405
25406 /**
25407 * @var _WP_Dependency $stylesheet
25408 */
25409 $stylesheet = $wp_styles->registered[ $handler ];
25410
25411 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
25412 $stylesheets[] = $stylesheet->src;
25413 }
25414 }
25415
25416 if ( ! empty( $stylesheets ) ) {
25417 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25418 }
25419 }
25420
25421 /**
25422 * Check if module's original settings page has any tabs.
25423 *
25424 * @author Vova Feldman (@svovaf)
25425 * @since 1.2.2.7
25426 *
25427 * @return bool
25428 */
25429 private function has_tabs() {
25430 return $this->_cache->has( 'tabs' );
25431 }
25432
25433 /**
25434 * Get module's settings page HTML content, starting
25435 * from the beginning of the <div class="wrap"> element,
25436 * until the tabs HTML (including).
25437 *
25438 * @author Vova Feldman (@svovaf)
25439 * @since 1.2.2.7
25440 *
25441 * @return string
25442 */
25443 private function get_tabs_html() {
25444 $this->_logger->entrance();
25445
25446 return $this->_cache->get( 'tabs' );
25447 }
25448
25449 /**
25450 * Check if page should include tabs.
25451 *
25452 * @author Vova Feldman (@svovaf)
25453 * @since 1.2.2.7
25454 *
25455 * @return bool
25456 */
25457 private function should_page_include_tabs() {
25458 if ( ! $this->has_settings_menu() ) {
25459 // Don't add tabs if no settings at all.
25460 return false;
25461 }
25462
25463 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
25464 // Only add tabs to themes for now.
25465 return false;
25466 }
25467
25468 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
25469 // Only add tabs to monetizing themes.
25470 return false;
25471 }
25472
25473 if ( ! $this->is_product_settings_page() ) {
25474 // Only add tabs if browsing one of the product's setting pages.
25475 return false;
25476 }
25477
25478 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
25479 // Don't include tabs in the activation page.
25480 return false;
25481 }
25482
25483 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
25484 // Don't add tabs on checkout page, we want to reduce distractions
25485 // as much as possible.
25486 return false;
25487 }
25488
25489 return true;
25490 }
25491
25492 /**
25493 * Add the tabs HTML before the setting's page content and
25494 * enqueue any required stylesheets.
25495 *
25496 * @author Vova Feldman (@svovaf)
25497 * @since 1.2.2.7
25498 *
25499 * @return bool If tabs were included.
25500 */
25501 function _add_tabs_before_content() {
25502 $this->_logger->entrance();
25503
25504 if ( ! $this->should_page_include_tabs() ) {
25505 return false;
25506 }
25507
25508 $tabs_html = $this->get_tabs_html();
25509
25510 if ( empty( $tabs_html ) ) {
25511 return false;
25512 }
25513
25514 /**
25515 * Enqueue the original stylesheets that are included in the
25516 * theme settings page. That way, if the theme settings has
25517 * some custom _styled_ content above the tabs UI, this
25518 * will make sure that the styling is preserved.
25519 */
25520 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
25521 if ( is_array( $stylesheets ) ) {
25522 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
25523 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
25524 }
25525 }
25526
25527 // Cut closing </div> tag.
25528 echo substr( trim( $tabs_html ), 0, - 6 );
25529
25530 return true;
25531 }
25532
25533 /**
25534 * Add the tabs closing HTML after the setting's page content.
25535 *
25536 * @author Vova Feldman (@svovaf)
25537 * @since 1.2.2.7
25538 *
25539 * @return bool If tabs closing HTML was included.
25540 */
25541 function _add_tabs_after_content() {
25542 $this->_logger->entrance();
25543
25544 if ( ! $this->should_page_include_tabs() ) {
25545 return false;
25546 }
25547
25548 echo '</div>';
25549
25550 return true;
25551 }
25552
25553 #endregion
25554
25555 /**
25556 * Add in-page JavaScript to inject the Freemius tabs into
25557 * the module's setting tabs section.
25558 *
25559 * @author Vova Feldman (@svovaf)
25560 * @since 1.2.2.7
25561 */
25562 function _add_freemius_tabs() {
25563 $this->_logger->entrance();
25564
25565 if ( ! $this->should_page_include_tabs() ) {
25566 return;
25567 }
25568
25569 $params = array( 'id' => $this->_module_id );
25570 fs_require_once_template( 'tabs.php', $params );
25571 }
25572
25573 #endregion
25574
25575 #--------------------------------------------------------------------------------
25576 #region Customizer Integration for Themes
25577 #--------------------------------------------------------------------------------
25578
25579 /**
25580 * @author Vova Feldman (@svovaf)
25581 * @since 1.2.2.7
25582 *
25583 * @param WP_Customize_Manager $customizer
25584 */
25585 function _customizer_register( $customizer ) {
25586 $this->_logger->entrance();
25587
25588 if ( $this->is_pricing_page_visible() ) {
25589 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
25590
25591 $customizer->add_section( 'freemius_upsell', array(
25592 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
25593 'priority' => 1,
25594 ) );
25595 $customizer->add_setting( 'freemius_upsell', array(
25596 'sanitize_callback' => 'esc_html',
25597 ) );
25598
25599 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
25600 'fs' => $this,
25601 'section' => 'freemius_upsell',
25602 'priority' => 100,
25603 ) ) );
25604 }
25605
25606 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
25607 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
25608
25609 // Main Documentation Link In Customizer Root.
25610 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
25611 'fs' => $this,
25612 'priority' => 1000,
25613 ) ) );
25614 }
25615 }
25616
25617 #endregion
25618
25619 /**
25620 * If the theme has a paid version, add some custom
25621 * styling to the theme's premium version (if exists)
25622 * to highlight that it's the premium version of the
25623 * same theme, making it easier for identification
25624 * after the user upgrades and upload it to the site.
25625 *
25626 * @author Vova Feldman (@svovaf)
25627 * @since 1.2.2.7
25628 */
25629 function _style_premium_theme() {
25630 $this->_logger->entrance();
25631
25632 if ( ! self::is_themes_page() ) {
25633 // Only include in the themes page.
25634 return;
25635 }
25636
25637 if ( ! $this->has_paid_plan() ) {
25638 // Only include if has any paid plans.
25639 return;
25640 }
25641
25642 $params = null;
25643 fs_require_once_template( '/js/jquery.content-change.php', $params );
25644
25645 $params = array(
25646 'slug' => $this->_slug,
25647 'id' => $this->_module_id,
25648 );
25649
25650 fs_require_template( '/js/style-premium-theme.php', $params );
25651 }
25652
25653 /**
25654 * This method will return the absolute URL of the module's local icon.
25655 *
25656 * When you are running your plugin or theme on a **localhost** environment, if the icon
25657 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
25658 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
25659 * If an icon is found, this method will automatically attempt to download the icon and store it
25660 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
25661 *
25662 * It's important to mention that this method is NOT phoning home since the developer will deploy
25663 * the product with the local icon in the assets folder. The download process just simplifies
25664 * the process for the developer.
25665 *
25666 * @author Vova Feldman (@svovaf)
25667 * @since 2.0.0
25668 *
25669 * @return string
25670 */
25671 function get_local_icon_url() {
25672 global $fs_active_plugins;
25673
25674 /**
25675 * @since 1.1.7.5
25676 */
25677 $local_path = $this->apply_filters( 'plugin_icon', false );
25678
25679 if ( is_string( $local_path ) ) {
25680 $icons = array( $local_path );
25681 } else {
25682 $img_dir = WP_FS__DIR_IMG;
25683
25684 // Locate the main assets folder.
25685 if ( 1 < count( $fs_active_plugins->plugins ) ) {
25686 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
25687
25688 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
25689 if ( $data->plugin_path == $this->get_plugin_basename() ) {
25690 $img_dir = $plugin_or_theme_img_dir
25691 . '/'
25692 /**
25693 * The basename will be `themes` or the basename of a custom themes directory.
25694 *
25695 * @author Leo Fajardo (@leorw)
25696 * @since 2.2.3
25697 */
25698 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
25699 . '/assets/img';
25700
25701 break;
25702 }
25703 }
25704 }
25705
25706 // Try to locate the icon in the assets folder.
25707 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
25708
25709 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
25710 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
25711 $icons = array(
25712 fs_normalize_path( $img_dir . '/theme-icon.png' )
25713 );
25714 } else {
25715 $icon_found = false;
25716 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
25717
25718 if ( ! function_exists( 'get_filesystem_method' ) ) {
25719 require_once ABSPATH . 'wp-admin/includes/file.php';
25720 }
25721
25722 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
25723
25724 /**
25725 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
25726 *
25727 * This code will only be executed once during the testing
25728 * of the plugin in a local environment. The plugin icon file WILL
25729 * already exist in the assets folder when the plugin is deployed to
25730 * the repository.
25731 */
25732 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
25733 // Fetch icon from Freemius.
25734 $icon = $this->fetch_remote_icon_url();
25735
25736 // Fetch icon from WordPress.org.
25737 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
25738 if ( ! function_exists( 'plugins_api' ) ) {
25739 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
25740 }
25741
25742 $plugin_information = plugins_api( 'plugin_information', array(
25743 'slug' => $this->_slug,
25744 'fields' => array(
25745 'sections' => false,
25746 'tags' => false,
25747 'icons' => true
25748 )
25749 ) );
25750
25751 if (
25752 ! is_wp_error( $plugin_information )
25753 && isset( $plugin_information->icons )
25754 && ! empty( $plugin_information->icons )
25755 ) {
25756 /**
25757 * Get the smallest icon.
25758 *
25759 * @author Leo Fajardo (@leorw)
25760 * @since 1.2.2
25761 */
25762 $icon = end( $plugin_information->icons );
25763 }
25764 }
25765
25766 if ( ! empty( $icon ) ) {
25767 if ( 0 !== strpos( $icon, 'http' ) ) {
25768 $icon = 'http:' . $icon;
25769 }
25770
25771 /**
25772 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
25773 *
25774 * @author Leo Fajardo (@leorw)
25775 * @since 1.2.2
25776 */
25777 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
25778
25779 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
25780
25781 // Try to download the icon.
25782 $icon_found = fs_download_image( $icon, $local_path );
25783 }
25784 }
25785
25786 if ( ! $icon_found ) {
25787 // No icons found, fallback to default icon.
25788 if ( $have_write_permissions ) {
25789 // If have write permissions, copy default icon.
25790 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
25791 } else {
25792 // If doesn't have write permissions, use default icon path.
25793 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
25794 }
25795 }
25796
25797 $icons = array( $local_path );
25798 }
25799 }
25800 }
25801
25802 $icon_dir = dirname( $icons[0] );
25803
25804 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
25805 }
25806
25807 /**
25808 * Fetch module's extended info.
25809 *
25810 * @author Vova Feldman (@svovaf)
25811 * @since 2.0.0
25812 *
25813 * @return object|mixed
25814 */
25815 private function fetch_module_info() {
25816 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
25817 }
25818
25819 /**
25820 * Fetch module's remote icon URL.
25821 *
25822 * @author Vova Feldman (@svovaf)
25823 * @since 2.0.0
25824 *
25825 * @return string
25826 */
25827 function fetch_remote_icon_url() {
25828 $info = $this->fetch_module_info();
25829
25830 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
25831 $info->icon :
25832 '';
25833 }
25834
25835 #--------------------------------------------------------------------------------
25836 #region GDPR
25837 #--------------------------------------------------------------------------------
25838
25839 /**
25840 * @author Leo Fajardo (@leorw)
25841 * @since 2.1.0
25842 *
25843 * @param array $user_plugins
25844 *
25845 * @return string
25846 */
25847 private function get_gdpr_admin_notice_string( $user_plugins ) {
25848 $this->_logger->entrance();
25849
25850 $addons = self::get_all_addons();
25851
25852 foreach ( $user_plugins as $user_plugin ) {
25853 $has_addons = isset( $addons[ $user_plugin->id ] );
25854
25855 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
25856 if ( $this->_module_id == $user_plugin->id ) {
25857 $addons = $this->get_addons();
25858 $has_addons = ( ! empty( $addons ) );
25859 } else {
25860 $plugin_api = FS_Api::instance(
25861 $user_plugin->id,
25862 'plugin',
25863 $user_plugin->id,
25864 $user_plugin->public_key,
25865 ! $user_plugin->is_live,
25866 false,
25867 $this->get_sdk_version()
25868 );
25869
25870 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
25871
25872 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
25873 is_array( $addons_result->plugins ) &&
25874 ! empty( $addons_result->plugins )
25875 ) {
25876 $has_addons = true;
25877 }
25878 }
25879 }
25880
25881 $user_plugin->has_addons = $has_addons;
25882 }
25883
25884 $is_single_parent_product = ( 1 === count( $user_plugins ) );
25885
25886 $multiple_products_text = '';
25887
25888 if ( $is_single_parent_product ) {
25889 $single_parent_product = reset( $user_plugins );
25890
25891 $thank_you = sprintf(
25892 "<span data-plugin-id='%d'>%s</span>",
25893 $single_parent_product->id,
25894 sprintf(
25895 $single_parent_product->has_addons ?
25896 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
25897 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
25898 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
25899 )
25900 );
25901
25902 $already_opted_in = sprintf(
25903 $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' ),
25904 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
25905 );
25906 } else {
25907 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
25908 $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' );
25909
25910 $products_and_add_ons = '';
25911 foreach ( $user_plugins as $user_plugin ) {
25912 if ( ! empty( $products_and_add_ons ) ) {
25913 $products_and_add_ons .= ', ';
25914 }
25915
25916 if ( ! $user_plugin->has_addons ) {
25917 $products_and_add_ons .= sprintf(
25918 "<span data-plugin-id='%d'>%s</span>",
25919 $user_plugin->id,
25920 $user_plugin->title
25921 );
25922 } else {
25923 $products_and_add_ons .= sprintf(
25924 "<span data-plugin-id='%d'>%s</span>",
25925 $user_plugin->id,
25926 sprintf(
25927 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
25928 $user_plugin->title
25929 )
25930 );
25931 }
25932 }
25933
25934 $multiple_products_text = sprintf(
25935 "<small class='products'><strong>%s:</strong> %s</small>",
25936 $this->get_text_inline( 'Products', 'products' ),
25937 $products_and_add_ons
25938 );
25939 }
25940
25941 $actions = sprintf(
25942 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
25943 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
25944 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
25945 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
25946 sprintf(
25947 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
25948 '<span class="underlined">',
25949 '</span>'
25950 )
25951 );
25952
25953 return sprintf(
25954 '%s %s %s',
25955 $thank_you,
25956 $already_opted_in,
25957 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>' ) .
25958 '<br><br>' .
25959 '<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>' .
25960 $actions .
25961 ( $is_single_parent_product ? '' : $multiple_products_text )
25962 );
25963 }
25964
25965 /**
25966 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
25967 * plugins and themes they've opted in to.
25968 *
25969 * @author Leo Fajardo (@leorw)
25970 * @since 2.1.0
25971 *
25972 * @param string $user_email
25973 * @param string $license_key
25974 * @param array $plugin_ids
25975 * @param string|null $license_key
25976 *
25977 * @return array|false
25978 */
25979 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
25980 $request = array(
25981 'method' => 'POST',
25982 'body' => array(),
25983 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
25984 );
25985
25986 if ( is_string( $user_email ) ) {
25987 $request['body']['email'] = $user_email;
25988 } else {
25989 $request['body']['license_key'] = $license_key;
25990 }
25991
25992 $result = array();
25993
25994 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
25995 $total_plugin_ids = count( $plugin_ids );
25996
25997 $plugin_ids_count_per_request = 10;
25998 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
25999 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
26000
26001 $request['body']['plugin_ids'] = $plugin_ids_set;
26002
26003 $response = self::safe_remote_post(
26004 $url,
26005 $request,
26006 WP_FS__TIME_24_HOURS_IN_SEC,
26007 WP_FS__TIME_12_HOURS_IN_SEC
26008 );
26009
26010 if ( ! is_wp_error( $response ) ) {
26011 $decoded = is_string( $response['body'] ) ?
26012 json_decode( $response['body'] ) :
26013 null;
26014
26015 if (
26016 !is_object($decoded) ||
26017 !isset($decoded->success) ||
26018 true !== $decoded->success ||
26019 !isset( $decoded->data ) ||
26020 !is_array( $decoded->data )
26021 ) {
26022 return false;
26023 }
26024
26025 $result = array_merge( $result, $decoded->data );
26026 }
26027 }
26028
26029 return $result;
26030 }
26031
26032 /**
26033 * @author Leo Fajardo (@leorw)
26034 * @since 2.1.0
26035 */
26036 function _maybe_show_gdpr_admin_notice() {
26037 if ( ! $this->is_user_in_admin() ) {
26038 return;
26039 }
26040
26041 if ( ! $this->should_handle_gdpr_admin_notice() ) {
26042 return;
26043 }
26044
26045 if ( ! $this->is_user_admin() ) {
26046 return;
26047 }
26048
26049 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26050
26051 $lock = FS_User_Lock::instance();
26052
26053 /**
26054 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
26055 */
26056 if ( ! $lock->try_lock( 60 ) ) {
26057 return;
26058 }
26059
26060 /**
26061 * @var $current_wp_user WP_User
26062 */
26063 $current_wp_user = self::_get_current_wp_user();
26064
26065 /**
26066 * @var FS_User $current_fs_user
26067 */
26068 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
26069
26070 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
26071
26072 if ( ! is_object( $current_fs_user ) ) {
26073 // 10-year lock.
26074 $lock->lock( $ten_years_in_sec );
26075
26076 return;
26077 }
26078
26079 $gdpr = FS_GDPR_Manager::instance();
26080
26081 if ( $gdpr->is_opt_in_notice_shown() ) {
26082 // 30-day lock.
26083 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26084
26085 return;
26086 }
26087
26088 if ( ! $gdpr->should_show_opt_in_notice() ) {
26089 // 10-year lock.
26090 $lock->lock( $ten_years_in_sec );
26091
26092 return;
26093 }
26094
26095 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
26096 $was_notice_shown_before = ( false !== $last_time_notice_shown );
26097
26098 if ( $was_notice_shown_before &&
26099 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
26100 ) {
26101 // If the notice was shown before, show it again after 30 days from the last time it was shown.
26102 return;
26103 }
26104
26105 /**
26106 * Find all plugin IDs that were installed by the current admin.
26107 */
26108 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
26109
26110 if ( empty( $plugin_ids_map )) {
26111 $lock->lock( $ten_years_in_sec );
26112
26113 return;
26114 }
26115
26116 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26117 $current_fs_user->email,
26118 null,
26119 array_keys( $plugin_ids_map )
26120 );
26121
26122 if ( empty( $user_plugins ) ) {
26123 $lock->lock(
26124 is_array($user_plugins) ?
26125 $ten_years_in_sec :
26126 // Lock for 24-hours on errors.
26127 WP_FS__TIME_24_HOURS_IN_SEC
26128 );
26129
26130 return;
26131 }
26132
26133 $has_unset_marketing_optin = false;
26134
26135 foreach ( $user_plugins as $user_plugin ) {
26136 if ( true == $user_plugin->is_marketing_allowed ) {
26137 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
26138 }
26139
26140 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
26141 $has_unset_marketing_optin = true;
26142 }
26143 }
26144
26145 if ( empty( $plugin_ids_map ) ||
26146 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
26147 ) {
26148 $lock->lock( $ten_years_in_sec );
26149
26150 return;
26151 }
26152
26153 $modules = array_merge(
26154 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26155 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26156 );
26157
26158 foreach ( $modules as $module ) {
26159 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
26160 $plugin_ids_map[ $module->id ] = $module;
26161 }
26162 }
26163
26164 $plugin_title = null;
26165 if ( 1 === count( $plugin_ids_map ) ) {
26166 $module = reset( $plugin_ids_map );
26167 $plugin_title = $module->title;
26168 }
26169
26170 $gdpr->add_opt_in_sticky_notice(
26171 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
26172 $plugin_title
26173 );
26174
26175 $this->add_gdpr_optin_ajax_handler_and_style();
26176
26177 $gdpr->notice_was_just_shown();
26178
26179 // 30-day lock.
26180 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26181 }
26182
26183 /**
26184 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
26185 * marketing.
26186 *
26187 * @author Leo Fajardo (@leorw)
26188 * @since 2.1.0
26189 */
26190 private function disable_opt_in_notice_and_lock_user() {
26191 FS_GDPR_Manager::instance()->disable_opt_in_notice();
26192
26193 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26194
26195 // 10-year lock.
26196 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26197 }
26198
26199 /**
26200 * @author Leo Fajardo (@leorw)
26201 * @since 2.5.4
26202 */
26203 static function _add_api_connectivity_notice_handler_js() {
26204 fs_require_once_template( 'api-connectivity-message-js.php' );
26205 }
26206
26207 /**
26208 * @author Leo Fajardo (@leorw)
26209 * @since 2.1.0
26210 */
26211 function _add_gdpr_optin_js() {
26212 $vars = array( 'id' => $this->_module_id );
26213
26214 fs_require_once_template( 'gdpr-optin-js.php', $vars );
26215 }
26216
26217 /**
26218 * @author Leo Fajardo (@leorw)
26219 * @since 2.1.0
26220 */
26221 function enqueue_gdpr_optin_notice_style() {
26222 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
26223 }
26224
26225 /**
26226 * @author Leo Fajardo (@leorw)
26227 * @since 2.1.0
26228 */
26229 function _maybe_add_gdpr_optin_ajax_handler() {
26230 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
26231
26232 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
26233 $this->add_gdpr_optin_ajax_handler_and_style();
26234 }
26235 }
26236
26237 /**
26238 * @author Leo Fajardo (@leorw)
26239 * @since 2.1.0
26240 */
26241 function _fetch_is_marketing_required_flag_value_ajax_action() {
26242 $this->_logger->entrance();
26243
26244 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
26245
26246 $license_key = fs_request_get_raw( 'license_key' );
26247
26248 if ( empty($license_key) ) {
26249 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
26250 }
26251
26252 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26253 null,
26254 $license_key,
26255 array( $this->_module_id )
26256 );
26257
26258 if ( ! is_array( $user_plugins ) ||
26259 empty($user_plugins) ||
26260 !isset($user_plugins[0]->plugin_id) ||
26261 $user_plugins[0]->plugin_id != $this->_module_id
26262 ) {
26263 /**
26264 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
26265 *
26266 * @author Vova Feldman (@svovaf)
26267 */
26268 self::shoot_ajax_success( array(
26269 'is_marketing_allowed' => null,
26270 'license_owner_id' => null
26271 ) );
26272 }
26273
26274 self::shoot_ajax_success( array(
26275 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
26276 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
26277 ) );
26278 }
26279
26280 /**
26281 * @author Leo Fajardo (@leorw)
26282 * @since 2.3.2
26283 *
26284 * @param number[] $install_ids
26285 *
26286 * @return array {
26287 * An array of objects containing the installs' licenses owners data.
26288 *
26289 * @property number $id User ID.
26290 * @property string $email User email (can be masked email).
26291 * }
26292 */
26293 private function fetch_installs_licenses_owners_data( $install_ids ) {
26294 $this->_logger->entrance();
26295
26296 $response = $this->get_api_user_scope()->get(
26297 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
26298 );
26299
26300 $license_owners = array();
26301
26302 if ( $this->is_api_result_object( $response, 'owners' ) ) {
26303 $license_owners = $response->owners;
26304 }
26305
26306 return $license_owners;
26307 }
26308
26309 /**
26310 * @author Leo Fajardo (@leorw)
26311 * @since 2.1.0
26312 */
26313 private function add_gdpr_optin_ajax_handler_and_style() {
26314 // Add GDPR action AJAX callback.
26315 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
26316
26317 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
26318 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
26319 }
26320
26321 /**
26322 * @author Leo Fajardo (@leorw)
26323 * @since 2.1.0
26324 */
26325 function _gdpr_optin_ajax_action() {
26326 $this->_logger->entrance();
26327
26328 $this->check_ajax_referer( 'gdpr_optin_action' );
26329
26330 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
26331 self::shoot_ajax_failure();
26332 }
26333
26334 $current_wp_user = self::_get_current_wp_user();
26335
26336 $plugin_ids = fs_request_get( 'plugin_ids', array() );
26337 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
26338 self::shoot_ajax_failure();
26339 }
26340
26341 $modules = array_merge(
26342 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26343 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26344 );
26345
26346 foreach ( $modules as $key => $module ) {
26347 if ( ! in_array( $module->id, $plugin_ids ) ) {
26348 unset( $modules[ $key ] );
26349 }
26350 }
26351
26352 if ( empty( $modules ) ) {
26353 self::shoot_ajax_failure();
26354 }
26355
26356 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
26357
26358 foreach ( $modules as $module ) {
26359 $user_api->call( "?plugin_id={$module->id}", 'put', array(
26360 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
26361 ) );
26362 }
26363
26364 FS_GDPR_Manager::instance()->remove_opt_in_notice();
26365
26366 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26367
26368 // 10-year lock.
26369 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26370
26371 self::shoot_ajax_success();
26372 }
26373
26374 /**
26375 * 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.
26376 *
26377 * @author Vova Feldman (@svovaf)
26378 * @since 2.1.0
26379 *
26380 * @return bool
26381 */
26382 private function should_handle_gdpr_admin_notice() {
26383 return $this->apply_filters(
26384 'handle_gdpr_admin_notice',
26385 // Default to false.
26386 false
26387 );
26388 }
26389
26390 #endregion
26391
26392 #----------------------------------------------------------------------------------
26393 #region Marketing
26394 #----------------------------------------------------------------------------------
26395
26396 /**
26397 * Check if current user purchased any other plugins before.
26398 *
26399 * @author Vova Feldman (@svovaf)
26400 * @since 1.0.9
26401 *
26402 * @return bool
26403 */
26404 function has_purchased_before() {
26405 // TODO: Implement has_purchased_before() method.
26406 throw new Exception( 'not implemented' );
26407 }
26408
26409 /**
26410 * Check if current user classified as an agency.
26411 *
26412 * @author Vova Feldman (@svovaf)
26413 * @since 1.0.9
26414 *
26415 * @return bool
26416 */
26417 function is_agency() {
26418 // TODO: Implement is_agency() method.
26419 throw new Exception( 'not implemented' );
26420 }
26421
26422 /**
26423 * Check if current user classified as a developer.
26424 *
26425 * @author Vova Feldman (@svovaf)
26426 * @since 1.0.9
26427 *
26428 * @return bool
26429 */
26430 function is_developer() {
26431 // TODO: Implement is_developer() method.
26432 throw new Exception( 'not implemented' );
26433 }
26434
26435 /**
26436 * Check if current user classified as a business.
26437 *
26438 * @author Vova Feldman (@svovaf)
26439 * @since 1.0.9
26440 *
26441 * @return bool
26442 */
26443 function is_business() {
26444 // TODO: Implement is_business() method.
26445 throw new Exception( 'not implemented' );
26446 }
26447
26448 #endregion
26449
26450 #----------------------------------------------------------------------------------
26451 #region Helper
26452 #----------------------------------------------------------------------------------
26453
26454 /**
26455 * If running with a secret key, assume it's the developer and show pending plans as well.
26456 *
26457 * @author Vova Feldman (@svovaf)
26458 * @since 2.1.2
26459 *
26460 * @param string $path
26461 *
26462 * @return string
26463 */
26464 function add_show_pending( $path ) {
26465 if ( ! $this->has_secret_key() ) {
26466 return $path;
26467 }
26468
26469 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
26470 }
26471
26472 #endregion
26473 }
26474