PluginProbe
Forumax – AI Powered Advanced Community Forum Plugin / 2.5.1
Forumax – AI Powered Advanced Community Forum Plugin v2.5.1
2.5.1 2.5.0 2.4.4 2.4.3 2.4.2 2.4.1 2.4.0 trunk 1.0.8 1.1.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.4.1 All 31 releases
bbp-core / vendor / freemius / wordpress-sdk / includes / class-freemius.php

class-freemius.php in Forumax – AI Powered Advanced Community Forum Plugin 2.5.1, at vendor/freemius/wordpress-sdk/includes/class-freemius.php

26,376 lines 992.7 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 2.9.1
114 * @var string|null Hints the SDK whether the plugin supports parallel activation mode, preventing the auto-deactivation of the free version when the premium version is activated, and vice versa.
115 */
116 private $_premium_plugin_basename_from_parallel_activation;
117
118 /**
119 * @since 1.1.7.5
120 * @var bool Hints the SDK if plugin should run in anonymous mode (only adds feedback form).
121 */
122 private $_anonymous_mode;
123
124 /**
125 * @since 1.1.9
126 * @var bool Hints the SDK if plugin have any free plans.
127 */
128 private $_is_premium_only;
129
130 /**
131 * @since 1.2.1.6
132 * @var bool Hints the SDK if plugin have premium code version at all.
133 */
134 private $_has_premium_version;
135
136 /**
137 * @since 1.2.1.6
138 * @var bool Hints the SDK if plugin should ignore pending mode by simulating a skip.
139 */
140 private $_ignore_pending_mode;
141
142 /**
143 * @since 1.0.8
144 * @var bool Hints the SDK if the plugin has any paid plans.
145 */
146 private $_has_paid_plans;
147
148 /**
149 * @since 1.2.1.5
150 * @var int Hints the SDK if the plugin offers a trial period. If negative, no trial, if zero - has a trial but
151 * without a specified period, if positive - the number of trial days.
152 */
153 private $_trial_days = - 1;
154
155 /**
156 * @since 1.2.1.5
157 * @var bool Hints the SDK if the trial requires a payment method or not.
158 */
159 private $_is_trial_require_payment = false;
160
161 /**
162 * @since 1.0.7
163 * @var bool Hints the SDK if the plugin is WordPress.org compliant.
164 */
165 private $_is_org_compliant;
166
167 /**
168 * @since 1.0.7
169 * @var bool Hints the SDK if the plugin is has add-ons.
170 */
171 private $_has_addons;
172
173 /**
174 * @since 2.4.5
175 * @var string Navigation type: 'menu' or 'tabs'.
176 */
177 private $_navigation;
178
179 const NAVIGATION_MENU = 'menu';
180 const NAVIGATION_TABS = 'tabs';
181
182 /**
183 * @since 1.1.6
184 * @var string[]bool.
185 */
186 private $_permissions;
187
188 /**
189 * @var FS_Storage
190 */
191 private $_storage;
192
193 /**
194 * @since 1.2.2.7
195 * @var FS_Cache_Manager
196 */
197 private $_cache;
198
199 /**
200 * @since 1.0.0
201 *
202 * @var FS_Logger
203 */
204 private $_logger;
205 /**
206 * @since 1.0.4
207 *
208 * @var FS_Plugin
209 */
210 private $_plugin = false;
211 /**
212 * @since 1.0.4
213 *
214 * @var FS_Plugin|false
215 */
216 private $_parent_plugin = false;
217 /**
218 * @since 1.1.1
219 *
220 * @var Freemius
221 */
222 private $_parent = false;
223 /**
224 * @since 1.0.1
225 *
226 * @var FS_User
227 */
228 private $_user = false;
229 /**
230 * @since 1.0.1
231 *
232 * @var FS_Site
233 */
234 private $_site = false;
235 /**
236 * @since 1.0.1
237 *
238 * @var FS_Plugin_License
239 */
240 private $_license;
241 /**
242 * @since 1.0.2
243 *
244 * @var FS_Plugin_Plan[]
245 */
246 private $_plans = false;
247 /**
248 * @var FS_Plugin_License[]
249 * @since 1.0.5
250 */
251 private $_licenses = false;
252
253 /**
254 * @since 1.0.1
255 *
256 * @var FS_Admin_Menu_Manager
257 */
258 private $_menu;
259
260 /**
261 * @var FS_Admin_Notices
262 */
263 private $_admin_notices;
264
265 /**
266 * @since 1.1.6
267 *
268 * @var FS_Admin_Notices
269 */
270 private static $_global_admin_notices;
271
272 /**
273 * @var FS_Logger
274 * @since 1.0.0
275 */
276 private static $_static_logger;
277
278 /**
279 * @var FS_Options
280 * @since 1.0.2
281 */
282 private static $_accounts;
283
284 /**
285 * @since 1.2.2
286 *
287 * @var number
288 */
289 private $_module_id;
290
291 /**
292 * @var Freemius[]
293 */
294 private static $_instances = array();
295
296 /**
297 * @since 1.2.3
298 *
299 * @var FS_Affiliate
300 */
301 private $affiliate = null;
302
303 /**
304 * @since 1.2.3
305 *
306 * @var FS_AffiliateTerms
307 */
308 private $plugin_affiliate_terms = null;
309
310 /**
311 * @since 1.2.3
312 *
313 * @var FS_AffiliateTerms
314 */
315 private $custom_affiliate_terms = null;
316
317 /**
318 * @since 2.0.0
319 *
320 * @var bool
321 */
322 private $_is_multisite_integrated;
323
324 /**
325 * @since 2.0.0
326 *
327 * @var bool True if the current request is for a network admin screen and the plugin is network active.
328 */
329 private $_is_network_active;
330
331 /**
332 * @since 2.0.0
333 *
334 * @var int|null The original blog ID the plugin was loaded with.
335 */
336 private $_blog_id = null;
337
338 /**
339 * @since 2.0.0
340 *
341 * @var int|null The current execution context. When true, run on network context. When int, run on the specified blog context.
342 */
343 private $_context_is_network_or_blog_id = null;
344
345 /**
346 * @since 2.0.0
347 *
348 * @var string
349 */
350 private $_dynamically_added_top_level_page_hook_name = '';
351
352 /**
353 * @author Leo Fajardo (@leorw)
354 * @since 2.3.1
355 *
356 * @var bool
357 */
358 private $is_whitelabeled;
359
360 /**
361 * @author Leo Fajardo (@leorw)
362 * @since 2.4.0
363 *
364 * @var bool
365 */
366 private $_is_bundle_license_auto_activation_enabled = false;
367
368 #region Uninstall Reasons IDs
369
370 const REASON_NO_LONGER_NEEDED = 1;
371 const REASON_FOUND_A_BETTER_PLUGIN = 2;
372 const REASON_NEEDED_FOR_A_SHORT_PERIOD = 3;
373 const REASON_BROKE_MY_SITE = 4;
374 const REASON_SUDDENLY_STOPPED_WORKING = 5;
375 const REASON_CANT_PAY_ANYMORE = 6;
376 const REASON_OTHER = 7;
377 const REASON_DIDNT_WORK = 8;
378 const REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION = 9;
379 const REASON_COULDNT_MAKE_IT_WORK = 10;
380 const REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE = 11;
381 const REASON_NOT_WORKING = 12;
382 const REASON_NOT_WHAT_I_WAS_LOOKING_FOR = 13;
383 const REASON_DIDNT_WORK_AS_EXPECTED = 14;
384 const REASON_TEMPORARY_DEACTIVATION = 15;
385
386 #endregion
387
388 /**
389 * @author Leo Fajardo (@leorw)
390 * @since 2.4.2
391 *
392 * @var string|null
393 */
394 private $_pricing_js_path = null;
395
396 const VERSION_MAX_CHARS = 16;
397 const LANGUAGE_MAX_CHARS = 8;
398
399 /* Ctor
400 ------------------------------------------------------------------------------------------------------------------*/
401
402 /**
403 * Main singleton instance.
404 *
405 * @author Vova Feldman (@svovaf)
406 * @since 1.0.0
407 *
408 * @param number $module_id
409 * @param string|bool $slug
410 * @param bool $is_init Since 1.2.1 Is initiation sequence.
411 */
412 private function __construct( $module_id, $slug = false, $is_init = false ) {
413 $main_file = false;
414
415 if ( $is_init && is_numeric( $module_id ) && is_string( $slug ) ) {
416 $main_file = $this->store_id_slug_type_path_map( $module_id, $slug );
417 }
418
419 $this->_module_id = $module_id;
420 $this->_slug = $this->get_slug();
421 $this->_module_type = $this->get_module_type();
422
423 $this->_blog_id = is_multisite() ? get_current_blog_id() : null;
424
425 $this->_storage = FS_Storage::instance( $this->_module_type, $this->_slug );
426
427 // 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.
428 if (
429 ! isset( $this->_storage->last_load_timestamp ) ||
430 $this->_storage->last_load_timestamp < ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC ) )
431 ) {
432 $this->_storage->last_load_timestamp = time();
433 }
434
435 $this->_cache = FS_Cache_Manager::get_manager( WP_FS___OPTION_PREFIX . "cache_{$module_id}" );
436
437 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $this->get_unique_affix(), WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
438
439 $this->_plugin_main_file_path = $this->_find_caller_plugin_file( $is_init, $main_file );
440 $this->_plugin_dir_path = plugin_dir_path( $this->_plugin_main_file_path );
441 $this->_plugin_basename = $this->get_plugin_basename();
442 $this->_free_plugin_basename = str_replace( '-premium/', '/', $this->_plugin_basename );
443
444 $this->_is_multisite_integrated = (
445 defined( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) &&
446 ( true === constant( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) )
447 );
448
449 $this->_is_network_active = (
450 is_multisite() &&
451 $this->_is_multisite_integrated &&
452 // Themes are always network activated, but the ACTUAL activation is per site.
453 $this->is_plugin() &&
454 (
455 is_plugin_active_for_network( $this->_plugin_basename ) ||
456 // Plugin network level activation or uninstall.
457 ( fs_is_network_admin() && is_plugin_inactive( $this->_plugin_basename ) )
458 )
459 );
460
461 $this->_storage->set_network_active(
462 $this->_is_network_active,
463 $this->is_delegated_connection()
464 );
465
466 if ( ! isset( $this->_storage->is_network_activated ) ) {
467 $this->_storage->is_network_activated = $this->_is_network_active;
468 }
469
470 if ( $this->_storage->is_network_activated != $this->_is_network_active ) {
471 // Update last activation level.
472 $this->_storage->is_network_activated = $this->_is_network_active;
473
474 $this->maybe_adjust_storage();
475 }
476
477 #region Migration
478
479 if ( is_multisite() ) {
480 /**
481 * If the install_timestamp exists on the site level but doesn't exist on the
482 * network level storage, it means that we need to process the storage with migration.
483 *
484 * 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.
485 *
486 * @author Vova Feldman (@svovaf)
487 * @since 2.0.0
488 */
489 if ( false === $this->_storage->get( 'install_timestamp', false, true ) &&
490 false !== $this->_storage->get( 'install_timestamp', false, false )
491 ) {
492 // Initiate storage migration.
493 $this->_storage->migrate_to_network();
494
495 // Migrate module cache to network level storage.
496 $this->_cache->migrate_to_network();
497 }
498 }
499
500 #endregion
501
502 $base_name_split = explode( '/', $this->_plugin_basename );
503 $this->_plugin_dir_name = $base_name_split[0];
504
505 if ( $this->_logger->is_on() ) {
506 $this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
507 $this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
508 $this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
509 $this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
510 $this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
511 }
512
513 // Remember link between file to slug.
514 $this->store_file_slug_map();
515
516 // Store plugin's initial install timestamp.
517 if ( ! isset( $this->_storage->install_timestamp ) ) {
518 $this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
519 }
520
521 if ( ! is_object( $this->_plugin ) ) {
522 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
523 }
524
525 $this->_admin_notices = FS_Admin_Notices::instance(
526 $this->_slug . ( $this->is_theme() ? ':theme' : '' ),
527 /**
528 * Ensure that the admin notice will always have a title by using the stored plugin title if available and
529 * retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
530 *
531 * @author Leo Fajardo (@leorw)
532 * @since 1.2.2
533 */
534 ( is_object( $this->_plugin ) && isset( $this->_plugin->title ) ?
535 $this->_plugin->title :
536 $this->get_plugin_name()
537 ),
538 $this->get_unique_affix()
539 );
540
541 if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
542 fs_request_is_action( 'restart_freemius' )
543 ) {
544 FS_Api::clear_cache();
545 $this->_cache->clear();
546 }
547
548 $this->register_constructor_hooks();
549
550 /**
551 * Starting from version 2.0.0, `FS_Site` entities no longer have the `plan` property and have `plan_id`
552 * instead. This should be called before calling `_load_account()`, otherwise, `$this->_site` will not be
553 * loaded in `_load_account` for versions of SDK starting from 2.0.0.
554 *
555 * @author Leo Fajardo (@leorw)
556 */
557 self::migrate_install_plan_to_plan_id( $this->_storage );
558
559 $this->_load_account();
560
561 $this->_version_updates_handler();
562 }
563
564 /**
565 * @author Leo Fajardo (@leorw)
566 * @since 2.3.0
567 */
568 private function maybe_adjust_storage() {
569 $install_timestamp = null;
570 $prev_is_premium = null;
571
572 $options_to_update = array();
573
574 $is_network_admin = fs_is_network_admin();
575
576 $network_install_timestamp = $this->_storage->get( 'install_timestamp', null, true );
577
578 if ( ! $is_network_admin ) {
579 if ( is_null( $network_install_timestamp ) ) {
580 // Plugin was not network-activated before.
581 return;
582 }
583
584 if ( is_null( $this->_storage->get( 'install_timestamp', null, false ) ) ) {
585 // Set the `install_timestamp` only if it's not yet set.
586 $install_timestamp = $network_install_timestamp;
587 }
588
589 $prev_is_premium = $this->_storage->get( 'prev_is_premium', null, true );
590 } else {
591 $current_wp_user = self::_get_current_wp_user();
592 $current_fs_user = self::_get_user_by_email( $current_wp_user->user_email );
593 $network_user_info = array();
594
595 $skips_count = 0;
596
597 $sites = self::get_sites();
598 $sites_count = count( $sites );
599
600 $blog_id_2_install_map = array();
601
602 $is_first_non_ignored_blog = true;
603
604 foreach ( $sites as $site ) {
605 $blog_id = self::get_site_blog_id( $site );
606
607 $blog_install_timestamp = $this->_storage->get( 'install_timestamp', null, $blog_id );
608
609 if ( is_null( $blog_install_timestamp ) ) {
610 // Plugin has not been installed on this blog.
611 continue;
612 }
613
614 $is_earlier_install = (
615 ! is_null( $install_timestamp ) &&
616 $blog_install_timestamp < $install_timestamp
617 );
618
619 $install = $this->get_install_by_blog_id( $blog_id );
620
621 $update_network_user_info = false;
622
623 if ( ! is_object( $install ) ) {
624 if ( ! $this->_storage->get( 'is_anonymous', false, $blog_id ) ) {
625 // The opt-in decision (whether to skip or opt in) is yet to be made.
626 continue;
627 }
628
629 $skips_count ++;
630 } else {
631 $blog_id_2_install_map[ $blog_id ] = $install;
632
633 if ( empty( $network_user_info ) ) {
634 // Set the network user info for the 1st time. Choose any user information whether or not it is for the current WP user.
635 $update_network_user_info = true;
636 }
637
638 if ( ! $update_network_user_info &&
639 is_object( $current_fs_user ) &&
640 $network_user_info['user_id'] != $current_fs_user->id &&
641 $install->user_id == $current_fs_user->id
642 ) {
643 // If an install that is owned by the current WP user is found, use its user information instead.
644 $update_network_user_info = true;
645 }
646
647 if ( ! $update_network_user_info &&
648 $is_earlier_install &&
649 ( ! is_object( $current_fs_user ) || $current_fs_user->id == $install->user_id )
650 ) {
651 // 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.
652 $update_network_user_info = true;
653 }
654 }
655
656 if ( $update_network_user_info ) {
657 $network_user_info = array(
658 'user_id' => $install->user_id,
659 'blog_id' => $blog_id
660 );
661 }
662
663 $site_prev_is_premium = $this->_storage->get( 'prev_is_premium', null, $blog_id );
664
665 if ( $is_first_non_ignored_blog ) {
666 $prev_is_premium = $site_prev_is_premium;
667
668 if ( is_null( $network_install_timestamp ) ) {
669 $install_timestamp = $blog_install_timestamp;
670 }
671
672 $is_first_non_ignored_blog = false;
673
674 continue;
675 }
676
677 if ( ! is_null( $prev_is_premium ) && $prev_is_premium !== $site_prev_is_premium ) {
678 // If a different `$site_prev_is_premium` value is found, do not include the option in the collection of options to update.
679 $prev_is_premium = null;
680 }
681
682 if ( $is_earlier_install ) {
683 // If an earlier install timestamp is found.
684 $install_timestamp = $blog_install_timestamp;
685 }
686 }
687
688 $installs_count = count( $blog_id_2_install_map );
689
690 if ( $sites_count === ( $installs_count + $skips_count ) ) {
691 if ( ! empty( $network_user_info ) ) {
692 $options_to_update['network_user_id'] = $network_user_info['user_id'];
693 $options_to_update['network_install_blog_id'] = $network_user_info['blog_id'];
694
695 foreach ( $blog_id_2_install_map as $blog_id => $install ) {
696 if ( $install->user_id == $network_user_info['user_id'] ) {
697 continue;
698 }
699
700 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
701 }
702 }
703
704 if ( $sites_count === $skips_count ) {
705 /**
706 * Assume network-level skipping as the intended action if all actions identified were only
707 * skipping of the connection (i.e., no opt-ins and delegated connections so far).
708 */
709 $options_to_update['is_anonymous_ms'] = true;
710 } else if ( $sites_count === $installs_count ) {
711 /**
712 * Assume network-level opt-in as the intended action if all actions identified were only opt-ins
713 * (i.e., no delegation and skipping of the connections so far).
714 */
715 $options_to_update['is_network_connected'] = true;
716 }
717 }
718 }
719
720 if ( ! is_null( $install_timestamp ) ) {
721 $options_to_update['install_timestamp'] = $install_timestamp;
722 }
723
724 if ( ! is_null( $prev_is_premium ) ) {
725 $options_to_update['prev_is_premium'] = $prev_is_premium;
726 }
727
728 if ( ! empty( $options_to_update ) ) {
729 $this->adjust_storage( $options_to_update, $is_network_admin );
730 }
731 }
732
733 /**
734 * @author Leo Fajardo (@leorw)
735 * @since 2.3.0
736 *
737 * @param array $options
738 * @param bool $is_network_admin
739 */
740 private function adjust_storage( $options, $is_network_admin ) {
741 foreach ( $options as $name => $value ) {
742 $this->_storage->store( $name, $value, $is_network_admin ? true : null );
743 }
744 }
745
746 /**
747 * Checks whether this module has a settings menu.
748 *
749 * @author Leo Fajardo (@leorw)
750 * @since 1.2.2
751 *
752 * @return bool
753 */
754 function has_settings_menu() {
755 return ( $this->_is_network_active && fs_is_network_admin() ) ?
756 $this->_menu->has_network_menu() :
757 $this->_menu->has_menu();
758 }
759
760 /**
761 * 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.
762 *
763 * @author Vova Feldman (@svovaf)
764 * @since 2.4.5
765 *
766 * @return bool
767 */
768 function show_opt_in_on_themes_page() {
769 if ( ! $this->is_free_wp_org_theme() ) {
770 return false;
771 }
772
773 if ( ! $this->has_settings_menu() ) {
774 return true;
775 }
776
777 return $this->show_settings_with_tabs();
778 }
779
780 /**
781 * If `true` the opt-in should be shown on the product's main setting page.
782 *
783 * @author Vova Feldman (@svovaf)
784 * @since 2.4.5
785 *
786 * @return bool
787 *
788 * @uses show_opt_in_on_themes_page();
789 */
790 function show_opt_in_on_setting_page() {
791 return ! $this->show_opt_in_on_themes_page();
792 }
793
794 /**
795 * If `true` the settings should be shown using tabs.
796 *
797 * @author Vova Feldman (@svovaf)
798 * @since 2.4.5
799 *
800 * @return bool
801 */
802 function show_settings_with_tabs() {
803 return ( self::NAVIGATION_TABS === $this->_navigation );
804 }
805
806 /**
807 * Check if the context module is free wp.org theme.
808 *
809 * This method is helpful because:
810 * 1. wp.org themes are limited to a single submenu item,
811 * and sub-submenu items are most likely not allowed (never verified).
812 * 2. wp.org themes are not allowed to redirect the user
813 * after the theme activation, therefore, the agreed UX
814 * is showing the opt-in as a modal dialog box after
815 * activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
816 *
817 * @author Vova Feldman (@svovaf)
818 * @since 1.2.2.7
819 *
820 * @return bool
821 */
822 function is_free_wp_org_theme() {
823 return (
824 $this->is_theme() &&
825 $this->is_org_repo_compliant() &&
826 ! $this->is_premium()
827 );
828 }
829
830 /**
831 * Checks whether this a submenu item is visible.
832 *
833 * @author Vova Feldman (@svovaf)
834 * @since 1.2.2.6
835 * @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.
836 *
837 * @param string $slug
838 * @param bool $is_tabs_visibility_check This is used to decide if the associated tab should be shown or hidden.
839 *
840 * @return bool
841 */
842 function is_submenu_item_visible( $slug, $is_tabs_visibility_check = false ) {
843 if ( $this->is_admin_page( $slug ) ) {
844 /**
845 * It is the current context page, so show the submenu item
846 * so the user will have the right context page, even if it
847 * was set to hidden.
848 */
849 return true;
850 }
851
852 if ( ! $this->has_settings_menu() ) {
853 // No menu settings at all.
854 return false;
855 }
856
857 if (
858 ! $is_tabs_visibility_check &&
859 $this->is_org_repo_compliant() &&
860 $this->show_settings_with_tabs()
861 ) {
862 /**
863 * wp.org themes are limited to a single submenu item, and
864 * sub-submenu items are most likely not allowed (never verified).
865 */
866 return false;
867 }
868
869 return $this->_menu->is_submenu_item_visible( $slug );
870 }
871
872 /**
873 * Check if a Freemius page should be accessible via the UI.
874 *
875 * @author Vova Feldman (@svovaf)
876 * @since 1.2.2.7
877 *
878 * @param string $slug
879 *
880 * @return bool
881 */
882 function is_page_visible( $slug ) {
883 if ( $this->is_admin_page( $slug ) ) {
884 return true;
885 }
886
887 return $this->_menu->is_submenu_item_visible( $slug, true, true );
888 }
889
890 /**
891 * @author Vova Feldman (@svovaf)
892 * @since 1.0.9
893 */
894 private function _version_updates_handler() {
895 if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
896 // Freemius version upgrade mode.
897 $this->_storage->sdk_last_version = $this->_storage->sdk_version;
898 $this->_storage->sdk_version = $this->version;
899
900 if ( empty( $this->_storage->sdk_last_version ) ||
901 version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
902 ) {
903 $this->_storage->sdk_upgrade_mode = true;
904 $this->_storage->sdk_downgrade_mode = false;
905 } else {
906 $this->_storage->sdk_downgrade_mode = true;
907 $this->_storage->sdk_upgrade_mode = false;
908
909 }
910
911 $this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
912 }
913
914 $plugin_version = $this->get_plugin_version();
915 if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
916 // Plugin version upgrade mode.
917 $this->_storage->plugin_last_version = $this->_storage->plugin_version;
918 $this->_storage->plugin_version = $plugin_version;
919
920 if ( empty( $this->_storage->plugin_last_version ) ||
921 version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
922 ) {
923 $this->_storage->plugin_upgrade_mode = true;
924 $this->_storage->plugin_downgrade_mode = false;
925 } else {
926 $this->_storage->plugin_downgrade_mode = true;
927 $this->_storage->plugin_upgrade_mode = false;
928 }
929
930 if ( ! empty( $this->_storage->plugin_last_version ) ) {
931 // Different version of the plugin was installed before, therefore it's an update.
932 $this->_storage->is_plugin_new_install = false;
933 }
934
935 $this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
936 }
937 }
938
939 #--------------------------------------------------------------------------------
940 #region Data Migration on SDK Update
941 #--------------------------------------------------------------------------------
942
943 /**
944 * @author Vova Feldman (@svovaf)
945 * @since 1.1.5
946 *
947 * @param string $sdk_prev_version
948 * @param string $sdk_version
949 */
950 function _sdk_version_update( $sdk_prev_version, $sdk_version ) {
951 if ( empty( $sdk_prev_version ) ) {
952 return;
953 }
954
955 if (
956 version_compare( $sdk_prev_version, '2.5.1', '<' ) &&
957 version_compare( $sdk_version, '2.5.1', '>=' )
958 ) {
959 if ( $this->is_registered( true ) ) {
960 /**
961 * Migrate to new permissions layer.
962 */
963 require_once WP_FS__DIR_INCLUDES . '/supplements/fs-migration-2.5.1.php';
964
965 $install_by_blog_id = is_multisite() ?
966 $this->get_blog_install_map() :
967 array( 0 => $this->_site );
968
969 fs_migrate_251( $this, $install_by_blog_id );
970 }
971 }
972 }
973
974 /**
975 * @author Leo Fajardo (@leorw)
976 * @since 2.0.0
977 *
978 * @param \FS_Storage $storage
979 * @param bool|int|null $blog_id
980 */
981 private static function migrate_install_plan_to_plan_id( FS_Storage $storage, $blog_id = null ) {
982 if ( empty( $storage->sdk_version ) ) {
983 // New installation of the plugin, no need to upgrade.
984 return;
985 }
986
987 if ( ! version_compare( $storage->sdk_version, '2.0.0', '<' ) ) {
988 // Previous version is >= 2.0.0, so no need to migrate.
989 return;
990 }
991
992 // Alias.
993 $module_type = $storage->get_module_type();
994 $module_slug = $storage->get_module_slug();
995
996 $installs = self::get_all_sites( $module_type, $blog_id );
997 $install = isset( $installs[ $module_slug ] ) ? $installs[ $module_slug ] : null;
998
999 if ( ! is_object( $install ) ) {
1000 return;
1001 }
1002
1003 if ( isset( $install->plan ) && is_object( $install->plan ) ) {
1004 if ( isset( $install->plan->id ) && ! empty( $install->plan->id ) ) {
1005 $install->plan_id = self::_decrypt( $install->plan->id );
1006 }
1007
1008 unset( $install->plan );
1009
1010 $installs[ $module_slug ] = clone $install;
1011
1012 self::set_account_option_by_module(
1013 $module_type,
1014 'sites',
1015 $installs,
1016 true,
1017 $blog_id
1018 );
1019 }
1020 }
1021
1022 /**
1023 * @author Vova Feldman (@svovaf)
1024 * @since 1.2.2.7
1025 *
1026 * @param string $plugin_prev_version
1027 * @param string $plugin_version
1028 */
1029 function _after_version_update( $plugin_prev_version, $plugin_version ) {
1030 if ( $this->is_theme() ) {
1031 // Expire the cache of the previous tabs since the theme may
1032 // have setting updates.
1033 $this->_cache->expire( 'tabs' );
1034 $this->_cache->expire( 'tabs_stylesheets' );
1035 }
1036 }
1037
1038 /**
1039 * A special migration logic for the $_accounts, executed for all the plugins in the system:
1040 * - Moves some data to the network level storage.
1041 * - If the plugin's connection was skipped for all sites, set the plugin as if it was network skipped.
1042 * - If the plugin's connection was ignored for all sites, don't do anything in terms of the network connection.
1043 * - 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.
1044 * - 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.
1045 * - 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.
1046 *
1047 * @author Vova Feldman (@svovaf)
1048 * @since 2.0.0
1049 */
1050 private static function migrate_accounts_to_network() {
1051 $sites = self::get_sites();
1052 $sites_count = count( $sites );
1053 $connection_status = array();
1054 $plugin_slugs = array();
1055 foreach ( $sites as $site ) {
1056 $blog_id = self::get_site_blog_id( $site );
1057
1058 self::$_accounts->migrate_to_network( $blog_id );
1059
1060 /**
1061 * Build a list of all Freemius powered plugins slugs.
1062 */
1063 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array(), $blog_id );
1064 foreach ( $id_slug_type_path_map as $module_id => $data ) {
1065 if ( WP_FS__MODULE_TYPE_PLUGIN === $data['type'] ) {
1066 $plugin_slugs[ $data['slug'] ] = true;
1067 }
1068 }
1069
1070 $installs = self::get_account_option( 'sites', WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
1071
1072 if ( is_array( $installs ) ) {
1073 foreach ( $installs as $slug => $install ) {
1074 if ( ! isset( $connection_status[ $slug ] ) ) {
1075 $connection_status[ $slug ] = array();
1076 }
1077
1078 if ( is_object( $install ) &&
1079 FS_Site::is_valid_id( $install->id ) &&
1080 FS_User::is_valid_id( $install->user_id )
1081 ) {
1082 $connection_status[ $slug ][ $blog_id ] = $install->user_id;
1083 }
1084 }
1085 }
1086 }
1087
1088 foreach ( $plugin_slugs as $slug => $true ) {
1089 if ( ! isset( $connection_status[ $slug ] ) ) {
1090 $connection_status[ $slug ] = array();
1091 }
1092
1093 foreach ( $sites as $site ) {
1094 $blog_id = self::get_site_blog_id( $site );
1095
1096 if ( isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1097 continue;
1098 }
1099
1100 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1101
1102 $is_anonymous = $storage->get( 'is_anonymous', null, $blog_id );
1103
1104 if ( ! is_null( $is_anonymous ) ) {
1105 // Since 1.1.3 is_anonymous is an array.
1106 if ( is_array( $is_anonymous ) && isset( $is_anonymous['is'] ) ) {
1107 $is_anonymous = $is_anonymous['is'];
1108 }
1109
1110 if ( is_bool( $is_anonymous ) && true === $is_anonymous ) {
1111 $connection_status[ $slug ][ $blog_id ] = 'skipped';
1112 }
1113 }
1114
1115 if ( ! isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1116 $connection_status[ $slug ][ $blog_id ] = 'ignored';
1117 }
1118 }
1119 }
1120
1121 $super_admins = array();
1122
1123 foreach ( $connection_status as $slug => $blogs_status ) {
1124 $skips = 0;
1125 $ignores = 0;
1126 $connections = 0;
1127 $opted_in_users = array();
1128 $opted_in_super_admins = array();
1129
1130 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1131
1132 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1133 if ( 'skipped' === $status_or_user_id ) {
1134 $skips ++;
1135 } else if ( 'ignored' === $status_or_user_id ) {
1136 $ignores ++;
1137 } else if ( FS_User::is_valid_id( $status_or_user_id ) ) {
1138 $connections ++;
1139
1140 if ( ! isset( $opted_in_users[ $status_or_user_id ] ) ) {
1141 $opted_in_users[ $status_or_user_id ] = array();
1142 }
1143
1144 $opted_in_users[ $status_or_user_id ][] = $blog_id;
1145
1146 if ( isset( $super_admins[ $status_or_user_id ] ) ||
1147 self::is_super_admin( $status_or_user_id )
1148 ) {
1149 // Cache super-admin data.
1150 $super_admins[ $status_or_user_id ] = true;
1151
1152 // Remember opted-in super-admins for the plugin.
1153 $opted_in_super_admins[ $status_or_user_id ] = true;
1154 }
1155 }
1156 }
1157
1158 $main_super_admin_user_id = null;
1159 $all_migrated = false;
1160 if ( $sites_count == $skips ) {
1161 // All sites were skipped -> network skip by copying the anonymous mode from any of the sites.
1162 $storage->is_anonymous_ms = $storage->is_anonymous;
1163
1164 $all_migrated = true;
1165 } else if ( $sites_count == $ignores ) {
1166 // Don't do anything, still in activation mode.
1167
1168 $all_migrated = true;
1169 } else if ( 0 < count( $opted_in_super_admins ) ) {
1170 // Find the super-admin with the majority of installs.
1171 $max_installs_by_super_admin = 0;
1172 foreach ( $opted_in_super_admins as $user_id => $true ) {
1173 $installs_count = count( $opted_in_users[ $user_id ] );
1174
1175 if ( $installs_count > $max_installs_by_super_admin ) {
1176 $max_installs_by_super_admin = $installs_count;
1177 $main_super_admin_user_id = $user_id;
1178 }
1179 }
1180
1181 if ( $sites_count == $connections && 1 == count( $opted_in_super_admins ) ) {
1182 // Super-admin opted-in for all sites in the network.
1183 $storage->is_network_connected = true;
1184
1185 $all_migrated = true;
1186 }
1187
1188 // Store network user.
1189 $storage->network_user_id = $main_super_admin_user_id;
1190
1191 $storage->network_install_blog_id = ( $sites_count == $connections ) ?
1192 // Since all sites are opted-in, associating with the main site.
1193 get_current_blog_id() :
1194 // Associating with the 1st found opted-in site.
1195 $opted_in_users[ $main_super_admin_user_id ][0];
1196
1197 /**
1198 * Make sure we migrate the plan ID of the network install, otherwise, if after the migration
1199 * the 1st page that will be loaded is the network level WP Admin and $storage->network_install_blog_id
1200 * is different than the main site of the network, the $this->_site will not be set since the plan_id
1201 * will be empty.
1202 */
1203 $storage->migrate_to_network();
1204 self::migrate_install_plan_to_plan_id( $storage, $storage->network_install_blog_id );
1205 } else {
1206 // At least one opt-in. All the opt-in were created by a non-super-admin.
1207 if ( 0 == $ignores ) {
1208 // All sites were opted-in or skipped, all by non-super-admin. So delegate all.
1209 $storage->store( 'is_delegated_connection', true, true );
1210
1211 $all_migrated = true;
1212 }
1213 }
1214
1215 if ( ! $all_migrated ) {
1216 /**
1217 * Delegate all sites that were:
1218 * 1) Opted-in by a user that is NOT the main-super-admin.
1219 * 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.
1220 */
1221 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1222 if ( $status_or_user_id == $main_super_admin_user_id ) {
1223 continue;
1224 }
1225
1226 if ( FS_User::is_valid_id( $status_or_user_id ) ||
1227 ( 'skipped' === $status_or_user_id && is_null( $main_super_admin_user_id ) )
1228 ) {
1229 $storage->store( 'is_delegated_connection', true, $blog_id );
1230 }
1231 }
1232 }
1233
1234
1235 if ( ( $connections + $skips > 0 ) ) {
1236 if ( $ignores > 0 ) {
1237 /**
1238 * If admin already opted-in or skipped in any of the network sites, and also
1239 * have sites which the connection decision was not yet taken, set this plugin
1240 * into network activation mode so the super-admin can choose what to do with
1241 * the rest of the sites.
1242 */
1243 self::set_network_upgrade_mode( $storage );
1244 }
1245 }
1246 }
1247 }
1248
1249 /**
1250 * Set a module into network upgrade mode.
1251 *
1252 * @author Vova Feldman (@svovaf)
1253 * @since 2.0.0
1254 *
1255 * @param \FS_Storage $storage
1256 *
1257 * @return bool
1258 */
1259 public static function set_network_upgrade_mode( FS_Storage $storage ) {
1260 return $storage->is_network_activation = true;
1261 }
1262
1263 /**
1264 * Will return true after upgrading to the SDK with the network level integration,
1265 * when the super-admin involvement is required regarding the rest of the sites.
1266 *
1267 * @author Vova Feldman (@svovaf)
1268 * @since 2.0.0
1269 *
1270 * @return bool
1271 */
1272 function is_network_upgrade_mode() {
1273 return $this->_storage->get( 'is_network_activation' );
1274 }
1275
1276 /**
1277 * Clear flag after the upgrade mode completion.
1278 *
1279 * @author Vova Feldman (@svovaf)
1280 * @since 2.0.0
1281 *
1282 * @return bool True if network activation was on and now completed.
1283 */
1284 private function network_upgrade_mode_completed() {
1285 if ( fs_is_network_admin() && $this->is_network_upgrade_mode() ) {
1286 $this->_storage->remove( 'is_network_activation' );
1287
1288 return true;
1289 }
1290
1291 return false;
1292 }
1293
1294 #endregion
1295
1296 /**
1297 * This action is connected to the 'plugins_loaded' hook and helps to determine
1298 * if this is a new plugin installation or a plugin update.
1299 *
1300 * There are 3 different use-cases:
1301 * 1) New plugin installation right with Freemius:
1302 * 1.1 _activate_plugin_event_hook() will be executed first
1303 * 1.2 Since $this->_storage->is_plugin_new_install is not set,
1304 * and $this->_storage->plugin_last_version is not set,
1305 * $this->_storage->is_plugin_new_install will be set to TRUE.
1306 * 1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1307 * be already set to TRUE.
1308 *
1309 * 2) Plugin update, didn't have Freemius before, and now have the SDK:
1310 * 2.1 _activate_plugin_event_hook() will not be executed, because
1311 * the activation hook do NOT fires on updates since WP 3.1.
1312 * 2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1313 * be empty, therefore, it will be set to FALSE.
1314 *
1315 * 3) Plugin update, had Freemius in prev version as well:
1316 * 3.1 _version_updates_handler() will be executed 1st, since FS was installed
1317 * before, $this->_storage->plugin_last_version will NOT be empty,
1318 * therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
1319 * 3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
1320 * already set, therefore, it will not be modified.
1321 *
1322 * Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
1323 *
1324 * NOTE:
1325 * The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
1326 * and then, the next immediate PageView is the plugin's main settings page, it will not
1327 * show the opt-in right away. The reason it will happen is because Freemius execution
1328 * will be turned off till the plugin is fully loaded at least once
1329 * (till $this->_storage->was_plugin_loaded is TRUE).
1330 *
1331 * @author Vova Feldman (@svovaf)
1332 * @since 1.1.9
1333 *
1334 */
1335 function _plugins_loaded() {
1336 // Update flag that plugin was loaded with Freemius at least once.
1337 $this->_storage->was_plugin_loaded = true;
1338
1339 /**
1340 * Bug fix - only set to false when it's a plugin, due to the
1341 * execution sequence of the theme hooks and our methods, if
1342 * this will be set for themes, Freemius will always assume
1343 * it's a theme update.
1344 *
1345 * @author Vova Feldman (@svovaf)
1346 * @since 1.2.2.2
1347 */
1348 if ( $this->is_plugin() &&
1349 ! isset( $this->_storage->is_plugin_new_install )
1350 ) {
1351 $this->_storage->is_plugin_new_install = (
1352 ! is_plugin_active( $this->_plugin_basename ) &&
1353 empty( $this->_storage->plugin_last_version )
1354 );
1355 }
1356 }
1357
1358 function _run_garbage_collector() {
1359 if ( true !== fs_get_optional_constant( 'WP_FS__ENABLE_GARBAGE_COLLECTOR', true ) ) {
1360 return;
1361 }
1362
1363 if ( ! $this->is_user_in_admin() ) {
1364 return;
1365 }
1366
1367 require_once WP_FS__DIR_INCLUDES . '/class-fs-lock.php';
1368
1369 $lock = new FS_Lock( 'garbage_collection' );
1370
1371 if ( $lock->is_locked() ) {
1372 return;
1373 }
1374
1375 // Create a 1-day lock.
1376 $lock->lock( WP_FS__TIME_24_HOURS_IN_SEC );
1377
1378 FS_Garbage_Collector::instance()->clean();
1379 }
1380
1381 /**
1382 * Modifies all external links in the submenu by altering their href, and also opens them in new tab if needed.
1383 *
1384 * @author Vova Feldman (@svovaf)
1385 * @author Swashata Ghosh (@swashata)
1386 * @since 2.1.4
1387 */
1388 static function _handle_submenu_external_link() {
1389 ?>
1390 <script type="text/javascript">
1391 (function ( $ ) {
1392 $( '.fs-submenu-item' ).each( function () {
1393 var $this = $( this ),
1394 $parent = $this.parent(),
1395 externalLink = $this.data( 'fs-external-url' ),
1396 isOpensInNewTab = $this.data( 'fs-new-tab' );
1397
1398 if ( externalLink ) {
1399 $parent.attr( 'href', externalLink );
1400 }
1401
1402 if ( isOpensInNewTab ) {
1403 $parent.attr( { target: '_blank', rel: 'noopener noreferrer' } );
1404 }
1405 } );
1406 } )( jQuery );
1407 </script>
1408 <?php
1409 }
1410
1411 /**
1412 * @author Vova Feldman (@svovaf)
1413 * @since 1.0.9
1414 */
1415 private function register_constructor_hooks() {
1416 $this->_logger->entrance();
1417
1418 if ( is_admin() ) {
1419 add_action( 'admin_init', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
1420
1421 if ( $this->is_plugin() ) {
1422 if ( self::is_plugin_install_page() && true !== fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) {
1423 /**
1424 * Unless the `fs_allow_updater_and_dialog` URL param exists and its value is `true`, make
1425 * Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php)
1426 * so that they won't interfere with the .org plugins' functionalities on that page (e.g.
1427 * updating of a .org plugin).
1428 */
1429 add_filter( 'site_transient_update_plugins', array( 'Freemius', '_remove_fs_updates_from_plugin_install_page' ), 10, 2 );
1430 } else if ( self::is_plugins_page() || self::is_updates_page() ) {
1431 /**
1432 * 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.
1433 *
1434 * @author Leo Fajardo (@leorw)
1435 * @since 2.2.3
1436 */
1437 add_action( 'admin_footer', array( 'Freemius', '_prepend_fs_allow_updater_and_dialog_flag_url_param' ) );
1438 }
1439
1440 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
1441
1442 /**
1443 * @since 1.2.2
1444 *
1445 * Hook to both free and premium version activations to support
1446 * auto deactivation on the other version activation.
1447 */
1448 register_activation_hook(
1449 $plugin_dir . $this->_free_plugin_basename,
1450 array( &$this, '_activate_plugin_event_hook' )
1451 );
1452
1453 register_activation_hook(
1454 $plugin_dir . $this->premium_plugin_basename(),
1455 array( &$this, '_activate_plugin_event_hook' )
1456 );
1457 } else {
1458 add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
1459
1460 add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
1461 }
1462
1463 /**
1464 * Part of the mechanism to identify new plugin install vs. plugin update.
1465 *
1466 * @author Vova Feldman (@svovaf)
1467 * @since 1.1.9
1468 */
1469 if ( empty( $this->_storage->was_plugin_loaded ) ) {
1470 /**
1471 * During the plugin activation (not theme), 'plugins_loaded' will be already executed
1472 * when the logic gets here since the activation logic first add the activate plugins,
1473 * then triggers 'plugins_loaded', and only then include the code of the plugin that
1474 * is activated. Which means that _plugins_loaded() will NOT be executed during the
1475 * plugin activation, and that IS intentional.
1476 *
1477 * @author Vova Feldman (@svovaf)
1478 */
1479 if ( $this->is_plugin() &&
1480 $this->is_activation_mode( false ) &&
1481 0 == did_action( 'plugins_loaded' )
1482 ) {
1483 add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
1484 } else {
1485 // If was activated before, then it was already loaded before.
1486 $this->_plugins_loaded();
1487 }
1488 }
1489
1490 add_action( 'plugins_loaded', array( &$this, '_run_garbage_collector' ) );
1491
1492 if ( ! self::is_ajax() ) {
1493 if ( ! $this->is_addon() ) {
1494 add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
1495 }
1496 }
1497
1498 if ( $this->_storage->handle_gdpr_admin_notice ) {
1499 add_action( 'init', array( &$this, '_maybe_show_gdpr_admin_notice' ) );
1500 }
1501
1502 add_action( 'init', array( &$this, '_maybe_add_gdpr_optin_ajax_handler') );
1503 add_action( 'init', array( &$this, '_add_pricing_ajax_handler' ) );
1504 }
1505
1506 if ( $this->is_plugin() ) {
1507 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1508 add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
1509 } else {
1510 add_action( 'wp_initialize_site', array( $this, '_after_wp_initialize_site_callback' ), 11, 2 );
1511 }
1512
1513 register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
1514 }
1515
1516 if ( is_multisite() ) {
1517 add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
1518 add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) );
1519 add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) );
1520
1521 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1522 add_action( 'deleted_blog', array( $this, '_after_site_deleted_callback' ), 10, 2 );
1523 } else {
1524 add_action( 'wp_delete_site', array( $this, '_after_wpsite_deleted_callback' ) );
1525 }
1526
1527 add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) );
1528 add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) );
1529 add_action( 'make_ham_blog', array( &$this, '_after_site_reactivated_callback' ) );
1530 }
1531
1532 if ( $this->is_theme() &&
1533 self::is_customizer() &&
1534 $this->apply_filters( 'show_customizer_upsell', true )
1535 ) {
1536 // Register customizer upsell.
1537 add_action( 'customize_register', array( &$this, '_customizer_register' ) );
1538 }
1539
1540 add_action( 'admin_init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
1541
1542 if ( $this->is_theme() && ! $this->is_migration() ) {
1543 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1544 }
1545
1546 add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
1547 add_action( 'admin_init', array( &$this, '_add_premium_version_upgrade_selection' ) );
1548 add_action( 'admin_init', array( &$this, '_add_beta_mode_update_handler' ) );
1549 add_action( 'admin_init', array( &$this, '_add_user_change_option' ) );
1550 add_action( 'admin_init', array( &$this, '_add_email_address_update_option' ) );
1551
1552 $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
1553 $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
1554 $this->add_ajax_action( 'set_data_debug_mode', array( &$this, '_set_data_debug_mode' ) );
1555 $this->add_ajax_action( 'toggle_whitelabel_mode', array( &$this, '_toggle_whitelabel_mode_ajax_handler' ) );
1556
1557 if ( $this->_is_network_active && fs_is_network_admin() ) {
1558 $this->add_ajax_action( 'network_activate', array( &$this, '_network_activate_ajax_action' ) );
1559 }
1560
1561 $this->add_ajax_action( 'install_premium_version', array(
1562 &$this,
1563 '_install_premium_version_ajax_action'
1564 ) );
1565
1566 $this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
1567
1568 $this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
1569
1570 $this->add_action( 'sdk_version_update', array( &$this, '_sdk_version_update' ), WP_FS__DEFAULT_PRIORITY, 2 );
1571
1572 $this->add_action(
1573 'plugin_version_update',
1574 array( &$this, '_after_version_update' ),
1575 WP_FS__DEFAULT_PRIORITY,
1576 2
1577 );
1578 $this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
1579
1580 add_action( 'admin_init', array( &$this, '_add_trial_notice' ) ); // @phpstan-ignore-line
1581 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) ); // @phpstan-ignore-line
1582 add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) );
1583
1584 /**
1585 * Handle request to reset anonymous mode for `get_reconnect_url()` or reset the pending activation mode.
1586 *
1587 * @author Vova Feldman (@svovaf)
1588 * @since 1.2.1.5
1589 */
1590 if (
1591 (
1592 fs_request_is_action( 'reset_anonymous_mode' ) ||
1593 fs_request_is_action( 'reset_pending_activation_mode' )
1594 ) &&
1595 $this->get_unique_affix() === fs_request_get_raw( 'fs_unique_affix' )
1596 ) {
1597 add_action( 'admin_init', array( &$this, 'connect_again' ) );
1598 }
1599
1600 FS_DebugManager::register_hooks();
1601 }
1602
1603 /**
1604 * Register the required hooks right after the settings parse is completed.
1605 *
1606 * @author Vova Feldman (@svovaf)
1607 * @since 2.3.1
1608 */
1609 private function register_after_settings_parse_hooks() {
1610 if ( is_admin() &&
1611 $this->is_theme() &&
1612 $this->is_premium() &&
1613 ! $this->has_active_valid_license()
1614 ) {
1615 $this->add_ajax_action(
1616 'delete_theme_update_data',
1617 array( &$this, '_delete_theme_update_data_action' )
1618 );
1619 }
1620
1621 if ( $this->show_settings_with_tabs() ) {
1622 /**
1623 * Include the required hooks to capture the theme settings' page tabs
1624 * and cache them.
1625 *
1626 * @author Vova Feldman (@svovaf)
1627 * @since 1.2.2.7
1628 */
1629 if ( ! $this->_cache->has_valid( 'tabs' ) ) {
1630 add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
1631 // Add license activation AJAX callback.
1632 $this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
1633
1634 add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
1635 }
1636
1637 add_action(
1638 'admin_footer',
1639 array( &$this, '_add_freemius_tabs' ),
1640 /**
1641 * The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
1642 * That's why the priority is 11 while the tabs capture logic is added
1643 * with priority 10.
1644 *
1645 * @author Vova Feldman (@svovaf)
1646 */
1647 11
1648 );
1649 }
1650
1651 if ( ! self::is_ajax() ) {
1652 if ( ! $this->is_addon() || $this->is_only_premium() ) {
1653 add_action(
1654 ( $this->_is_network_active && fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu',
1655 array( &$this, '_prepare_admin_menu' ),
1656 WP_FS__LOWEST_PRIORITY
1657 );
1658 }
1659 }
1660
1661 if (
1662 $this->is_user_in_admin() &&
1663 $this->is_parallel_activation() &&
1664 $this->_premium_plugin_basename !== $this->_premium_plugin_basename_from_parallel_activation
1665 ) {
1666 $this->_premium_plugin_basename = $this->_premium_plugin_basename_from_parallel_activation;
1667
1668 register_activation_hook(
1669 dirname( $this->_plugin_dir_path ) . '/' . $this->_premium_plugin_basename,
1670 array( &$this, '_activate_plugin_event_hook' )
1671 );
1672 }
1673 }
1674
1675 /**
1676 * Determines if a plugin is running in parallel activation mode.
1677 *
1678 * @author Leo Fajardo (@leorw)
1679 * @since 2.9.1
1680 *
1681 * @return bool
1682 */
1683 private function is_parallel_activation() {
1684 return ! empty( $this->_premium_plugin_basename_from_parallel_activation );
1685 }
1686
1687 /**
1688 * Makes Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php) so that
1689 * they won't interfere with the .org plugins' functionalities on that page (e.g. updating of a .org plugin).
1690 *
1691 * @author Leo Fajardo (@leorw)
1692 * @since 2.2.3
1693 *
1694 * @param object $updates
1695 * @param string|null $transient
1696 *
1697 * @return object
1698 */
1699 static function _remove_fs_updates_from_plugin_install_page( $updates, $transient = null ) {
1700 if ( is_object( $updates ) && isset( $updates->response ) ) {
1701 foreach ( $updates->response as $file => $plugin ) {
1702 if ( isset( $plugin->package ) && false !== strpos( $plugin->package, 'api.freemius' ) ) {
1703 unset( $updates->response[ $file ] );
1704 }
1705 }
1706 }
1707
1708 return $updates;
1709 }
1710
1711 /**
1712 * Prepends the `fs_allow_updater_and_dialog` param to the plugin information URLs to tell the SDK to handle
1713 * the information that is shown on the plugin details dialog that is shown when the relevant link is clicked.
1714 *
1715 * @author Leo Fajardo (@leorw)
1716 * @since 2.2.3
1717 *
1718 * @return void
1719 */
1720 static function _prepend_fs_allow_updater_and_dialog_flag_url_param() {
1721 $slug_basename_map = array();
1722 foreach ( self::$_instances as $instance ) {
1723 if ( ! $instance->is_plugin() ) {
1724 continue;
1725 }
1726
1727 $slug_basename_map[ $instance->get_slug() ] = $instance->premium_plugin_basename();
1728 }
1729 ?>
1730 <script type="text/javascript">
1731 (function( $ ) {
1732 var slugBasenameMap = <?php echo json_encode( $slug_basename_map ) ?>;
1733 for ( var slug in slugBasenameMap ) {
1734 var basename = slugBasenameMap[ slug ];
1735
1736 // Try to get the plugin rows if on the "Plugins" page.
1737 var $pluginRows = $( '.wp-list-table.plugins tr[data-plugin="' + basename + '"]');
1738
1739 if ( 0 === $pluginRows.length ) {
1740 // Try to get the plugin rows if on the "Updates" page.
1741 var $pluginCheckbox = $( '#update-plugins-table input[type="checkbox"][value="' + basename + '"]' );
1742 if ( 0 !== $pluginCheckbox.length ) {
1743 $pluginRows = $pluginCheckbox.parents( 'tr:first' );
1744 }
1745 }
1746
1747 if ( 0 === $pluginRows.length ) {
1748 // No plugin rows found.
1749 continue;
1750 }
1751
1752 // Find the "View details" links and add the `fs_allow_updater_and_dialog` param to the URL.
1753 $pluginRows.find( 'a[href*="plugin-install.php?tab=plugin-information"]' ).each(function() {
1754 var $this = $( this ),
1755 href = $this.attr( 'href' ).replace( '?tab=', '?fs_allow_updater_and_dialog=true&tab=');
1756
1757 $this.attr( 'href', href );
1758 });
1759 }
1760 })( jQuery );
1761 </script>
1762 <?php
1763 }
1764
1765 /**
1766 * @author Leo Fajardo (@leorw)
1767 * @since 2.3.0
1768 */
1769 static function _maybe_add_beta_label_styles() {
1770 $has_any_beta_version = false;
1771
1772 foreach ( self::$_instances as $instance ) {
1773 if ( $instance->is_beta() ) {
1774 $has_any_beta_version = true;
1775 break;
1776 }
1777 }
1778
1779 if ( $has_any_beta_version ) {
1780 fs_enqueue_local_style( 'fs_plugins', '/admin/plugins.css' );
1781 }
1782 }
1783
1784 /**
1785 * @author Leo Fajardo (@leorw)
1786 * @since 2.3.0
1787 */
1788 static function _maybe_add_beta_label_to_plugins_and_handle_confirmation() {
1789 $beta_data = array();
1790
1791 foreach ( self::$_instances as $instance ) {
1792 if ( ! $instance->is_premium() ) {
1793 continue;
1794 }
1795
1796 /**
1797 * If there's an available beta version update, a confirmation message will be shown when the
1798 * "Update now" link on the "Plugins" or "Themes" page is clicked.
1799 */
1800 $has_beta_update = $instance->has_beta_update();
1801
1802 $is_beta = (
1803 // The "Beta" label is added separately for themes.
1804 $instance->is_plugin() &&
1805 $instance->is_beta()
1806 );
1807
1808 if ( ! $is_beta && ! $has_beta_update ) {
1809 continue;
1810 }
1811
1812 $beta_data[ $instance->get_plugin_basename() ] = array( 'is_installed_version_beta' => $is_beta );
1813
1814 if ( ! $has_beta_update ) {
1815 continue;
1816 }
1817
1818 $beta_data[ $instance->get_plugin_basename() ]['beta_version_update_confirmation_message'] = sprintf(
1819 '%s %s',
1820 sprintf(
1821 fs_esc_attr_inline(
1822 '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.',
1823 'beta-version-update-caution',
1824 $instance->get_slug()
1825 ),
1826 $instance->get_plugin_title()
1827 ),
1828 fs_esc_attr_inline( 'Would you like to proceed with the update?', 'update-confirmation', $instance->get_slug() )
1829 );
1830 }
1831
1832 if ( empty( $beta_data ) ) {
1833 return;
1834 }
1835 ?>
1836 <script type="text/javascript">
1837 ( function( $ ) {
1838 var betaData = <?php echo json_encode( $beta_data ) ?>;
1839
1840 for ( var pluginBasename in betaData ) {
1841 if ( ! betaData.hasOwnProperty( pluginBasename ) ) {
1842 continue;
1843 }
1844
1845 if ( ! betaData[ pluginBasename ].is_installed_version_beta ) {
1846 continue;
1847 }
1848
1849 var $parentContainer = $( '.wp-list-table.plugins tr[data-plugin="' + pluginBasename + '"]' );
1850 if ( 0 === $parentContainer.length ) {
1851 continue;
1852 }
1853
1854 $parentContainer.find( '.plugin-title > strong:first-child').append(
1855 '<span class="fs-tag fs-info"><?php fs_esc_js_echo_inline( 'Beta', 'beta' ) ?></span>'
1856 );
1857 }
1858
1859 setTimeout( function() {
1860 // Wait a little bit before adding the event handler, otherwise, it will be overridden by the core WP logic.
1861 $( '.plugins .update-message .update-link, .themes .theme .update-message' ).on( 'click', function() {
1862 var $parentContainer = $( this ).parents( 'tr:first' );
1863 pluginBasename = ( 0 !== $parentContainer.length ) ?
1864 $parentContainer.data( 'plugin' ) :
1865 $( this ).parents( '.theme:first' ).data( 'slug' );
1866
1867 if (
1868 betaData[ pluginBasename ] &&
1869 betaData[ pluginBasename ].beta_version_update_confirmation_message &&
1870 ! confirm( betaData[ pluginBasename ].beta_version_update_confirmation_message )
1871 ) {
1872 return false;
1873 }
1874 } );
1875 }, 20 );
1876 } )( jQuery );
1877 </script>
1878 <?php
1879 }
1880
1881 /**
1882 * Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
1883 * could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
1884 * plugin will trigger inclusion of the free or premium version and if one of them is active during the
1885 * uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
1886 *
1887 * @author Leo Fajardo (@leorw)
1888 *
1889 * @since 1.2.0
1890 */
1891 private function unregister_uninstall_hook() {
1892 $uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
1893 unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
1894 unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
1895
1896 update_option( 'uninstall_plugins', $uninstallable_plugins );
1897 }
1898
1899 /**
1900 * @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
1901 *
1902 * @param bool $store_prev_path
1903 */
1904 private function clear_module_main_file_cache( $store_prev_path = true ) {
1905 if ( ! isset( $this->_storage->plugin_main_file ) ||
1906 empty( $this->_storage->plugin_main_file->path )
1907 ) {
1908 return;
1909 }
1910
1911 if ( ! $store_prev_path ) {
1912 /**
1913 * Storing the previous path is not needed when clearing the cache after an SDK version update since
1914 * the main purpose of the cache clearing in that event is to correct a wrong plugin main file path
1915 * which causes data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
1916 *
1917 * @author Leo Fajardo (@leorw)
1918 * @since 2.2.1
1919 */
1920 unset( $this->_storage->plugin_main_file->path );
1921 } else {
1922 $plugin_main_file = clone $this->_storage->plugin_main_file;
1923
1924 // Store cached path (2nd layer cache).
1925 $plugin_main_file->prev_path = $plugin_main_file->path;
1926
1927 // Clear cached path.
1928 unset( $plugin_main_file->path );
1929
1930 $this->_storage->plugin_main_file = $plugin_main_file;
1931 }
1932
1933 /**
1934 * Clear global cached path.
1935 *
1936 * @author Leo Fajardo (@leorw)
1937 * @since 1.2.2
1938 */
1939 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
1940 unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
1941 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
1942 }
1943
1944 /**
1945 * @author Leo Fajardo (@leorw)
1946 * @since 2.0.0
1947 */
1948 function _hook_action_links_and_register_account_hooks() {
1949 if ( $this->is_migration() ) {
1950 return;
1951 }
1952
1953 if (
1954 ( self::is_plugins_page() && $this->is_plugin() ) ||
1955 ( self::is_themes_page() && $this->is_theme() ) ||
1956 fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' )
1957 ) {
1958 $this->_add_tracking_links();
1959 }
1960
1961 if ( self::is_plugins_page() && $this->is_plugin() ) {
1962 $this->hook_plugin_action_links();
1963 }
1964
1965 $this->_register_account_hooks();
1966 }
1967
1968 /**
1969 * @author Vova Feldman (@svovaf)
1970 * @since 1.0.9
1971 */
1972 private function _register_account_hooks() {
1973 if ( ! is_admin() ) {
1974 return;
1975 }
1976
1977 /**
1978 * Always show the deactivation feedback form since we added
1979 * automatic free version deactivation upon premium code activation.
1980 *
1981 * @since 1.2.1.6
1982 */
1983 $this->add_ajax_action(
1984 'submit_uninstall_reason',
1985 array( &$this, '_submit_uninstall_reason_action' )
1986 );
1987
1988 $this->add_ajax_action(
1989 'cancel_subscription_or_trial',
1990 array( &$this, 'cancel_subscription_or_trial_ajax_action' )
1991 );
1992
1993 if ( ! $this->is_addon() || $this->is_parent_plugin_installed() ) {
1994 if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
1995 ( $this->is_theme() && self::is_themes_page() )
1996 ) {
1997 add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
1998 }
1999 }
2000 }
2001
2002 /**
2003 * Leverage backtrace to find caller plugin file path.
2004 *
2005 * @param bool $is_init Is initiation sequence.
2006 * @param string $main_file Since 2.5.0 expects the module's main file path to potentially purge the cached path.
2007 *
2008 * @return string
2009 * @since 1.0.6
2010 *
2011 * @author Vova Feldman (@svovaf)
2012 */
2013 private function _find_caller_plugin_file( $is_init = false, $main_file = '' ) {
2014 // Try to load the cached value of the file path.
2015 if ( isset( $this->_storage->plugin_main_file ) ) {
2016 $plugin_main_file = $this->_storage->plugin_main_file;
2017 if ( ! empty( $plugin_main_file->path ) ) {
2018 $absolute_path = $this->get_absolute_path( $plugin_main_file->path );
2019 if ( file_exists( $absolute_path ) ) {
2020 if ( $is_init && $absolute_path !== $this->get_absolute_path( $main_file ) ) {
2021 // Update cached path if not matching the actual path.
2022 $plugin_main_file->path = $main_file;
2023 $this->_storage->plugin_main_file = $plugin_main_file;
2024 }
2025
2026 return $absolute_path;
2027 }
2028 }
2029 }
2030
2031 /**
2032 * @since 1.2.1
2033 *
2034 * `clear_module_main_file_cache()` is clearing the plugin's cached path on
2035 * deactivation. Therefore, if any plugin/theme was initiating `Freemius`
2036 * with that plugin's slug, it was overriding the empty plugin path with a wrong path.
2037 *
2038 * So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
2039 * when the class instantiator isn't the module.
2040 */
2041 if ( ! $is_init ) {
2042 // Fetch prev path cache.
2043 if ( isset( $this->_storage->plugin_main_file ) &&
2044 ! empty( $this->_storage->plugin_main_file->prev_path )
2045 ) {
2046 $absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
2047 if ( file_exists( $absolute_path ) ) {
2048 return $absolute_path;
2049 }
2050 }
2051
2052 wp_die(
2053 $this->get_text_inline( 'Freemius SDK couldn\'t find the plugin\'s main file. Please contact [email protected] with the current error.', 'failed-finding-main-path' ) .
2054 " Module: {$this->_slug}; SDK: " . WP_FS__SDK_VERSION . ";",
2055 $this->get_text_inline( 'Error', 'error' ),
2056 array( 'back_link' => true )
2057 );
2058 }
2059
2060 /**
2061 * @since 1.2.1
2062 *
2063 * Only the original instantiator that calls dynamic_init can modify the module's path.
2064 */
2065 // Find caller module.
2066 $this->_storage->plugin_main_file = (object) array(
2067 'path' => $main_file,
2068 );
2069
2070 return $this->get_absolute_path( $main_file );
2071 }
2072
2073 /**
2074 * @author Leo Fajardo (@leorw)
2075 * @since 1.2.3
2076 *
2077 * @param string $path
2078 *
2079 * @return string
2080 */
2081 private function get_relative_path( $path ) {
2082 $module_root_dir = $this->get_module_root_dir_path();
2083 if ( 0 === strpos( $path, $module_root_dir ) ) {
2084 $path = substr( $path, strlen( $module_root_dir ) );
2085 }
2086
2087 return $path;
2088 }
2089
2090 /**
2091 * @author Leo Fajardo (@leorw)
2092 * @since 1.2.3
2093 *
2094 * @param string $path
2095 * @param string|bool $module_type
2096 *
2097 * @return string
2098 */
2099 private function get_absolute_path( $path, $module_type = false ) {
2100 $module_root_dir = $this->get_module_root_dir_path( $module_type );
2101 if ( 0 !== strpos( $path, $module_root_dir ) ) {
2102 $path = fs_normalize_path( $module_root_dir . $path );
2103 }
2104
2105 return $path;
2106 }
2107
2108 /**
2109 * @author Leo Fajardo (@leorw)
2110 * @since 1.2.3
2111 *
2112 * @param string|bool $module_type
2113 *
2114 * @return string
2115 */
2116 private function get_module_root_dir_path( $module_type = false ) {
2117 $is_plugin = empty( $module_type ) ?
2118 $this->is_plugin() :
2119 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
2120
2121 return fs_normalize_path( trailingslashit( $is_plugin ?
2122 WP_PLUGIN_DIR :
2123 get_theme_root( get_stylesheet() ) ) );
2124 }
2125
2126 /**
2127 * @author Leo Fajardo (@leorw)
2128 *
2129 * @param number $module_id
2130 * @param string $slug
2131 *
2132 * @return string Since 2.5.0 return the module's main file path.
2133 *
2134 * @since 1.2.2
2135 */
2136 private function store_id_slug_type_path_map( $module_id, $slug ) {
2137 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
2138
2139 $store_option = false;
2140
2141 if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
2142 $id_slug_type_path_map[ $module_id ] = array(
2143 'slug' => $slug
2144 );
2145
2146 $store_option = true;
2147 } else if (
2148 isset( $id_slug_type_path_map[ $module_id ]['slug'] ) &&
2149 $slug !== $id_slug_type_path_map[ $module_id ]['slug']
2150 ) {
2151 $id_slug_type_path_map[ $module_id ]['slug'] = $slug;
2152 $store_option = true;
2153 }
2154
2155 $find_caller = empty( $id_slug_type_path_map[ $module_id ]['path'] );
2156
2157 if ( ! $find_caller ) {
2158 /**
2159 * This verification is for cases when suddenly the same module
2160 * is installed but with a different folder name.
2161 *
2162 * @author Vova Feldman (@svovaf)
2163 * @since 1.2.3
2164 */
2165 $find_caller = ! file_exists( $this->get_absolute_path(
2166 $id_slug_type_path_map[ $module_id ]['path'],
2167 $id_slug_type_path_map[ $module_id ]['type']
2168 ) );
2169 }
2170
2171 foreach ( $id_slug_type_path_map as $id => $data ) {
2172 if ( empty( $id ) ) {
2173 // Remove maps with empty module ID.
2174 unset( $id_slug_type_path_map[ $id ] );
2175 $store_option = true;
2176 continue;
2177 }
2178
2179 /**
2180 * 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.
2181 *
2182 * @author Vova Feldman (@svovaf)
2183 * @since 2.5.0
2184 */
2185 if ( ! $find_caller ) {
2186 if ( $id == $module_id ) {
2187 continue;
2188 }
2189
2190 if (
2191 isset( $data['path'] ) &&
2192 $data['path'] === $id_slug_type_path_map[ $module_id ]['path']
2193 ) {
2194 $find_caller = true;
2195 }
2196 }
2197 }
2198
2199 if ( $find_caller ) {
2200 $caller_main_file_and_type = $this->get_caller_main_file_and_type( $module_id );
2201
2202 $id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type;
2203 $id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path;
2204
2205 $store_option = true;
2206 }
2207
2208 if ( $store_option ) {
2209 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
2210 }
2211
2212 return $id_slug_type_path_map[ $module_id ]['path'];
2213 }
2214
2215 /**
2216 * Identifies the caller type: plugin or theme.
2217 *
2218 * @author Leo Fajardo (@leorw)
2219 * @since 1.2.2
2220 *
2221 * @author Vova Feldman (@svovaf)
2222 * @since 1.2.2.3 Find the earliest module in the call stack that calls to the SDK. This fix is for cases when
2223 * add-ons are relying on loading the SDK from the parent module, and also allows themes including the
2224 * SDK an internal file instead of directly from functions.php.
2225 * @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic.
2226 *
2227 * @param number $module_id @since 2.5.0
2228 */
2229 private function get_caller_main_file_and_type( $module_id ) {
2230 self::require_plugin_essentials();
2231
2232 $all_plugins = fs_get_plugins( true );
2233 $all_plugins_paths = array();
2234
2235 // Get active plugin's main files real full names (might be symlinks).
2236 foreach ( $all_plugins as $relative_path => $data ) {
2237 if ( false === strpos( fs_normalize_path( $relative_path ), '/' ) ) {
2238 /**
2239 * Ignore plugins that don't have a folder (e.g. Hello Dolly) since they
2240 * can't really include the SDK.
2241 *
2242 * @author Vova Feldman
2243 * @since 1.2.1.7
2244 */
2245 continue;
2246 }
2247
2248 $all_plugins_paths[] = fs_normalize_path( realpath( WP_PLUGIN_DIR . '/' . $relative_path ) );
2249 }
2250
2251 $caller_file_candidate = false;
2252 $caller_map = array();
2253 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2254 $themes_dir = fs_normalize_path( get_theme_root( get_stylesheet() ) );
2255 $plugin_dir_to_skip = false;
2256
2257 for ( $i = 1, $bt = debug_backtrace(), $len = count( $bt ); $i < $len; $i ++ ) {
2258 if ( empty( $bt[ $i ]['file'] ) ) {
2259 continue;
2260 }
2261
2262 if ( $i > 1 && ! empty( $bt[ $i - 1 ]['file'] ) && $bt[ $i ]['file'] === $bt[ $i - 1 ]['file'] ) {
2263 // If file same as the prev file in the stack, skip it.
2264 continue;
2265 }
2266
2267 if ( ! empty( $bt[ $i ]['function'] ) && in_array( $bt[ $i ]['function'], array(
2268 'do_action',
2269 'apply_filter',
2270 // The string split is stupid, but otherwise, theme check
2271 // throws info notices.
2272 'requir' . 'e_once',
2273 'requir' . 'e',
2274 'includ' . 'e_once',
2275 'includ' . 'e',
2276 'install_and_activate_plugin',
2277 'try_activate_plugin',
2278 'activate_plugin'
2279 ) )
2280 ) {
2281 if ( 'activate_plugin' === $bt[ $i ]['function'] ) {
2282 /**
2283 * Store the directory of the activator plugin so that any other file that starts with it
2284 * cannot be mistakenly chosen as a candidate caller file.
2285 *
2286 * @author Leo Fajardo
2287 *
2288 * @since 2.3.0
2289 */
2290 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2291
2292 foreach ( $all_plugins_paths as $plugin_path ) {
2293 $plugin_dir = fs_normalize_path( dirname( $plugin_path ) . '/' );
2294 if ( false !== strpos( $caller_file_path, $plugin_dir ) ) {
2295 $plugin_dir_to_skip = $plugin_dir;
2296
2297 break;
2298 }
2299 }
2300 }
2301
2302 // Ignore call stack hooks and files inclusion.
2303 continue;
2304 }
2305
2306 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2307
2308 if ( ! empty( $plugin_dir_to_skip ) ) {
2309 /**
2310 * Skip if it's an activator plugin file to avoid mistakenly choosing it as a candidate caller file.
2311 *
2312 * @author Leo Fajardo
2313 *
2314 * @since 2.3.0
2315 */
2316 if ( 0 === strpos( $caller_file_path, $plugin_dir_to_skip ) ) {
2317 continue;
2318 }
2319 }
2320
2321 if ( 'functions.php' === basename( $caller_file_path ) ) {
2322 /**
2323 * 1. Assumes that theme's starting execution file is functions.php.
2324 * 2. This complex logic fixes symlink issues (e.g. with Vargant).
2325 *
2326 * @author Vova Feldman (@svovaf)
2327 * @since 1.2.2.5
2328 */
2329
2330 if ( $caller_file_path == fs_normalize_path( realpath( trailingslashit( $themes_dir ) . basename( dirname( $caller_file_path ) ) . '/' . basename( $caller_file_path ) ) ) ) {
2331 $module_type = WP_FS__MODULE_TYPE_THEME;
2332
2333 /**
2334 * Relative path of the theme, e.g.:
2335 * `my-theme/functions.php`
2336 *
2337 * @author Leo Fajardo (@leorw)
2338 */
2339 $caller_file_candidate = basename( dirname( $caller_file_path ) ) .
2340 '/' .
2341 basename( $caller_file_path );
2342
2343 continue;
2344 }
2345 }
2346
2347 $caller_file_hash = md5( $caller_file_path );
2348
2349 if ( ! isset( $caller_map[ $caller_file_hash ] ) ) {
2350 foreach ( $all_plugins_paths as $plugin_path ) {
2351 if ( empty( $plugin_path ) ) {
2352 continue;
2353 }
2354
2355 if ( false !== strpos( $caller_file_path, fs_normalize_path( dirname( $plugin_path ) . '/' ) ) ) {
2356 $caller_map[ $caller_file_hash ] = fs_normalize_path( $plugin_path );
2357 break;
2358 }
2359 }
2360 }
2361
2362 if ( isset( $caller_map[ $caller_file_hash ] ) ) {
2363 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2364 $caller_file_candidate = plugin_basename( $caller_map[ $caller_file_hash ] );
2365 }
2366 }
2367
2368 $caller_main_file_and_type = (object) array(
2369 'module_type' => $module_type,
2370 'path' => $caller_file_candidate
2371 );
2372
2373 return apply_filters( "fs_{$module_id}_caller_main_file_and_type", $caller_main_file_and_type );
2374 }
2375
2376 #----------------------------------------------------------------------------------
2377 #region Deactivation Feedback Form
2378 #----------------------------------------------------------------------------------
2379
2380 /**
2381 * Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
2382 * page.
2383 *
2384 * @author Vova Feldman (@svovaf)
2385 * @author Leo Fajardo (@leorw)
2386 *
2387 * @since 1.1.2
2388 */
2389 function _add_deactivation_feedback_dialog_box() {
2390 if (
2391 $this->is_clone() ||
2392 ( is_object( $this->_site ) && ! $this->is_registered() )
2393 ) {
2394 return;
2395 }
2396
2397 $subscription_cancellation_dialog_box_template_params = $this->apply_filters( 'show_deactivation_subscription_cancellation', true ) ?
2398 $this->_get_subscription_cancellation_dialog_box_template_params() :
2399 array();
2400
2401 /**
2402 * @since 2.3.0 Developers can optionally hide the deactivation feedback form using the 'show_deactivation_feedback_form' filter.
2403 */
2404 $show_deactivation_feedback_form = ! self::is_deactivation_snoozed();
2405 if ( $this->has_filter( 'show_deactivation_feedback_form' ) ) {
2406 $show_deactivation_feedback_form = $this->apply_filters( 'show_deactivation_feedback_form', true );
2407 } else if ( $this->is_addon() ) {
2408 /**
2409 * If the add-on's 'show_deactivation_feedback_form' is not set, try to inherit the value from the parent.
2410 */
2411 $show_deactivation_feedback_form = $this->get_parent_instance()->apply_filters( 'show_deactivation_feedback_form', true );
2412 }
2413
2414 $uninstall_confirmation_message = $this->apply_filters( 'uninstall_confirmation_message', '' );
2415
2416 if (
2417 empty( $subscription_cancellation_dialog_box_template_params ) &&
2418 ! $show_deactivation_feedback_form &&
2419 empty( $uninstall_confirmation_message )
2420 ) {
2421 return;
2422 }
2423
2424 $vars = array( 'id' => $this->_module_id );
2425
2426 if ( $show_deactivation_feedback_form ) {
2427 /* Check the type of user:
2428 * 1. Long-term (long-term)
2429 * 2. Non-registered and non-anonymous short-term (non-registered-and-non-anonymous-short-term).
2430 * 3. Short-term (short-term)
2431 */
2432 $is_long_term_user = true;
2433
2434 // Check if the site is at least 2 days old.
2435 $time_installed = $this->_storage->install_timestamp;
2436
2437 // Difference in seconds.
2438 $date_diff = time() - $time_installed;
2439
2440 // Convert seconds to days.
2441 $date_diff_days = floor( $date_diff / ( 60 * 60 * 24 ) );
2442
2443 if ( $date_diff_days < 2 ) {
2444 $is_long_term_user = false;
2445 }
2446
2447 $is_long_term_user = $this->apply_filters( 'is_long_term_user', $is_long_term_user );
2448
2449 if ( $is_long_term_user ) {
2450 $user_type = 'long-term';
2451 } else {
2452 if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
2453 $user_type = 'non-registered-and-non-anonymous-short-term';
2454 } else {
2455 $user_type = 'short-term';
2456 }
2457 }
2458
2459 $uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
2460
2461 $vars['reasons'] = $uninstall_reasons;
2462 }
2463
2464 $vars['subscription_cancellation_dialog_box_template_params'] = &$subscription_cancellation_dialog_box_template_params;
2465 $vars['show_deactivation_feedback_form'] = $show_deactivation_feedback_form;
2466 $vars['uninstall_confirmation_message'] = $uninstall_confirmation_message;
2467
2468 /**
2469 * Load the HTML template for the deactivation feedback dialog box.
2470 *
2471 * @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.
2472 */
2473 fs_require_template( 'forms/deactivation/form.php', $vars );
2474 }
2475
2476 /**
2477 * @author Leo Fajardo (@leorw)
2478 * @since 1.1.2
2479 *
2480 * @param string $user_type
2481 *
2482 * @return array The uninstall reasons for the specified user type.
2483 */
2484 function _get_uninstall_reasons( $user_type = 'long-term' ) {
2485 $module_type = $this->_module_type;
2486
2487 $internal_message_template_var = array(
2488 'id' => $this->_module_id
2489 );
2490
2491 $plan = $this->get_plan();
2492
2493 if ( $this->is_registered() && is_object( $plan ) && $plan->has_technical_support() ) {
2494 $contact_support_template = fs_get_template( 'forms/deactivation/contact.php', $internal_message_template_var );
2495 } else {
2496 $contact_support_template = '';
2497 }
2498
2499 $reason_found_better_plugin = array(
2500 'id' => self::REASON_FOUND_A_BETTER_PLUGIN,
2501 'text' => sprintf( $this->get_text_inline( 'I found a better %s', 'reason-found-a-better-plugin' ), $module_type ),
2502 'input_type' => 'textfield',
2503 'input_placeholder' => sprintf( $this->get_text_inline( "What's the %s's name?", 'placeholder-plugin-name' ), $module_type ),
2504 );
2505
2506 $reason_temporary_deactivation = array(
2507 'id' => self::REASON_TEMPORARY_DEACTIVATION,
2508 'text' => sprintf(
2509 $this->get_text_inline( "It's a temporary %s - I'm troubleshooting an issue", 'reason-temporary-x' ),
2510 strtolower( $this->is_plugin() ?
2511 $this->get_text_inline( 'Deactivation', 'deactivation' ) :
2512 $this->get_text_inline( 'Theme Switch', 'theme-switch' )
2513 )
2514 ),
2515 'input_type' => '',
2516 'input_placeholder' => ''
2517 );
2518
2519 $reason_other = array(
2520 'id' => self::REASON_OTHER,
2521 'text' => $this->get_text_inline( 'Other', 'reason-other' ),
2522 'input_type' => 'textfield',
2523 'input_placeholder' => ''
2524 );
2525
2526 $long_term_user_reasons = array(
2527 array(
2528 'id' => self::REASON_NO_LONGER_NEEDED,
2529 'text' => sprintf( $this->get_text_inline( 'I no longer need the %s', 'reason-no-longer-needed' ), $module_type ),
2530 'input_type' => '',
2531 'input_placeholder' => ''
2532 ),
2533 $reason_found_better_plugin,
2534 array(
2535 'id' => self::REASON_NEEDED_FOR_A_SHORT_PERIOD,
2536 'text' => sprintf( $this->get_text_inline( 'I only needed the %s for a short period', 'reason-needed-for-a-short-period' ), $module_type ),
2537 'input_type' => '',
2538 'input_placeholder' => ''
2539 ),
2540 array(
2541 'id' => self::REASON_BROKE_MY_SITE,
2542 'text' => sprintf( $this->get_text_inline( 'The %s broke my site', 'reason-broke-my-site' ), $module_type ),
2543 'input_type' => '',
2544 'input_placeholder' => '',
2545 'internal_message' => $contact_support_template
2546 ),
2547 array(
2548 'id' => self::REASON_SUDDENLY_STOPPED_WORKING,
2549 'text' => sprintf( $this->get_text_inline( 'The %s suddenly stopped working', 'reason-suddenly-stopped-working' ), $module_type ),
2550 'input_type' => '',
2551 'input_placeholder' => '',
2552 'internal_message' => $contact_support_template
2553 )
2554 );
2555
2556 if ( $this->is_paying() ) {
2557 $long_term_user_reasons[] = array(
2558 'id' => self::REASON_CANT_PAY_ANYMORE,
2559 'text' => $this->get_text_inline( "I can't pay for it anymore", 'reason-cant-pay-anymore' ),
2560 'input_type' => 'textfield',
2561 'input_placeholder' => $this->get_text_inline( 'What price would you feel comfortable paying?', 'placeholder-comfortable-price' )
2562 );
2563 }
2564
2565 $reason_dont_share_info = array(
2566 'id' => self::REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION,
2567 'text' => $this->get_text_inline( "I don't like to share my information with you", 'reason-dont-like-to-share-my-information' ),
2568 'input_type' => '',
2569 'input_placeholder' => ''
2570 );
2571
2572 /**
2573 * If the current user has selected the "don't share data" reason in the deactivation feedback modal, inform the
2574 * user by showing additional message that he doesn't have to share data and can just choose to skip the opt-in
2575 * (the Skip button is included in the message to show). This message will only be shown if anonymous mode is
2576 * enabled and the user's account is currently not in pending activation state (similar to the way the Skip
2577 * button in the opt-in form is shown/hidden).
2578 */
2579 if ( $this->is_enable_anonymous() && ! $this->is_pending_activation() ) {
2580 $reason_dont_share_info['internal_message'] = fs_get_template( 'forms/deactivation/retry-skip.php', $internal_message_template_var );
2581 }
2582
2583 $uninstall_reasons = array(
2584 'long-term' => $long_term_user_reasons,
2585 'non-registered-and-non-anonymous-short-term' => array(
2586 array(
2587 'id' => self::REASON_DIDNT_WORK,
2588 'text' => sprintf( $this->get_text_inline( "The %s didn't work", 'reason-didnt-work' ), $module_type ),
2589 'input_type' => '',
2590 'input_placeholder' => ''
2591 ),
2592 $reason_dont_share_info,
2593 $reason_found_better_plugin
2594 ),
2595 'short-term' => array(
2596 array(
2597 'id' => self::REASON_COULDNT_MAKE_IT_WORK,
2598 'text' => $this->get_text_inline( "I couldn't understand how to make it work", 'reason-couldnt-make-it-work' ),
2599 'input_type' => '',
2600 'input_placeholder' => '',
2601 'internal_message' => $contact_support_template
2602 ),
2603 $reason_found_better_plugin,
2604 array(
2605 'id' => self::REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE,
2606 '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 ),
2607 'input_type' => 'textarea',
2608 'input_placeholder' => $this->get_text_inline( 'What feature?', 'placeholder-feature' )
2609 ),
2610 array(
2611 'id' => self::REASON_NOT_WORKING,
2612 'text' => sprintf( $this->get_text_inline( 'The %s is not working', 'reason-not-working' ), $module_type ),
2613 'input_type' => 'textarea',
2614 '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' )
2615 ),
2616 array(
2617 'id' => self::REASON_NOT_WHAT_I_WAS_LOOKING_FOR,
2618 'text' => $this->get_text_inline( "It's not what I was looking for", 'reason-not-what-i-was-looking-for' ),
2619 'input_type' => 'textarea',
2620 'input_placeholder' => $this->get_text_inline( "What you've been looking for?", 'placeholder-what-youve-been-looking-for' )
2621 ),
2622 array(
2623 'id' => self::REASON_DIDNT_WORK_AS_EXPECTED,
2624 'text' => sprintf( $this->get_text_inline( "The %s didn't work as expected", 'reason-didnt-work-as-expected' ), $module_type ),
2625 'input_type' => 'textarea',
2626 'input_placeholder' => $this->get_text_inline( 'What did you expect?', 'placeholder-what-did-you-expect' )
2627 )
2628 )
2629 );
2630
2631 // Randomize the reasons for the current user type.
2632 shuffle( $uninstall_reasons[ $user_type ] );
2633
2634 // Keep the following reasons as the last items in the list.
2635 $uninstall_reasons[ $user_type ][] = $reason_temporary_deactivation;
2636 $uninstall_reasons[ $user_type ][] = $reason_other;
2637
2638 $uninstall_reasons = $this->apply_filters( 'uninstall_reasons', $uninstall_reasons );
2639
2640 return $uninstall_reasons[ $user_type ];
2641 }
2642
2643 /**
2644 * Called after the user has submitted his reason for deactivating the plugin.
2645 *
2646 * @author Leo Fajardo (@leorw)
2647 * @since 1.1.2
2648 */
2649 function _submit_uninstall_reason_action() {
2650 $this->_logger->entrance();
2651
2652 $this->check_ajax_referer( 'submit_uninstall_reason' );
2653
2654 $reason_id = fs_request_get( 'reason_id' );
2655
2656 // Check if the given reason ID is an unsigned integer.
2657 if ( ! ctype_digit( $reason_id ) ) {
2658 exit;
2659 }
2660
2661 $reason_info = trim( fs_request_get( 'reason_info', '' ) );
2662 if ( ! empty( $reason_info ) ) {
2663 $reason_info = substr( $reason_info, 0, 128 );
2664 }
2665
2666 $reason = (object) array(
2667 'id' => $reason_id,
2668 'info' => $reason_info,
2669 'is_anonymous' => fs_request_get_bool( 'is_anonymous' )
2670 );
2671
2672 $this->_storage->store( 'uninstall_reason', $reason );
2673
2674 if ( self::REASON_TEMPORARY_DEACTIVATION == $reason->id ) {
2675 $snooze_period = fs_request_get( 'snooze_period' );
2676
2677 if ( is_numeric( $snooze_period ) && 0 < $snooze_period ) {
2678 self::snooze_deactivation_form( (int) $snooze_period );
2679 }
2680 }
2681
2682 /**
2683 * If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do
2684 * not support uninstall hook.
2685 *
2686 * @author Leo Fajardo (@leorw)
2687 * @since 1.2.2
2688 */
2689 if ( $this->is_theme() ) {
2690 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
2691 FS_Plugin_Updater::instance( $this )->delete_update_data();
2692 }
2693
2694 $this->_uninstall_plugin_event( false );
2695 $this->remove_sdk_reference();
2696 }
2697
2698 // Print '1' for successful operation.
2699 echo 1;
2700 exit;
2701 }
2702
2703 #--------------------------------------------------------------------------------
2704 #region Deactivation Feedback Snoozing
2705 #--------------------------------------------------------------------------------
2706
2707 /**
2708 * @author Vova Feldman (@svovaf)
2709 * @since 2.4.3
2710 *
2711 * @param int $period
2712 *
2713 * @return bool True if the value was set, false otherwise.
2714 */
2715 private static function snooze_deactivation_form( $period ) {
2716 return ( 0 < $period && self::reset_deactivation_snoozing( $period ) );
2717 }
2718
2719 /**
2720 * Check if deactivation feedback form is snoozed.
2721 *
2722 * @author Vova Feldman (@svovaf)
2723 * @since 2.4.3
2724 *
2725 * @return bool
2726 */
2727 static function is_deactivation_snoozed() {
2728 $is_snoozed = ( ! is_multisite() || fs_is_network_admin() ) ?
2729 get_transient( 'fs_snooze_period' ) :
2730 get_site_transient( 'fs_snooze_period' );
2731
2732
2733 return ( 'true' === $is_snoozed );
2734 }
2735
2736 /**
2737 * Reset deactivation snoozing. When `$period` is `0` will stop deactivation snoozing by deleting the transients. Otherwise, will set the transients for the selected period.
2738 *
2739 * @param int $period Period in seconds.
2740 *
2741 * @author Vova Feldman (@svovaf)
2742 * @since 2.4.3
2743 */
2744 public static function reset_deactivation_snoozing( $period = 0 ) {
2745 $value = ( 0 === $period ) ? null : 'true';
2746
2747 if ( ! is_multisite() || fs_is_network_admin() ) {
2748 return set_transient( 'fs_snooze_period', $value, $period );
2749 } else {
2750 return set_site_transient( 'fs_snooze_period', $value, $period );
2751 }
2752 }
2753
2754 /**
2755 * The deactivation snooze expiration UNIX timestamp (in sec).
2756 *
2757 * @author Vova Feldman (@svovaf)
2758 * @since 2.4.3
2759 *
2760 * @return int
2761 */
2762 static function deactivation_snooze_expires_at() {
2763 return ( ! is_multisite() || fs_is_network_admin() ) ?
2764 (int) get_option( '_transient_timeout_fs_snooze_period' ) :
2765 (int) get_site_option( '_site_transient_timeout_fs_snooze_period' );
2766 }
2767
2768 #endregion
2769
2770 /**
2771 * @author Leo Fajardo (@leorw)
2772 * @since 2.1.4
2773 */
2774 function cancel_subscription_or_trial_ajax_action() {
2775 $this->_logger->entrance();
2776
2777 $this->check_ajax_referer( 'cancel_subscription_or_trial' );
2778
2779 $result = $this->cancel_subscription_or_trial( fs_request_get( 'plugin_id', $this->get_id() ), false );
2780
2781 if ( $this->is_api_error( $result ) ) {
2782 $this->shoot_ajax_failure( $result->error->message );
2783 }
2784
2785 $this->shoot_ajax_success();
2786 }
2787
2788 /**
2789 * @author Leo Fajardo (@leorw)
2790 * @since 2.1.4
2791 *
2792 * @param number $plugin_id
2793 *
2794 * @return object
2795 */
2796 private function cancel_subscription_or_trial( $plugin_id ) {
2797 $fs = null;
2798 if ( $plugin_id == $this->get_id() ) {
2799 $fs = $this;
2800 } else if ( $this->is_addon_activated( $plugin_id ) ) {
2801 $fs = self::get_instance_by_id( $plugin_id );
2802 }
2803
2804 $result = null;
2805
2806 if ( ! is_null( $fs ) ) {
2807 $result = $fs->is_paid_trial() ?
2808 $fs->_cancel_trial() :
2809 $fs->_downgrade_site();
2810 }
2811
2812 return $result;
2813 }
2814
2815 /**
2816 * @author Leo Fajardo (@leorw)
2817 * @since 2.0.2
2818 */
2819 function _delete_theme_update_data_action() {
2820 FS_Plugin_Updater::instance( $this )->delete_update_data();
2821 }
2822
2823 #endregion
2824
2825 #----------------------------------------------------------------------------------
2826 #region Instance
2827 #----------------------------------------------------------------------------------
2828
2829 /**
2830 * Main singleton instance.
2831 *
2832 * @author Vova Feldman (@svovaf)
2833 * @since 1.0.0
2834 *
2835 * @param number $module_id
2836 * @param string|bool $slug
2837 * @param bool $is_init Is initiation sequence.
2838 *
2839 * @return Freemius|false
2840 */
2841 static function instance( $module_id, $slug = false, $is_init = false ) {
2842 if ( empty( $module_id ) ) {
2843 return false;
2844 }
2845
2846 /**
2847 * 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.
2848 */
2849 self::_load_required_static();
2850
2851 if ( ! is_numeric( $module_id ) ) {
2852 if ( ! $is_init && true === $slug ) {
2853 $is_init = true;
2854 }
2855
2856 $slug = $module_id;
2857
2858 $module = FS_Plugin_Manager::instance( $slug )->get();
2859
2860 if ( is_object( $module ) ) {
2861 $module_id = $module->id;
2862 }
2863 }
2864
2865 $key = 'm_' . $module_id;
2866
2867 if ( ! isset( self::$_instances[ $key ] ) ) {
2868 self::$_instances[ $key ] = new Freemius( $module_id, $slug, $is_init );
2869 }
2870
2871 return self::$_instances[ $key ];
2872 }
2873
2874 /**
2875 * @author Vova Feldman (@svovaf)
2876 * @since 1.0.6
2877 *
2878 * @param number $addon_id
2879 *
2880 * @return bool
2881 */
2882 private static function has_instance( $addon_id ) {
2883 return isset( self::$_instances[ 'm_' . $addon_id ] );
2884 }
2885
2886 /**
2887 * @author Leo Fajardo (@leorw)
2888 * @since 1.2.2
2889 *
2890 * @param string|number $id_or_slug
2891 * @param string $module_type
2892 *
2893 * @return number|false
2894 */
2895 private static function get_module_id( $id_or_slug, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2896 if ( is_numeric( $id_or_slug ) ) {
2897 return $id_or_slug;
2898 }
2899
2900 foreach ( self::$_instances as $instance ) {
2901 // Also check the module type since there can be a plugin and a theme with the same slug.
2902 if ( ( $module_type === $instance->get_module_type() ) && ( $id_or_slug === $instance->get_slug() ) ) {
2903 return $instance->get_id();
2904 }
2905 }
2906
2907 return false;
2908 }
2909
2910 /**
2911 * @author Vova Feldman (@svovaf)
2912 * @since 1.0.6
2913 *
2914 * @param number $id
2915 *
2916 * @return false|Freemius
2917 */
2918 static function get_instance_by_id( $id ) {
2919 return isset ( self::$_instances[ 'm_' . $id ] ) ?
2920 self::$_instances[ 'm_' . $id ] :
2921 false;
2922 }
2923
2924 /**
2925 *
2926 * @author Vova Feldman (@svovaf)
2927 * @since 1.0.1
2928 *
2929 * @param string $plugin_file
2930 * @param string $module_type
2931 *
2932 * @return false|Freemius
2933 */
2934 static function get_instance_by_file( $plugin_file, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2935 $slug = self::find_slug_by_basename( $plugin_file );
2936
2937 return ( false !== $slug ) ?
2938 self::instance( self::get_module_id( $slug, $module_type ) ) :
2939 false;
2940 }
2941
2942 /**
2943 * @author Vova Feldman (@svovaf)
2944 * @since 1.0.6
2945 *
2946 * @return false|Freemius
2947 */
2948 function get_parent_instance() {
2949 return self::get_instance_by_id( $this->_plugin->parent_plugin_id );
2950 }
2951
2952 /**
2953 * @author Vova Feldman (@svovaf)
2954 * @since 1.0.6
2955 *
2956 * @param string|number $id_or_slug
2957 *
2958 * @return false|Freemius
2959 */
2960 function get_addon_instance( $id_or_slug ) {
2961 $addon_id = self::get_module_id( $id_or_slug );
2962
2963 return self::instance( $addon_id );
2964 }
2965
2966 /**
2967 * @return Freemius[]
2968 */
2969 static function _get_all_instances() {
2970 return self::$_instances;
2971 }
2972
2973 #endregion ------------------------------------------------------------------
2974
2975 /**
2976 * @author Vova Feldman (@svovaf)
2977 * @since 1.0.6
2978 *
2979 * @return bool
2980 */
2981 function is_parent_plugin_installed() {
2982 $is_active = self::has_instance( $this->_plugin->parent_plugin_id );
2983
2984 if ( $is_active ) {
2985 return true;
2986 }
2987
2988 /**
2989 * Parent module might be a theme. If that's the case, the add-on's FS
2990 * instance will be loaded prior to the theme's FS instance, therefore,
2991 * we need to check if it's active with a "look ahead".
2992 *
2993 * @author Vova Feldman
2994 * @since 1.2.2.3
2995 */
2996 global $fs_active_plugins;
2997 if ( is_object( $fs_active_plugins ) && is_array( $fs_active_plugins->plugins ) ) {
2998 $active_theme = wp_get_theme();
2999
3000 foreach ( $fs_active_plugins->plugins as $sdk => $module ) {
3001 if ( WP_FS__MODULE_TYPE_THEME === $module->type ) {
3002 if ( $module->plugin_path == $active_theme->get_stylesheet() ) {
3003 // Parent module is a theme and it's currently active.
3004 return true;
3005 }
3006 }
3007 }
3008 }
3009
3010 return false;
3011 }
3012
3013 /**
3014 * Check if add-on parent plugin in activation mode.
3015 *
3016 * @author Vova Feldman (@svovaf)
3017 * @since 1.0.7
3018 *
3019 * @return bool
3020 */
3021 function is_parent_in_activation() {
3022 $parent_fs = $this->get_parent_instance();
3023 if ( ! is_object( $parent_fs ) ) {
3024 return false;
3025 }
3026
3027 return ( $parent_fs->is_activation_mode() );
3028 }
3029
3030 /**
3031 * Is plugin in activation mode.
3032 *
3033 * @author Vova Feldman (@svovaf)
3034 * @since 1.0.7
3035 *
3036 * @param bool $and_on
3037 *
3038 * @return bool
3039 */
3040 function is_activation_mode( $and_on = true ) {
3041 return fs_is_network_admin() ?
3042 $this->is_network_activation_mode( $and_on ) :
3043 $this->is_site_activation_mode( $and_on );
3044 }
3045
3046 /**
3047 * Is plugin in activation mode.
3048 *
3049 * @author Vova Feldman (@svovaf)
3050 * @since 1.0.7
3051 *
3052 * @param bool $and_on
3053 *
3054 * @return bool
3055 */
3056 function is_site_activation_mode( $and_on = true ) {
3057 return (
3058 ( $this->is_on() || ! $and_on ) &&
3059 (
3060 ( $this->is_premium() && true === $this->_storage->require_license_activation ) ||
3061 (
3062 ( ! $this->is_registered() ||
3063 ( $this->is_only_premium() && ! $this->has_features_enabled_license() ) ) &&
3064 ( ! $this->is_enable_anonymous() ||
3065 ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) )
3066 )
3067 )
3068 );
3069 }
3070
3071 /**
3072 * Checks if the SDK in network activation mode.
3073 *
3074 * @author Leo Fajardo (@leorw)
3075 * @since 2.0.0
3076 *
3077 * @param bool $and_on
3078 *
3079 * @return bool
3080 */
3081 private function is_network_activation_mode( $and_on = true ) {
3082 if ( ! $this->_is_network_active ) {
3083 // Not network activated.
3084 return false;
3085 }
3086
3087 if ( $this->is_network_upgrade_mode() ) {
3088 // Special flag to enforce network activation mode to decide what to do with the sites that are not yet opted-in nor skipped.
3089 return true;
3090 }
3091
3092 if ( ! $this->is_site_activation_mode( $and_on ) ) {
3093 // 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.
3094 return false;
3095 }
3096
3097 if ( $this->is_network_delegated_connection() ) {
3098 // Super-admin delegated the connection to the site admins -> not activation mode.
3099 return false;
3100 }
3101
3102 if ( $this->is_network_anonymous() && true !== $this->_storage->require_license_activation ) {
3103 // Super-admin skipped the connection network wide -> not activation mode.
3104 return false;
3105 }
3106
3107 if ( $this->is_network_registered() ) {
3108 // Super-admin connected at least one site -> not activation mode.
3109 return false;
3110 }
3111
3112 return true;
3113 }
3114
3115 /**
3116 * Check if current page is the opt-in/pending-activation page.
3117 *
3118 * @author Vova Feldman (@svovaf)
3119 * @since 1.2.1.7
3120 *
3121 * @return bool
3122 */
3123 function is_activation_page() {
3124 if ( $this->_menu->is_activation_page( $this->show_opt_in_on_themes_page() ) ) {
3125 return true;
3126 }
3127
3128 if ( ! $this->is_activation_mode() ) {
3129 return false;
3130 }
3131
3132 // Check if current page is matching the activation page.
3133 return $this->is_matching_url( $this->get_activation_url() );
3134 }
3135
3136 /**
3137 * Check if URL path's are matching and that all querystring
3138 * arguments of the $sub_url exist in the $url with the same values.
3139 *
3140 * WARNING:
3141 * 1. This method doesn't check if the sub/domain are matching.
3142 * 2. Ignore case sensitivity.
3143 *
3144 * @author Vova Feldman (@svovaf)
3145 * @since 1.2.1.7
3146 *
3147 * @param string $sub_url
3148 * @param string $url If argument is not set, check if the sub_url matching the current's page URL.
3149 *
3150 * @return bool
3151 */
3152 private function is_matching_url( $sub_url, $url = '' ) {
3153 if ( empty( $url ) ) {
3154 $url = $_SERVER['REQUEST_URI'];
3155 }
3156
3157 $url = strtolower( $url );
3158 $sub_url = strtolower( $sub_url );
3159
3160 if ( parse_url( $sub_url, PHP_URL_PATH ) !== parse_url( $url, PHP_URL_PATH ) ) {
3161 // Different path - DO NOT OVERRIDE PAGE.
3162 return false;
3163 }
3164
3165 $url_params = fs_parse_url_params( $url );
3166 $sub_url_params = fs_parse_url_params( $sub_url );
3167
3168 foreach ( $sub_url_params as $key => $val ) {
3169 if ( ! isset( $url_params[ $key ] ) || $val != $url_params[ $key ] ) {
3170 // Not matching query string - DO NOT OVERRIDE PAGE.
3171 return false;
3172 }
3173 }
3174
3175 return true;
3176 }
3177
3178 /**
3179 * Get the basenames of all active plugins for specific blog. Including network activated plugins.
3180 *
3181 * @author Vova Feldman (@svovaf)
3182 * @since 2.0.0
3183 *
3184 * @param int $blog_id
3185 *
3186 * @return string[]
3187 */
3188 private static function get_active_plugins_basenames( $blog_id = 0 ) {
3189 if ( is_multisite() && $blog_id > 0 ) {
3190 $active_basenames = get_blog_option( $blog_id, 'active_plugins' );
3191 } else {
3192 $active_basenames = get_option( 'active_plugins' );
3193 }
3194
3195 if ( ! is_array( $active_basenames ) ) {
3196 $active_basenames = array();
3197 }
3198
3199 if ( is_multisite() ) {
3200 $network_active_basenames = get_site_option( 'active_sitewide_plugins' );
3201
3202 if ( is_array( $network_active_basenames ) && ! empty( $network_active_basenames ) ) {
3203 $active_basenames = array_merge( $active_basenames, array_keys( $network_active_basenames ) );
3204 }
3205 }
3206
3207 return $active_basenames;
3208 }
3209
3210 /**
3211 * @author Leo Fajardo (@leorw)
3212 * @since 2.3.0
3213 *
3214 * @param int $blog_id
3215 *
3216 * @return array
3217 */
3218 static function get_active_plugins_directories_map( $blog_id = 0 ) {
3219 $active_basenames = self::get_active_plugins_basenames( $blog_id );
3220
3221 $map = array();
3222
3223 foreach ( $active_basenames as $active_basename ) {
3224 $active_basename = fs_normalize_path( $active_basename );
3225
3226 if ( false === strpos( $active_basename, '/' ) ) {
3227 continue;
3228 }
3229
3230 $map[ dirname( $active_basename ) ] = true;
3231 }
3232
3233 return $map;
3234 }
3235
3236 /**
3237 * Get collection of all active plugins. Including network activated plugins.
3238 *
3239 * @author Vova Feldman (@svovaf)
3240 * @since 1.0.9
3241 *
3242 * @param int $blog_id Since 2.0.0
3243 *
3244 * @return array[string]array
3245 */
3246 private static function get_active_plugins( $blog_id = 0 ) {
3247 self::require_plugin_essentials();
3248
3249 $active_plugin = array();
3250 $all_plugins = fs_get_plugins();
3251 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3252
3253 foreach ( $active_plugins_basenames as $plugin_basename ) {
3254 $active_plugin[ $plugin_basename ] = $all_plugins[ $plugin_basename ];
3255 }
3256
3257 return $active_plugin;
3258 }
3259
3260 /**
3261 * Get collection of all site active plugins for a specified blog.
3262 *
3263 * @author Vova Feldman (@svovaf)
3264 * @since 2.0.0
3265 *
3266 * @param int $blog_id
3267 *
3268 * @return array[string]array
3269 */
3270 private static function get_site_active_plugins( $blog_id = 0 ) {
3271 $active_basenames = ( is_multisite() && $blog_id > 0 ) ?
3272 get_blog_option( $blog_id, 'active_plugins' ) :
3273 get_option( 'active_plugins' );
3274
3275 $active = array();
3276
3277 if ( ! is_array( $active_basenames ) ) {
3278 return $active;
3279 }
3280
3281 foreach ( $active_basenames as $basename ) {
3282 $active[ $basename ] = array(
3283 'is_active' => true,
3284 'Version' => '1.0', // Dummy version.
3285 'slug' => self::get_plugin_slug( $basename ),
3286 );
3287 }
3288
3289 return $active;
3290 }
3291
3292 /**
3293 * Get collection of all plugins with their activation status for a specified blog.
3294 *
3295 * @author Vova Feldman (@svovaf)
3296 * @since 1.1.8
3297 *
3298 * @param int $blog_id Since 2.0.0
3299 *
3300 * @return array Key is the plugin file path and the value is an array of the plugin data.
3301 */
3302 private static function get_all_plugins( $blog_id = 0 ) {
3303 self::require_plugin_essentials();
3304
3305 $all_plugins = fs_get_plugins();
3306
3307 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3308
3309 foreach ( $all_plugins as $basename => &$data ) {
3310 // By default set to inactive (next foreach update the active plugins).
3311 $data['is_active'] = false;
3312 // Enrich with plugin slug.
3313 $data['slug'] = self::get_plugin_slug( $basename );
3314 }
3315
3316 // Flag active plugins.
3317 foreach ( $active_plugins_basenames as $basename ) {
3318 if ( isset( $all_plugins[ $basename ] ) ) {
3319 $all_plugins[ $basename ]['is_active'] = true;
3320 }
3321 }
3322
3323 return $all_plugins;
3324 }
3325
3326 /**
3327 * Get collection of all plugins and if they are network level activated.
3328 *
3329 * @author Vova Feldman (@svovaf)
3330 * @since 2.0.0
3331 *
3332 * @return array Key is the plugin basename and the value is an array of the plugin data.
3333 */
3334 private static function get_network_plugins() {
3335 self::require_plugin_essentials();
3336
3337 $all_plugins = fs_get_plugins();
3338
3339 $network_active_basenames = is_multisite() ?
3340 get_site_option( 'active_sitewide_plugins' ) :
3341 array();
3342
3343 foreach ( $all_plugins as $basename => &$data ) {
3344 // By default set to inactive (next foreach update the active plugins).
3345 $data['is_active'] = false;
3346 // Enrich with plugin slug.
3347 $data['slug'] = self::get_plugin_slug( $basename );
3348 }
3349
3350 // Flag active plugins.
3351 foreach ( $network_active_basenames as $basename ) {
3352 if ( isset( $all_plugins[ $basename ] ) ) {
3353 $all_plugins[ $basename ]['is_active'] = true;
3354 }
3355 }
3356
3357 return $all_plugins;
3358 }
3359
3360 /**
3361 * Cached result of get_site_transient( 'update_plugins' )
3362 *
3363 * @author Vova Feldman (@svovaf)
3364 * @since 1.1.8
3365 *
3366 * @var object
3367 */
3368 private static $_plugins_info;
3369
3370 /**
3371 * Helper function to get specified plugin's slug.
3372 *
3373 * @author Vova Feldman (@svovaf)
3374 * @since 1.1.8
3375 *
3376 * @param $basename
3377 *
3378 * @return string
3379 */
3380 private static function get_plugin_slug( $basename ) {
3381 if ( ! isset( self::$_plugins_info ) ) {
3382 self::$_plugins_info = get_site_transient( 'update_plugins' );
3383 }
3384
3385 $slug = '';
3386
3387 if ( is_object( self::$_plugins_info ) ) {
3388 if ( isset( self::$_plugins_info->no_update ) &&
3389 isset( self::$_plugins_info->no_update[ $basename ] ) &&
3390 ! empty( self::$_plugins_info->no_update[ $basename ]->slug )
3391 ) {
3392 $slug = self::$_plugins_info->no_update[ $basename ]->slug;
3393 } else if ( isset( self::$_plugins_info->response ) &&
3394 isset( self::$_plugins_info->response[ $basename ] ) &&
3395 ! empty( self::$_plugins_info->response[ $basename ]->slug )
3396 ) {
3397 $slug = self::$_plugins_info->response[ $basename ]->slug;
3398 }
3399 }
3400
3401 if ( empty( $slug ) ) {
3402 // Try to find slug from FS data.
3403 $slug = self::find_slug_by_basename( $basename );
3404 }
3405
3406 if ( empty( $slug ) ) {
3407 // Fallback to plugin's folder name.
3408 $slug = dirname( $basename );
3409 }
3410
3411 return $slug;
3412 }
3413
3414 private static $_statics_loaded = false;
3415
3416 /**
3417 * Load static resources.
3418 *
3419 * @author Vova Feldman (@svovaf)
3420 * @since 1.0.1
3421 */
3422 private static function _load_required_static() {
3423 if ( self::$_statics_loaded ) {
3424 return;
3425 }
3426
3427 self::$_static_logger = FS_Logger::get_logger( WP_FS__SLUG, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
3428
3429 self::$_static_logger->entrance();
3430
3431 self::$_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
3432
3433 if ( is_multisite() ) {
3434 $has_skipped_migration = (
3435 // 'id_slug_type_path_map' - was never stored on older versions, therefore, not exists on the site level.
3436 null === self::$_accounts->get_option( 'id_slug_type_path_map', null, false ) &&
3437 // 'file_slug_map' stored on the site level, so it was running an SDK version before it was integrated with MS-network.
3438 null !== self::$_accounts->get_option( 'file_slug_map', null, false )
3439 );
3440
3441 /**
3442 * If the file_slug_map exists on the site level but doesn't exist on the
3443 * network level storage, it means that we need to process the storage with migration.
3444 *
3445 * 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.
3446 *
3447 * @author Vova Feldman (@svovaf)
3448 * @since 2.0.0
3449 */
3450 if (
3451 ( $has_skipped_migration && true !== self::$_accounts->get_option( 'ms_migration_complete', false, true ) ) ||
3452 ( null === self::$_accounts->get_option( 'file_slug_map', null, true ) &&
3453 null !== self::$_accounts->get_option( 'file_slug_map', null, false ) )
3454 ) {
3455 self::migrate_options_to_network();
3456 }
3457 }
3458
3459 self::$_global_admin_notices = FS_Admin_Notices::instance( 'global' );
3460
3461 FS_DebugManager::load_required_static();
3462
3463 if ( 0 == did_action( 'plugins_loaded' ) ) {
3464 add_action( 'plugins_loaded', array( 'Freemius', '_load_textdomain' ), 1 );
3465 }
3466
3467 $clone_manager = FS_Clone_Manager::instance();
3468 add_action( 'init', array( $clone_manager, '_init' ) );
3469
3470 add_action( 'admin_footer', array( 'Freemius', '_handle_submenu_external_link' ) );
3471
3472 if ( self::is_plugins_page() || self::is_themes_page() ) {
3473 add_action( 'admin_print_footer_scripts', array( 'Freemius', '_maybe_add_beta_label_styles' ), 9 );
3474
3475 /**
3476 * Specifically use this hook so that the JS event handlers will work properly on the "Themes"
3477 * page.
3478 *
3479 * @author Leo Fajardo (@leorw)
3480 * @since 2.3.0
3481 */
3482 add_action( 'admin_footer-' . self::get_current_page(), array( 'Freemius', '_maybe_add_beta_label_to_plugins_and_handle_confirmation') );
3483 }
3484
3485 self::$_statics_loaded = true;
3486 }
3487
3488 public static function get_static_logger() {
3489 return self::$_static_logger;
3490 }
3491
3492 public static function get_accounts() {
3493 return self::$_accounts;
3494 }
3495
3496 #--------------------------------------------------------------------------------
3497 #region Clone
3498 #--------------------------------------------------------------------------------
3499
3500 /**
3501 * @author Leo Fajardo (@leorw)
3502 * @since 2.5.0
3503 *
3504 * @param bool $only_if_manual_resolution_is_not_hidden
3505 *
3506 * @return bool
3507 */
3508 private function is_unresolved_clone( $only_if_manual_resolution_is_not_hidden = false ) {
3509 if ( ! $this->is_clone( $only_if_manual_resolution_is_not_hidden ) ) {
3510 return false;
3511 }
3512
3513 return FS_Clone_Manager::instance()->has_temporary_duplicate_mode_expired();
3514 }
3515
3516 /**
3517 * @author Leo Fajardo (@leorw)
3518 * @since 2.5.0
3519 *
3520 * @param bool $only_if_manual_resolution_is_not_hidden
3521 */
3522 function is_clone( $only_if_manual_resolution_is_not_hidden = false ) {
3523 if ( ! is_object( $this->_site ) ) {
3524 return false;
3525 }
3526
3527 $blog_id = null;
3528
3529 if (
3530 fs_is_network_admin() &&
3531 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
3532 ) {
3533 // Ensure that we're comparing the network install's URL with the relevant subsite's URL.
3534 $blog_id = $this->_storage->network_install_blog_id;
3535 }
3536
3537 $site_url = Freemius::get_unfiltered_site_url( $blog_id, true, true );
3538
3539 if ( ! $this->_site->is_clone( $site_url ) ) {
3540 return false;
3541 }
3542
3543 return (
3544 ! $only_if_manual_resolution_is_not_hidden ||
3545 ! FS_Clone_Manager::instance()->should_hide_manual_resolution()
3546 );
3547 }
3548
3549 /**
3550 * @author Leo Fajardo (@leorw)
3551 * @since 2.5.0
3552 *
3553 * @param int|null $blog_id
3554 * @param bool $strip_protocol
3555 * @param bool $add_trailing_slash
3556 *
3557 * @return string
3558 */
3559 static function get_unfiltered_site_url( $blog_id = null, $strip_protocol = false, $add_trailing_slash = false ) {
3560 $url = ( ! is_multisite() && defined( 'WP_SITEURL' ) ) ? WP_SITEURL : self::get_site_url_from_wp_option( $blog_id );
3561
3562 if ( $strip_protocol ) {
3563 $url = fs_strip_url_protocol( $url );
3564 }
3565
3566 if ( $add_trailing_slash ) {
3567 $url = trailingslashit( $url );
3568 }
3569
3570 return $url;
3571 }
3572
3573 /**
3574 * @author Leo Fajardo (@leorw)
3575 * @since 2.6.0
3576 *
3577 * @param int|null $blog_id
3578 *
3579 * @return string
3580 */
3581 private static function get_site_url_from_wp_option( $blog_id = null ) {
3582 global $wp_filter;
3583
3584 $site_url_filters = array(
3585 'site_url' => null,
3586 'pre_option_siteurl' => null,
3587 'default_option_siteurl' => null,
3588 'option_siteurl' => null,
3589 );
3590
3591 // Detach all URL-related filters to get the actual site's URL (stripped of potential manipulations by multilingual plugins).
3592 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3593 if ( ! empty( $wp_filter[ $hook_name ] ) ) {
3594 $site_url_filters[ $hook_name ] = $wp_filter[ $hook_name ];
3595 unset( $wp_filter[ $hook_name ] );
3596 }
3597 }
3598
3599 $url = get_site_url( $blog_id );
3600
3601 // Re-attach the filters back.
3602 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3603 if ( ! empty( $site_url_filter ) ) {
3604 $wp_filter[ $hook_name ] = $site_url_filter;
3605 }
3606 }
3607
3608 return $url;
3609 }
3610
3611 /**
3612 * @author Leo Fajardo (@leorw)
3613 * @since 2.5.0
3614 *
3615 * @param number $site_id
3616 */
3617 function fetch_install_by_id( $site_id ) {
3618 return $this->get_current_or_network_user_api_scope()->get( "/installs/{$site_id}.json" );
3619 }
3620
3621 /**
3622 * @author Leo Fajardo (@leorw)
3623 * @since 2.5.0
3624 *
3625 * @return string|object|bool
3626 */
3627 function _handle_long_term_duplicate() {
3628 $this->_logger->entrance();
3629
3630 $this->delete_current_install( false );
3631
3632 $license = null;
3633
3634 if (
3635 is_object( $this->_license ) &&
3636 ! $this->_license->is_utilized(
3637 ( WP_FS__IS_LOCALHOST_FOR_SERVER || FS_Site::is_localhost_by_address( self::get_unfiltered_site_url() ) )
3638 )
3639 ) {
3640 $license = $this->_license;
3641 }
3642
3643 return $this->opt_in(
3644 false,
3645 false,
3646 false,
3647 ( is_object( $license ) ? $license->secret_key : false ),
3648 false,
3649 false,
3650 false,
3651 null,
3652 array(),
3653 false,
3654 ( is_object( $license ) ? $license->user_id : null )
3655 );
3656 }
3657
3658 #endregion
3659
3660 /**
3661 * @author Leo Fajardo (@leorw)
3662 *
3663 * @since 2.1.3
3664 */
3665 public static function migrate_options_to_network() {
3666 self::migrate_accounts_to_network();
3667
3668 // Migrate API options from site level to network level.
3669 $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
3670 $api_network_options->migrate_to_network();
3671
3672 // Migrate API cache to network level storage.
3673 FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
3674
3675 self::$_accounts->set_option( 'ms_migration_complete', true, true );
3676 }
3677
3678 #----------------------------------------------------------------------------------
3679 #region Localization
3680 #----------------------------------------------------------------------------------
3681
3682 /**
3683 * Load framework's text domain.
3684 *
3685 * @author Vova Feldman (@svovaf)
3686 * @since 1.2.1
3687 */
3688 static function _load_textdomain() {
3689 if ( ! is_admin() ) {
3690 return;
3691 }
3692
3693 global $fs_active_plugins;
3694
3695 // Works both for plugins and themes.
3696 load_plugin_textdomain(
3697 'freemius',
3698 false,
3699 $fs_active_plugins->newest->sdk_path . '/languages/'
3700 );
3701 }
3702
3703 #endregion
3704
3705 #----------------------------------------------------------------------------------
3706 #region Connectivity Issues
3707 #----------------------------------------------------------------------------------
3708
3709 /**
3710 * Check if Freemius should be turned on for the current plugin install.
3711 *
3712 * Note:
3713 * $this->_is_on is updated in has_api_connectivity()
3714 *
3715 * @author Vova Feldman (@svovaf)
3716 * @since 1.0.9
3717 *
3718 * @return bool
3719 */
3720 function is_on() {
3721 self::$_static_logger->entrance();
3722
3723 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
3724 return false;
3725 }
3726
3727 if ( isset( $this->_is_on ) ) {
3728 return $this->_is_on;
3729 }
3730
3731 // If already installed or pending then sure it's on :)
3732 if ( $this->is_registered() || $this->is_pending_activation() ) {
3733 $this->_is_on = true;
3734
3735 return true;
3736 }
3737
3738 return false;
3739 }
3740
3741 /**
3742 * @author Vova Feldman (@svovaf)
3743 * @since 1.1.7.3
3744 *
3745 * @param bool $flush_if_no_connectivity
3746 *
3747 * @return bool
3748 */
3749 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
3750 if ( ! isset( $this->_storage->connectivity_test ) ) {
3751 // Connectivity test was never executed, or cache was cleared.
3752 return true;
3753 }
3754
3755 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
3756 if ( WP_FS__IS_HTTP_REQUEST ) {
3757 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
3758 // Domain changed.
3759 return true;
3760 }
3761
3762 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
3763 // Server IP changed.
3764 return true;
3765 }
3766 }
3767 }
3768
3769 if ( $this->_storage->connectivity_test['is_connected'] &&
3770 $this->_storage->connectivity_test['is_active']
3771 ) {
3772 // API connected and Freemius is active - no need to run connectivity check.
3773 return false;
3774 }
3775
3776 if ( $flush_if_no_connectivity ) {
3777 /**
3778 * If explicitly asked to flush when no connectivity - do it only
3779 * if at least 10 sec passed from the last API connectivity test.
3780 */
3781 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
3782 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
3783 }
3784
3785 /**
3786 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
3787 */
3788 $version = $this->get_plugin_version();
3789 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
3790 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
3791 return true;
3792 }
3793
3794 return false;
3795 }
3796
3797 /**
3798 * @author Leo Fajardo (@leorw)
3799 * @since 2.5.4
3800 *
3801 * @param bool $is_update
3802 *
3803 * @return bool
3804 */
3805 private function should_turn_fs_on( $is_update = true ) {
3806 if (
3807 empty( $this->_plugin->opt_in_moderation ) ||
3808 ! is_array( $this->_plugin->opt_in_moderation )
3809 ) {
3810 return true;
3811 }
3812
3813 $optin_config = $this->_plugin->opt_in_moderation;
3814
3815 if (
3816 WP_FS__IS_LOCALHOST &&
3817 ( ! isset( $optin_config['localhost'] ) || false !== $optin_config['localhost'] )
3818 ) {
3819 return true;
3820 }
3821
3822 $optin_config_key = $is_update ?
3823 'updates' :
3824 'new';
3825
3826 if ( ! isset( $optin_config[ $optin_config_key ] ) ) {
3827 return true;
3828 }
3829
3830 $visibility_percentage = $optin_config[ $optin_config_key ];
3831
3832 if ( 0 == $visibility_percentage ) {
3833 return false;
3834 }
3835
3836 if ( ! is_numeric( $visibility_percentage ) ) {
3837 return true;
3838 }
3839
3840 $min = 1;
3841 $max = 100;
3842
3843 if ( function_exists( 'random_int' ) ) {
3844 $random = random_int( $min, $max ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.random_intFound
3845 } else {
3846 $random = rand( $min, $max );
3847 }
3848
3849 return ( $random <= $visibility_percentage );
3850 }
3851
3852 /**
3853 * Check if there's any connectivity issue to Freemius API.
3854 *
3855 * @author Vova Feldman (@svovaf)
3856 * @since 1.0.9
3857 *
3858 * @param bool $flush_if_no_connectivity
3859 *
3860 * @return bool|null
3861 */
3862 function has_api_connectivity( $flush_if_no_connectivity = false ) {
3863 $this->_logger->entrance();
3864
3865 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
3866 return $this->_has_api_connection;
3867 }
3868
3869 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
3870 isset( $this->_storage->connectivity_test ) &&
3871 true === $this->_storage->connectivity_test['is_connected']
3872 ) {
3873 $this->clear_connectivity_info();
3874 }
3875
3876 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
3877 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
3878 /**
3879 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
3880 *
3881 * @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.
3882 */
3883 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
3884 $this->is_premium() ||
3885 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3886
3887 return $this->_has_api_connection;
3888 }
3889
3890 if (
3891 ! empty( $this->_storage->connectivity_test ) &&
3892 isset( $this->_storage->connectivity_test['is_active'] )
3893 ) {
3894 $is_connected = isset( $this->_storage->connectivity_test['is_connected'] ) ?
3895 $this->_storage->connectivity_test['is_connected'] :
3896 null;
3897 $is_active = ( $this->_storage->connectivity_test['is_active'] || is_object( $this->_site ) );
3898 } else {
3899 $is_connected = null;
3900 $is_active = $this->should_turn_fs_on( $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() ) );
3901 }
3902
3903 $this->store_connectivity_info( (object) array( 'is_active' => $is_active ), $is_connected );
3904
3905 if ( $is_active ) {
3906 $this->_is_on = true;
3907 }
3908
3909 return $this->_has_api_connection;
3910 }
3911
3912 /**
3913 * @author Leo Fajardo (@leorw)
3914 * @since 2.5.4
3915 */
3916 private function clear_connectivity_info() {
3917 unset( $this->_storage->connectivity_test );
3918
3919 FS_Api::clear_force_http_flag();
3920 }
3921
3922 /**
3923 * @author Vova Feldman (@svovaf)
3924 * @since 1.1.7.4
3925 *
3926 * @param object $pong
3927 * @param bool|null $is_connected
3928 */
3929 private function store_connectivity_info( $pong, $is_connected ) {
3930 $this->_logger->entrance();
3931
3932 $version = $this->get_plugin_version();
3933
3934 if ( false === $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
3935 $is_active = false;
3936 } else {
3937 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
3938 }
3939
3940 $is_active = $this->apply_filters(
3941 'is_on',
3942 $is_active,
3943 $this->is_plugin_update(),
3944 $version
3945 );
3946
3947 $this->_storage->connectivity_test = array(
3948 'is_connected' => $is_connected,
3949 'host' => isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '',
3950 'server_ip' => WP_FS__REMOTE_ADDR,
3951 'is_active' => $is_active,
3952 'timestamp' => WP_FS__SCRIPT_START_TIME,
3953 // Last version with connectivity attempt.
3954 'version' => $version,
3955 );
3956
3957 $this->_has_api_connection = $is_connected;
3958 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3959 }
3960
3961 /**
3962 * @author Leo Fajardo (@leorw)
3963 * @since 2.5.4
3964 *
3965 * @param bool $is_connected
3966 */
3967 private function update_connectivity_info( $is_connected ) {
3968 $this->store_connectivity_info(
3969 // 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.
3970 (object) array( 'is_active' => true ),
3971 $is_connected
3972 );
3973 }
3974
3975 /**
3976 * Force turning Freemius on.
3977 *
3978 * @author Vova Feldman (@svovaf)
3979 * @since 1.1.8.1
3980 *
3981 * @return bool TRUE if successfully turned on.
3982 */
3983 private function turn_on() {
3984 $this->_logger->entrance();
3985
3986 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
3987 return false;
3988 }
3989
3990 $updated_connectivity = $this->_storage->connectivity_test;
3991 $updated_connectivity['is_active'] = true;
3992 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
3993 $this->_storage->connectivity_test = $updated_connectivity;
3994
3995 $this->_is_on = true;
3996
3997 return true;
3998 }
3999
4000 /**
4001 * Anonymous and unique site identifier (Hash).
4002 *
4003 * @author Vova Feldman (@svovaf)
4004 * @since 1.1.0
4005 *
4006 * @param null|int $blog_id Since 2.0.0
4007 *
4008 * @return string
4009 */
4010 function get_anonymous_id( $blog_id = null ) {
4011 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
4012
4013 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
4014 $key = self::get_unfiltered_site_url( $blog_id, true );
4015
4016 $secure_auth = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
4017 if ( empty( $secure_auth ) ||
4018 false !== strpos( $secure_auth, ' ' ) ||
4019 'put your unique phrase here' === $secure_auth
4020 ) {
4021 // Protect against default auth key.
4022 $secure_auth = md5( microtime() );
4023 }
4024
4025 /**
4026 * Base the unique identifier on the WP secure authentication key. Which
4027 * turns the key into a secret anonymous identifier. This will help us
4028 * to avoid duplicate installs generation on the backend upon opt-in.
4029 *
4030 * @author Vova Feldman (@svovaf)
4031 * @since 1.2.3
4032 */
4033 $unique_id = md5( $key . $secure_auth );
4034
4035 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
4036 }
4037
4038 $this->_logger->departure( $unique_id );
4039
4040 return $unique_id;
4041 }
4042
4043 /**
4044 * Returns anonymous network ID.
4045 *
4046 * @since 2.4.3
4047 *
4048 * @return string
4049 */
4050 function get_anonymous_network_id() {
4051 return $this->get_anonymous_id( get_network()->site_id );
4052 }
4053
4054 /**
4055 * @author Vova Feldman (@svovaf)
4056 * @since 1.1.7.4
4057 *
4058 * @return \WP_User
4059 */
4060 static function _get_current_wp_user() {
4061 self::require_pluggable_essentials();
4062 self::wp_cookie_constants();
4063
4064 return wp_get_current_user();
4065 }
4066
4067 /**
4068 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
4069 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
4070 * is network activated the cookie constants are only configured after the network
4071 * plugins activation, therefore, if we don't define those constants WP will throw
4072 * PHP warnings/notices.
4073 *
4074 * @author Vova Feldman (@svovaf)
4075 * @since 2.1.1
4076 */
4077 private static function wp_cookie_constants() {
4078 if ( defined( 'LOGGED_IN_COOKIE' ) &&
4079 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
4080 ) {
4081 return;
4082 }
4083
4084 /**
4085 * Used to guarantee unique hash cookies
4086 *
4087 * @since 1.5.0
4088 */
4089 if ( ! defined( 'COOKIEHASH' ) ) {
4090 $siteurl = get_site_option( 'siteurl' );
4091 if ( $siteurl ) {
4092 define( 'COOKIEHASH', md5( $siteurl ) );
4093 } else {
4094 define( 'COOKIEHASH', '' );
4095 }
4096 }
4097
4098 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
4099 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
4100 }
4101
4102 /**
4103 * @since 2.5.0
4104 */
4105 if ( ! defined( 'AUTH_COOKIE' ) ) {
4106 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
4107 }
4108
4109 /**
4110 * @since 2.6.0
4111 */
4112 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
4113 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
4114 }
4115 }
4116
4117 /**
4118 * @author Vova Feldman (@svovaf)
4119 * @since 2.1.0
4120 *
4121 * @return int
4122 */
4123 static function get_current_wp_user_id() {
4124 $wp_user = self::_get_current_wp_user();
4125
4126 return $wp_user->ID;
4127 }
4128
4129 /**
4130 * @author Vova Feldman (@svovaf)
4131 * @since 1.2.1.7
4132 *
4133 * @param string $email
4134 *
4135 * @return bool
4136 */
4137 static function is_valid_email( $email ) {
4138 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4139 return false;
4140 }
4141
4142 $parts = explode( '@', $email );
4143
4144 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4145 return false;
4146 }
4147
4148 $blacklist = array(
4149 'admin.',
4150 'webmaster.',
4151 'localhost.',
4152 'dev.',
4153 'development.',
4154 'test.',
4155 'stage.',
4156 'staging.',
4157 );
4158
4159 // Make sure domain is not one of the blacklisted.
4160 foreach ( $blacklist as $invalid ) {
4161 if ( 0 === strpos( $parts[1], $invalid ) ) {
4162 return false;
4163 }
4164 }
4165
4166 // Get the UTF encoded domain name.
4167 /**
4168 * @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.
4169 * @author - @swashata
4170 */
4171 $is_new_idn_available = (
4172 version_compare( PHP_VERSION, '5.6.40') > 0 &&
4173 defined( 'IDNA_DEFAULT' ) &&
4174 defined( 'INTL_IDNA_VARIANT_UTS46' )
4175 );
4176 if ( $is_new_idn_available ) {
4177 $domain = idn_to_ascii( $parts[1], IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 );
4178 } else {
4179 $domain = idn_to_ascii( $parts[1] ); // phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet
4180 }
4181
4182 $domain = $domain . '.';
4183
4184 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4185 }
4186
4187 #endregion
4188
4189 #----------------------------------------------------------------------------------
4190 #region Email
4191 #----------------------------------------------------------------------------------
4192
4193 /**
4194 * Generates and sends an HTML email with customizable sections.
4195 *
4196 * @author Leo Fajardo (@leorw)
4197 * @since 1.1.2
4198 *
4199 * @param string $to_address
4200 * @param string $subject
4201 * @param array $sections
4202 * @param array $headers
4203 *
4204 * @return bool Whether the email contents were sent successfully.
4205 */
4206 private function send_email(
4207 $to_address,
4208 $subject,
4209 $sections = array(),
4210 $headers = array()
4211 ) {
4212 $default_sections = $this->get_email_sections();
4213
4214 // Insert new sections or replace the default email sections.
4215 if ( is_array( $sections ) && ! empty( $sections ) ) {
4216 foreach ( $sections as $section_id => $custom_section ) {
4217 if ( ! isset( $default_sections[ $section_id ] ) ) {
4218 // If the section does not exist, add it.
4219 $default_sections[ $section_id ] = $custom_section;
4220 } else {
4221 // If the section already exists, override it.
4222 $current_section = $default_sections[ $section_id ];
4223
4224 // Replace the current section's title if a custom section title exists.
4225 if ( isset( $custom_section['title'] ) ) {
4226 $current_section['title'] = $custom_section['title'];
4227 }
4228
4229 // Insert new rows under the current section or replace the default rows.
4230 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4231 foreach ( $custom_section['rows'] as $row_id => $row ) {
4232 $current_section['rows'][ $row_id ] = $row;
4233 }
4234 }
4235
4236 $default_sections[ $section_id ] = $current_section;
4237 }
4238 }
4239 }
4240
4241 $vars = array( 'sections' => $default_sections );
4242 $message = fs_get_template( 'email.php', $vars );
4243
4244 // Set the type of email to HTML.
4245 $headers[] = 'Content-type: text/html; charset=UTF-8';
4246
4247 $header_string = implode( "\r\n", $headers );
4248
4249 return wp_mail(
4250 $to_address,
4251 $subject,
4252 $message,
4253 $header_string
4254 );
4255 }
4256
4257 /**
4258 * Generates the data for the sections of the email content.
4259 *
4260 * @author Leo Fajardo (@leorw)
4261 * @since 1.1.2
4262 *
4263 * @return array
4264 */
4265 private function get_email_sections() {
4266 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4267 $current_user = self::_get_current_wp_user();
4268
4269 // Retrieve the cURL version information so that we can get the version number below.
4270 $curl_version_information = curl_version();
4271
4272 $active_plugin = self::get_active_plugins();
4273
4274 // Generate the list of active plugins separated by new line.
4275 $active_plugin_string = '';
4276 foreach ( $active_plugin as $plugin ) {
4277 $active_plugin_string .= sprintf(
4278 '<a href="%s">%s</a> [v%s]<br>',
4279 $plugin['PluginURI'],
4280 $plugin['Name'],
4281 $plugin['Version']
4282 );
4283 }
4284
4285 $server_ip = WP_FS__REMOTE_ADDR;
4286
4287 // Add PHP info for deeper investigation.
4288 ob_start();
4289 phpinfo();
4290 $php_info = ob_get_clean();
4291
4292 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4293
4294 // Generate the default email sections.
4295 $sections = array(
4296 'sdk' => array(
4297 'title' => 'SDK',
4298 'rows' => array(
4299 'fs_version' => array( 'FS Version', $this->version ),
4300 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4301 )
4302 ),
4303 'plugin' => array(
4304 'title' => ucfirst( $this->get_module_type() ),
4305 'rows' => array(
4306 'name' => array( 'Name', $this->get_plugin_name() ),
4307 'version' => array( 'Version', $this->get_plugin_version() )
4308 )
4309 ),
4310 'api' => array(
4311 'title' => 'API Subdomain',
4312 'rows' => array(
4313 'dns' => array(
4314 'DNS_CNAME',
4315 function_exists( 'dns_get_record' ) ?
4316 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4317 'dns_get_record() disabled/blocked'
4318 ),
4319 'ip' => array(
4320 'IP',
4321 function_exists( 'gethostbyname' ) ?
4322 gethostbyname( $api_domain ) :
4323 'gethostbyname() disabled/blocked'
4324 ),
4325 ),
4326 ),
4327 'site' => array(
4328 'title' => 'Site',
4329 'rows' => array(
4330 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4331 'address' => array( 'Address', site_url() ),
4332 'host' => array(
4333 'HTTP_HOST',
4334 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4335 ),
4336 'hosting' => array(
4337 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4338 fs_request_get( 'hosting_company' ) :
4339 'Unknown',
4340 ),
4341 'server_addr' => array(
4342 'SERVER_ADDR',
4343 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4344 )
4345 )
4346 ),
4347 'user' => array(
4348 'title' => 'User',
4349 'rows' => array(
4350 'email' => array( 'Email', $current_user->user_email ),
4351 'first' => array( 'First', $current_user->user_firstname ),
4352 'last' => array( 'Last', $current_user->user_lastname )
4353 )
4354 ),
4355 'plugins' => array(
4356 'title' => 'Plugins',
4357 'rows' => array(
4358 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4359 )
4360 ),
4361 'php_info' => array(
4362 'title' => 'PHP Info',
4363 'rows' => array(
4364 'info' => array( $php_info )
4365 ),
4366 )
4367 );
4368
4369 // Allow the sections to be modified by other code.
4370 $sections = $this->apply_filters( 'email_template_sections', $sections );
4371
4372 return $sections;
4373 }
4374
4375 #endregion
4376
4377 #----------------------------------------------------------------------------------
4378 #region Initialization
4379 #----------------------------------------------------------------------------------
4380
4381 /**
4382 * Init plugin's Freemius instance.
4383 *
4384 * @author Vova Feldman (@svovaf)
4385 * @since 1.0.1
4386 *
4387 * @param number $id
4388 * @param string $public_key
4389 * @param bool $is_live
4390 * @param bool $is_premium
4391 */
4392 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4393 $this->_logger->entrance();
4394
4395 $this->dynamic_init( array(
4396 'id' => $id,
4397 'public_key' => $public_key,
4398 'is_live' => $is_live,
4399 'is_premium' => $is_premium,
4400 ) );
4401 }
4402
4403 /**
4404 * Dynamic initiator, originally created to support initiation
4405 * with parent_id for add-ons.
4406 *
4407 * @author Vova Feldman (@svovaf)
4408 * @since 1.0.6
4409 *
4410 * @param array $plugin_info
4411 *
4412 * @throws Freemius_Exception
4413 */
4414 function dynamic_init( array $plugin_info ) {
4415 $this->_logger->entrance();
4416
4417 $this->parse_settings( $plugin_info );
4418
4419 $this->register_after_settings_parse_hooks();
4420
4421 /**
4422 * 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.
4423 *
4424 * @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.
4425 */
4426 if ( $this->is_anonymous() ) {
4427 $is_network_level = ( $this->_is_network_active && fs_is_network_admin() );
4428
4429 if (
4430 ! $is_network_level ||
4431 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
4432 ) {
4433 if ( $this->is_paying_or_trial() ) {
4434 $this->reset_anonymous_mode( $is_network_level );
4435 }
4436 } else {
4437 $network = get_network();
4438
4439 if ( is_object( $network ) ) {
4440 $main_blog_id = $network->site_id;
4441 $first_install = $this->get_install_by_blog_id( $main_blog_id );
4442
4443 if ( is_object( $first_install ) ) {
4444 $this->_storage->network_install_blog_id = $main_blog_id;
4445 $this->_storage->network_user_id = $first_install->user_id;
4446 }
4447 }
4448 }
4449 }
4450
4451 if ( $this->should_stop_execution() ) {
4452 return;
4453 }
4454
4455 if ( ! $this->is_registered() ) {
4456 if ( $this->is_anonymous() ) {
4457 // If user skipped, no need to test connectivity.
4458 $this->_has_api_connection = true;
4459 $this->_is_on = true;
4460 } else {
4461 if ( false === $this->has_api_connectivity() ) {
4462 return;
4463 } else {
4464 if ( $this->_anonymous_mode ) {
4465 // Simulate anonymous mode.
4466 $this->_is_anonymous = true;
4467 }
4468 }
4469 }
4470 }
4471
4472 /**
4473 * This should be executed even if Freemius is off for the core module,
4474 * otherwise, the add-ons dialog box won't work properly. This is especially
4475 * relevant when the developer decided to turn FS off for existing users.
4476 *
4477 * @author Vova Feldman (@svovaf)
4478 */
4479 if ( $this->is_user_in_admin() &&
4480 'plugin-information' === fs_request_get( 'tab', false ) &&
4481 $this->should_use_freemius_updater_and_dialog() &&
4482 (
4483 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4484 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4485 )
4486 ) {
4487 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4488
4489 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4490 }
4491
4492 // Check if Freemius is on for the current plugin.
4493 // This MUST be executed after all the plugin variables has been loaded.
4494 if ( ! $this->is_registered() && ! $this->is_on() ) {
4495 return;
4496 }
4497
4498 $this->hook_callback_to_sync_cron();
4499
4500 if ( $this->has_api_connectivity() && ! self::is_cron() && $this->is_user_in_admin() ) {
4501 /**
4502 * Schedule daily data sync cron if:
4503 *
4504 * 1. User opted-in (for tracking).
4505 * 2. If skipped, but later upgraded (opted-in via upgrade).
4506 *
4507 * @author Vova Feldman (@svovaf)
4508 * @since 1.1.7.3
4509 *
4510 */
4511 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
4512 $this->maybe_schedule_sync_cron();
4513 }
4514
4515 /**
4516 * Check if requested for manual blocking background sync.
4517 */
4518 if ( fs_request_has( 'background_sync' ) ) {
4519 self::require_pluggable_essentials();
4520 self::wp_cookie_constants();
4521
4522 $this->run_manual_sync();
4523 }
4524 }
4525
4526 if ( $this->is_registered() ) {
4527 FS_Clone_Manager::instance()->maybe_resolve_new_subsite_install_automatically( $this );
4528
4529 $this->hook_callback_to_install_sync();
4530 }
4531
4532 if ( $this->is_addon() ) {
4533 if ( $this->is_parent_plugin_installed() ) {
4534 // Link to parent FS.
4535 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4536
4537 // Get parent plugin reference.
4538 $this->_parent_plugin = $this->_parent->get_plugin();
4539 }
4540 }
4541
4542 if ( $this->is_user_in_admin() ) {
4543 if ( $this->is_addon() ) {
4544 if ( ! $this->is_parent_plugin_installed() ) {
4545 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4546
4547 if ( isset( $plugin_info['parent'] ) ) {
4548 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4549 }
4550
4551 $this->_admin_notices->add(
4552 ( ! empty( $parent_name ) ?
4553 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 ) :
4554 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() )
4555 ),
4556 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4557 'error'
4558 );
4559
4560 return;
4561 } else {
4562 $is_network_admin = fs_is_network_admin();
4563
4564 if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4565 // If add-on activated and parent not, automatically install parent for the user.
4566 $this->activate_parent_account( $this->_parent );
4567 } else if (
4568 $this->_parent->is_registered() &&
4569 ! $this->is_registered() &&
4570 /**
4571 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
4572 * * Network active and in network admin - network activate add-on account.
4573 * * Network active and not in network admin - activate add-on account for the current blog.
4574 * * Not network active and not in network admin - activate add-on account for the current blog.
4575 *
4576 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
4577 *
4578 * @author Leo Fajardo (@leorw)
4579 * @since 2.3.0
4580 */
4581 ( $this->is_network_active() || ! $is_network_admin )
4582 ) {
4583 $premium_license = null;
4584
4585 if (
4586 ! $this->has_free_plan() &&
4587 $this->is_bundle_license_auto_activation_enabled() &&
4588 $this->_parent->is_activated_with_bundle_license()
4589 ) {
4590 /**
4591 * If the add-on has no free plan, try to activate the account only when there's a bundle license.
4592 *
4593 * @author Leo Fajardo (@leorw)
4594 * @since 2.4.0
4595 */
4596 $bundle_license = $this->get_active_parent_license( $this->_parent->_get_license()->secret_key, false );
4597
4598 if (
4599 is_object( $bundle_license ) &&
4600 ! empty( $bundle_license->products ) &&
4601 in_array( $this->get_id(), $bundle_license->products )
4602 ) {
4603 $premium_license = $bundle_license;
4604 }
4605 }
4606
4607 if ( $this->has_free_plan() || is_object( $premium_license) ) {
4608 // If parent plugin activated, automatically install add-on for the user.
4609 $this->_activate_addon_account(
4610 $this->_parent,
4611 ( $this->is_network_active() && $is_network_admin ) ?
4612 true :
4613 get_current_blog_id(),
4614 $premium_license
4615 );
4616 }
4617 }
4618
4619 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4620 if ( $this->is_premium() ) {
4621 // Remove add-on download admin-notice.
4622 $this->_parent->_admin_notices->remove_sticky( array(
4623 'addon_plan_upgraded_' . $this->_slug,
4624 'no_addon_license_' . $this->_slug,
4625 ) );
4626 }
4627
4628 // $this->deactivate_premium_only_addon_without_license();
4629 }
4630 }
4631
4632 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
4633
4634 // if ( $this->is_registered() ||
4635 // $this->is_anonymous() ||
4636 // $this->is_pending_activation()
4637 // ) {
4638 // $this->_init_admin();
4639 // }
4640 }
4641
4642 /**
4643 * Should be called outside `$this->is_user_in_admin()` scope
4644 * because the updater has some logic that needs to be executed
4645 * during AJAX calls.
4646 *
4647 * Currently, we need to hook to the `http_request_host_is_external` filter.
4648 * In the future, there might be additional logic added.
4649 *
4650 * @author Vova Feldman
4651 * @since 1.2.1.6
4652 */
4653 if (
4654 $this->should_use_freemius_updater_and_dialog() &&
4655 (
4656 $this->is_premium() ||
4657 /**
4658 * If not premium but the premium version is installed, also instantiate the updater so that the
4659 * plugin information dialog of the premium version will have the information from the server.
4660 *
4661 * @author Leo Fajardo (@leorw)
4662 * @since 2.2.3
4663 */
4664 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
4665 ) &&
4666 $this->has_release_on_freemius() &&
4667 ( ! $this->is_unresolved_clone( true ) )
4668 ) {
4669 FS_Plugin_Updater::instance( $this );
4670 }
4671
4672 $this->do_action( 'initiated' );
4673
4674 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
4675 if ( isset( $this->_storage->prev_is_premium ) ) {
4676 $this->apply_filters(
4677 'after_code_type_change',
4678 // New code type.
4679 $this->_plugin->is_premium
4680 );
4681 } else {
4682 // Set for code type for the first time.
4683 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
4684 }
4685 }
4686
4687 if ( ! $this->is_addon() ) {
4688 if ( $this->is_registered() ) {
4689 // Fix for upgrade from versions < 1.0.9.
4690 if ( ! isset( $this->_storage->activation_timestamp ) ) {
4691 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
4692 }
4693
4694 $this->do_action( 'after_init_plugin_registered' );
4695 } else if ( $this->is_anonymous() ) {
4696 $this->do_action( 'after_init_plugin_anonymous' );
4697 } else if ( $this->is_pending_activation() ) {
4698 $this->do_action( 'after_init_plugin_pending_activations' );
4699 }
4700 } else {
4701 if ( $this->is_registered() ) {
4702 $this->do_action( 'after_init_addon_registered' );
4703 } else if ( $this->is_anonymous() ) {
4704 $this->do_action( 'after_init_addon_anonymous' );
4705 } else if ( $this->is_pending_activation() ) {
4706 $this->do_action( 'after_init_addon_pending_activations' );
4707 }
4708 }
4709 }
4710
4711 /**
4712 * @author Leo Fajardo (@leorw)
4713 * @since 2.2.3
4714 *
4715 * @return bool
4716 */
4717 private function should_use_freemius_updater_and_dialog() {
4718 return (
4719 /**
4720 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
4721 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
4722 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
4723 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
4724 * plugin details from .org).
4725 */
4726 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
4727 (
4728 ! self::is_plugin_install_page() &&
4729 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
4730 ( 'install-plugin' !== fs_request_get( 'action' ) )
4731 )
4732 );
4733 }
4734
4735 /**
4736 * @param string[] $permissions
4737 * @param bool $is_enabled
4738 * @param int|null $blog_id
4739 *
4740 * @return true|object `true` on success, API error object on failure.
4741 */
4742 private function update_site_permissions( array $permissions, $is_enabled, $blog_id = null ) {
4743 $this->_logger->entrance();
4744
4745 $params = array(
4746 'permissions' => implode( ',', $permissions ),
4747 'is_enabled' => $is_enabled,
4748 );
4749
4750 $current_blog_id = get_current_blog_id();
4751 $is_blog_switched = false;
4752 if ( is_numeric( $blog_id ) && $current_blog_id != $blog_id ) {
4753 $is_blog_switched = $this->switch_to_blog( $blog_id );
4754 }
4755
4756 $result = $this->api_site_call( '/permissions.json', 'put', $params );
4757
4758 if ( $is_blog_switched ) {
4759 $this->switch_to_blog( $current_blog_id );
4760 }
4761
4762 if (
4763 ! $this->is_api_result_object( $result ) ||
4764 ! isset( $result->install_id )
4765 ) {
4766 $this->_logger->api_error( $result );
4767
4768 return $result;
4769 }
4770
4771 return true;
4772 }
4773
4774 /**
4775 * @param string[] $permissions
4776 * @param bool $is_enabled
4777 * @param bool $has_site_delegated_connection
4778 *
4779 * @return true|object `true` on success, API error object on failure.
4780 */
4781 private function update_network_permissions(
4782 array $permissions,
4783 $is_enabled,
4784 &$has_site_delegated_connection
4785 ) {
4786 $this->_logger->entrance();
4787
4788 $install_id_2_blog_id = array();
4789 $install_by_blog_id = $this->get_blog_install_map();
4790
4791 $has_site_delegated_connection = false;
4792
4793 foreach ( $install_by_blog_id as $blog_id => $install ) {
4794 if ( $this->is_site_delegated_connection( $blog_id ) ) {
4795 // Only update permissions of non-delegated installs.
4796 $has_site_delegated_connection = true;
4797 continue;
4798 }
4799
4800 $install_id_2_blog_id[ $install->id ] = $blog_id;
4801 }
4802
4803 if ( empty( $install_id_2_blog_id ) ) {
4804 return true;
4805 }
4806
4807 $params = array(
4808 'permissions' => implode( ',', $permissions ),
4809 'is_enabled' => $is_enabled,
4810 'install_ids' => implode( ',', array_keys( $install_id_2_blog_id ) ),
4811 );
4812
4813 // Send update to FS.
4814 $result = $this->get_current_or_network_user_api_scope()->call(
4815 "/plugins/{$this->_module_id}/installs/permissions.json",
4816 'put',
4817 $params
4818 );
4819
4820 if ( ! $this->is_api_result_object( $result, 'installs_metadata' ) ) {
4821 $this->_logger->api_error( $result );
4822
4823 return $result;
4824 }
4825
4826 return true;
4827 }
4828
4829 /**
4830 * @param mixed $result
4831 *
4832 * @return string
4833 */
4834 private function get_api_error_message( $result ) {
4835 $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:',
4836 'unexpected-api-error' ), $this->_module_type ) . ' ';
4837
4838 if (
4839 $this->is_api_error( $result ) &&
4840 isset( $result->error )
4841 ) {
4842 $code = empty( $result->error->code ) ? '' : " Code: {$result->error->code}";
4843
4844 // We do not send HTML from exceptions from Freemius servers, but we escape it nonetheless.
4845 $error_message .= "<b>" . esc_html( $result->error->message . $code ) . "</b>";
4846 } else {
4847 $error_message .= esc_html( var_export( $result, true ) );
4848 }
4849
4850 return $error_message;
4851 }
4852
4853 /**
4854 * @author Vova Feldman (@svovaf)
4855 * @since 2.5.1
4856 */
4857 function _toggle_permission_tracking_callback() {
4858 $this->_logger->entrance();
4859
4860 $this->check_ajax_referer( 'toggle_permission_tracking' );
4861
4862 if ( ! $this->is_registered( true ) ) {
4863 self::shoot_ajax_failure( 'User never opted-in.' );
4864 }
4865
4866 $is_enabled = fs_request_get_bool( 'is_enabled' );
4867 $permissions = fs_request_get( 'permissions' );
4868
4869 if ( ! is_string( $permissions ) ) {
4870 self::shoot_ajax_failure( 'The permissions param must be a string.' );
4871 }
4872
4873 $permissions = explode( ',', $permissions );
4874
4875 $result = $this->toggle_permission_tracking( $permissions, $is_enabled );
4876
4877 if ( true !== $result ) {
4878 self::shoot_ajax_failure( $this->get_api_error_message( $result ) );
4879 }
4880
4881 self::shoot_ajax_success();
4882 }
4883
4884 /**
4885 * @param string[] $permissions
4886 * @param bool $is_enabled
4887 * @param int|null $blog_id
4888 *
4889 * @return bool|mixed `true` if updated successfully or no update is needed.
4890 */
4891 private function toggle_permission_tracking( $permissions, $is_enabled, $blog_id = null ) {
4892 if ( ! $this->is_registered( true ) ) {
4893 // User never opted-in.
4894 return false;
4895 }
4896
4897 // Check if permissions are already set as needed.
4898 if ( FS_Permission_Manager::instance( $this )->are_permissions( $permissions, $is_enabled, $blog_id ) ) {
4899 /**
4900 * Note:
4901 * 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.
4902 */
4903 return true;
4904 }
4905
4906 $api_managed_permissions = array_intersect(
4907 $permissions,
4908 FS_Permission_Manager::get_api_managed_permission_ids()
4909 );
4910
4911 if (
4912 in_array( FS_Permission_Manager::PERMISSION_ESSENTIALS, $permissions ) &&
4913 ! in_array( FS_Permission_Manager::PERMISSION_SITE, $permissions )
4914 ) {
4915 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_SITE;
4916 }
4917
4918 if ( ! empty( $api_managed_permissions ) ) {
4919 $has_site_delegated_connection = false;
4920
4921 if (
4922 ! $is_enabled &&
4923 ! in_array( FS_Permission_Manager::PERMISSION_EXTENSIONS, $api_managed_permissions ) &&
4924 false === FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed( $blog_id )
4925 ) {
4926 /**
4927 * 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.
4928 *
4929 * @todo Remove this entire `if` after implementing granular opt-in that also sends the permissions to the API when opting in.
4930 */
4931 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_EXTENSIONS;
4932 }
4933
4934 if ( is_null( $blog_id ) && fs_is_network_admin() ) {
4935 $result = $this->update_network_permissions(
4936 $api_managed_permissions,
4937 $is_enabled,
4938 $has_site_delegated_connection
4939 );
4940 } else {
4941 $result = $this->update_site_permissions(
4942 $api_managed_permissions,
4943 $is_enabled,
4944 $blog_id
4945 );
4946 }
4947
4948 if ( true !== $result ) {
4949 return $result;
4950 }
4951
4952 if ( in_array( FS_Permission_Manager::PERMISSION_SITE, $api_managed_permissions ) ) {
4953 if ( $is_enabled ) {
4954 $this->schedule_sync_cron();
4955 } else {
4956 $this->clear_sync_cron( ! $has_site_delegated_connection );
4957 }
4958 }
4959
4960 if ( in_array( FS_Permission_Manager::PERMISSION_USER, $api_managed_permissions ) ) {
4961 $this->toggle_user_permission( $is_enabled, $blog_id );
4962 }
4963 }
4964
4965 $this->update_tracking_permissions(
4966 $permissions,
4967 $is_enabled,
4968 $blog_id
4969 );
4970
4971 return true;
4972 }
4973
4974 /**
4975 * @param bool $is_enabled
4976 * @param int|null $blog_id
4977 */
4978 private function toggle_user_permission( $is_enabled, $blog_id = null ) {
4979 $network_or_blog_ids = is_numeric( $blog_id ) ?
4980 $blog_id :
4981 fs_is_network_admin();
4982
4983 if ( $is_enabled ) {
4984 $this->reset_anonymous_mode( $network_or_blog_ids );
4985 } else {
4986 $this->skip_connection( $network_or_blog_ids );
4987 }
4988 }
4989
4990 /**
4991 * Opt-in back into usage tracking.
4992 *
4993 * Note: This will only work if the user opted-in previously.
4994 *
4995 * Returns:
4996 * 1. FALSE - If the user never opted-in.
4997 * 2. TRUE - If successfully opted-in back to usage tracking.
4998 * 3. object - API result on failure.
4999 *
5000 * @author Leo Fajardo (@leorw)
5001 * @since 1.2.1.5
5002 *
5003 * @bool $is_enabled
5004 *
5005 * @return bool|object
5006 */
5007 private function toggle_site_tracking( $is_enabled, $blog_id = null ) {
5008 $this->_logger->entrance();
5009
5010 return $this->toggle_permission_tracking(
5011 FS_Permission_Manager::instance( $this )->get_site_tracking_permission_names(),
5012 $is_enabled,
5013 $blog_id
5014 );
5015 }
5016
5017 /**
5018 * If user opted-in and later disabled usage-tracking,
5019 * re-allow tracking for licensing and updates.
5020 *
5021 * @author Leo Fajardo (@leorw)
5022 * @since 1.2.1.5
5023 *
5024 * @param bool $is_context_single_site
5025 */
5026 private function reconnect_locally( $is_context_single_site = false ) {
5027 $this->_logger->entrance();
5028
5029 if ( ! $this->is_registered() ) {
5030 return;
5031 }
5032
5033 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5034 if ( $this->is_tracking_prohibited() ) {
5035 FS_Permission_Manager::instance( $this )->update_site_tracking( true );
5036 }
5037 } else {
5038 $installs_map = $this->get_blog_install_map();
5039 foreach ( $installs_map as $blog_id => $install ) {
5040 /**
5041 * @var FS_Site $install
5042 */
5043 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
5044 FS_Permission_Manager::instance( $this )->update_site_tracking( true, $blog_id );
5045 }
5046 }
5047 }
5048 }
5049
5050 /**
5051 * 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.
5052 *
5053 * @param string[] $permissions
5054 * @param bool $is_enabled
5055 * @param int|null $blog_id
5056 *
5057 * @return array
5058 */
5059 private function update_tracking_permissions( $permissions, $is_enabled, $blog_id = null ) {
5060 // Alias.
5061 $permission_manager = FS_Permission_Manager::instance( $this );
5062
5063 $network_or_blog_ids = is_numeric( $blog_id ) ?
5064 $blog_id :
5065 fs_is_network_admin();
5066
5067 if ( true === $network_or_blog_ids ) {
5068 // Update the permission for all non-delegated sub-sites.
5069 $blog_ids = $this->get_non_delegated_blog_ids();
5070
5071 // Add the network-level to the array, to update the permission on the network-level storage.
5072 array_unshift( $blog_ids, null );
5073 }
5074 else
5075 {
5076 if ( false === $network_or_blog_ids ) {
5077 $network_or_blog_ids = null;
5078 }
5079
5080 $blog_ids = is_array( $network_or_blog_ids ) ?
5081 $network_or_blog_ids :
5082 array( $network_or_blog_ids );
5083 }
5084
5085 $result = array();
5086 foreach ( $permissions as $permission ) {
5087 $permission = trim( $permission );
5088 $is_permission_supported = true;
5089
5090 foreach ( $blog_ids as $id ) {
5091 $is_permission_supported = $permission_manager->update_permission_tracking_flag(
5092 $permission,
5093 $is_enabled,
5094 $id
5095 );
5096 }
5097
5098 if ( ! $is_permission_supported ) {
5099 $permission = 'no_match';
5100 }
5101
5102 $result[ $permission ] = $is_enabled;
5103 }
5104
5105 return $result;
5106 }
5107
5108 /**
5109 * Parse plugin's settings (as defined by the plugin dev).
5110 *
5111 * @author Vova Feldman (@svovaf)
5112 * @since 1.1.7.3
5113 *
5114 * @param array $plugin_info
5115 *
5116 * @throws \Freemius_Exception
5117 */
5118 private function parse_settings( &$plugin_info ) {
5119 $this->_logger->entrance();
5120
5121 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5122 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5123 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5124 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5125 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5126
5127 /**
5128 * @author Vova Feldman (@svovaf)
5129 * @since 1.1.9 Try to pull secret key from external config.
5130 */
5131 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5132 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5133 }
5134
5135 if ( isset( $plugin_info['parent'] ) ) {
5136 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5137 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5138 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5139 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5140 }
5141
5142 if ( false === $id ) {
5143 throw new Freemius_Exception( array(
5144 'error' => array(
5145 'type' => 'ParameterNotSet',
5146 'message' => 'Plugin id parameter is not set.',
5147 'code' => 'plugin_id_not_set',
5148 'http' => 500,
5149 )
5150 ) );
5151 }
5152 if ( false === $public_key ) {
5153 throw new Freemius_Exception( array(
5154 'error' => array(
5155 'type' => 'ParameterNotSet',
5156 'message' => 'Plugin public_key parameter is not set.',
5157 'code' => 'plugin_public_key_not_set',
5158 'http' => 500,
5159 )
5160 ) );
5161 }
5162
5163 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5164 $this->_plugin :
5165 new FS_Plugin();
5166
5167 $is_premium = $this->get_bool_option( $plugin_info, 'is_premium', true );
5168 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5169
5170 $module_type = $this->get_option( $plugin_info, 'type', $this->_module_type );
5171
5172 $parallel_activation = $this->get_option( $plugin_info, 'parallel_activation' );
5173
5174 if (
5175 ! $is_premium &&
5176 is_array( $parallel_activation ) &&
5177 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type ) &&
5178 $this->get_bool_option( $parallel_activation, 'enabled' )
5179 ) {
5180 $premium_basename = $this->get_option( $parallel_activation, 'premium_version_basename' );
5181
5182 if ( empty( $premium_basename ) ) {
5183 throw new Exception('You need to specify the premium version basename to enable parallel version activation.');
5184 }
5185
5186 $this->_premium_plugin_basename_from_parallel_activation = $premium_basename;
5187
5188 if ( is_plugin_active( $premium_basename ) ) {
5189 $is_premium = true;
5190 }
5191 }
5192
5193 $plugin->update( array(
5194 'id' => $id,
5195 'type' => $module_type,
5196 'public_key' => $public_key,
5197 'slug' => $this->_slug,
5198 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5199 'parent_plugin_id' => $parent_id,
5200 'version' => $this->get_plugin_version(),
5201 'title' => $this->get_plugin_name( $premium_suffix ),
5202 'file' => $this->_plugin_basename,
5203 'is_premium' => $is_premium,
5204 'premium_suffix' => $premium_suffix,
5205 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5206 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5207 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5208 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ),
5209 'opt_in_moderation' => $this->get_option(
5210 $plugin_info,
5211 'opt_in',
5212 // For backward compatibility, we support both parameter names: opt_in and opt_in_moderation.
5213 $this->get_option( $plugin_info, 'opt_in_moderation', null )
5214 ),
5215 ) );
5216
5217 if ( $plugin->is_updated() ) {
5218 // Update plugin details.
5219 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5220 }
5221 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5222 $this->_plugin->secret_key = $secret_key;
5223
5224 /**
5225 * 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).
5226 *
5227 * @author Vova Feldman
5228 * @since 2.4.5
5229 */
5230 if ( $this->is_network_active() && fs_is_network_admin() ) {
5231 if ( isset( $plugin_info['menu_network'] ) &&
5232 is_array( $plugin_info['menu_network'] ) &&
5233 ! empty( $plugin_info['menu_network'] )
5234 ) {
5235 $plugin_info['menu'] = $plugin_info['menu_network'];
5236 }
5237 }
5238
5239 if ( ! isset( $plugin_info['menu'] ) ) {
5240 $plugin_info['menu'] = array();
5241
5242 if ( ! empty( $this->_storage->sdk_last_version ) &&
5243 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5244 ) {
5245 // Backward compatibility to 1.1.2
5246 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5247 $plugin_info['menu_slug'] :
5248 $this->_slug;
5249 }
5250 }
5251
5252 $this->_menu = FS_Admin_Menu_Manager::instance(
5253 $this->_module_id,
5254 $this->_module_type,
5255 $this->get_unique_affix()
5256 );
5257
5258 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5259
5260 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5261 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5262 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5263 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5264 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5265 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5266 if ( $this->_is_premium_only ) {
5267 // If premium only plugin, disable anonymous mode.
5268 $this->_enable_anonymous = false;
5269 $this->_anonymous_mode = false;
5270 } else {
5271 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5272 $this->_anonymous_mode = (
5273 $this->get_bool_option( $plugin_info, 'anonymous_mode', false ) ||
5274 (
5275 $this->apply_filters( 'playground_anonymous_mode', true ) &&
5276 ! empty( $_SERVER['HTTP_HOST'] ) &&
5277 FS_Site::is_playground_wp_environment_by_host( $_SERVER['HTTP_HOST'] )
5278 )
5279 );
5280 }
5281 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5282 $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
5283
5284 if ( ! empty( $plugin_info['trial'] ) ) {
5285 $this->_trial_days = $this->get_numeric_option(
5286 $plugin_info['trial'],
5287 'days',
5288 // Default to 0 - trial without days specification.
5289 0
5290 );
5291
5292 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5293 }
5294
5295 $this->_navigation = $this->get_option(
5296 $plugin_info,
5297 'navigation',
5298 $this->is_free_wp_org_theme() ?
5299 self::NAVIGATION_TABS :
5300 self::NAVIGATION_MENU
5301 );
5302 }
5303
5304 /**
5305 * @param string[] $options
5306 * @param string $key
5307 * @param mixed $default
5308 *
5309 * @return bool
5310 */
5311 private function get_option( &$options, $key, $default = false ) {
5312 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5313 }
5314
5315 private function get_bool_option( &$options, $key, $default = false ) {
5316 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5317 }
5318
5319 private function get_numeric_option( &$options, $key, $default = false ) {
5320 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5321 }
5322
5323 /**
5324 * Gate keeper.
5325 *
5326 * @author Vova Feldman (@svovaf)
5327 * @since 1.1.7.3
5328 *
5329 * @return bool
5330 */
5331 private function should_stop_execution() {
5332 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5333 /**
5334 * Don't execute Freemius until plugin was fully loaded at least once,
5335 * to give the opportunity for the activation hook to run before pinging
5336 * the API for connectivity test. This logic is relevant for the
5337 * identification of new plugin install vs. plugin update.
5338 *
5339 * @author Vova Feldman (@svovaf)
5340 * @since 1.1.9
5341 */
5342 return true;
5343 }
5344
5345 if ( $this->is_activation_mode() ) {
5346 if ( ! is_admin() ) {
5347 /**
5348 * If in activation mode, don't execute Freemius outside the admin dashboard.
5349 *
5350 * @author Vova Feldman (@svovaf)
5351 * @since 1.1.7.3
5352 */
5353 return true;
5354 }
5355
5356 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5357 /**
5358 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5359 * then don't start Freemius.
5360 *
5361 * @author Vova Feldman (@svovaf)
5362 * @since 1.1.6.3
5363 *
5364 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5365 */
5366 return true;
5367 }
5368
5369 if ( self::is_cron() ) {
5370 /**
5371 * If in activation mode, don't execute Freemius during wp crons
5372 * (wp crons have HTTP context - called as HTTP request).
5373 *
5374 * @author Vova Feldman (@svovaf)
5375 * @since 1.1.7.3
5376 */
5377 return true;
5378 }
5379
5380 if ( self::is_ajax() ) {
5381 /**
5382 * During activation, if running in AJAX mode, unless there's a sticky
5383 * connectivity issue notice, don't run Freemius.
5384 *
5385 * @author Vova Feldman (@svovaf)
5386 * @since 1.1.7.3
5387 */
5388 return true;
5389 }
5390 }
5391
5392 return false;
5393 }
5394
5395 /**
5396 * Triggered after code type has changed.
5397 *
5398 * @author Vova Feldman (@svovaf)
5399 * @since 1.1.9.1
5400 */
5401 function _after_code_type_change() {
5402 $this->_logger->entrance();
5403
5404 if ( $this->is_theme() ) {
5405 // Expire the cache of the previous tabs since the theme may
5406 // have setting updates after code type has changed.
5407 $this->_cache->expire( 'tabs' );
5408 $this->_cache->expire( 'tabs_stylesheets' );
5409 }
5410
5411 if ( ! $this->is_addon() ) {
5412 add_action(
5413 is_admin() ? 'admin_init' : 'init',
5414 array( &$this, '_plugin_code_type_changed' )
5415 );
5416 }
5417
5418 if ( $this->is_registered() && $this->is_premium() ) {
5419 // Purge cached payments after switching to the premium version.
5420 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5421 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5422 }
5423 }
5424
5425 /**
5426 * Handles plugin's code type change (free <--> premium).
5427 *
5428 * @author Vova Feldman (@svovaf)
5429 * @since 1.0.9
5430 */
5431 function _plugin_code_type_changed() {
5432 $this->_logger->entrance();
5433
5434 if ( $this->is_premium() ) {
5435 $this->reconnect_locally();
5436
5437 // Activated premium code.
5438 $this->do_action( 'after_premium_version_activation' );
5439
5440 // Remove all sticky messages related to download of the premium version.
5441 $this->_admin_notices->remove_sticky( array(
5442 'trial_started',
5443 'plan_upgraded',
5444 'plan_changed',
5445 'license_activated',
5446 ) );
5447
5448 $notice = '';
5449 if ( ! $this->is_only_premium() ) {
5450 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5451 }
5452
5453 $license_notice = $this->get_license_network_activation_notice();
5454 if ( ! empty( $license_notice ) ) {
5455 $notice .= ' ' . $license_notice;
5456 }
5457
5458 if ( ! empty( $notice ) ) {
5459 $this->_admin_notices->add_sticky(
5460 trim( $notice ),
5461 'premium_activated',
5462 $this->get_text_x_inline( 'W00t',
5463 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5464 );
5465 }
5466 } else {
5467 // Remove sticky message related to premium code activation.
5468 $this->_admin_notices->remove_sticky( 'premium_activated' );
5469
5470 // Activated free code (after had the premium before).
5471 $this->do_action( 'after_free_version_reactivation' );
5472
5473 if ( $this->is_paying() && ! $this->is_premium() ) {
5474 $this->add_complete_upgrade_instructions_notice(
5475 sprintf(
5476 /* translators: %s: License type (e.g. you have a professional license) */
5477 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5478 $this->get_plan_title()
5479 ),
5480 'plan_upgraded'
5481 );
5482 }
5483 }
5484
5485 if ( $this->is_registered() ) {
5486 // Schedule code type changes event.
5487 $this->maybe_schedule_install_sync_cron();
5488 }
5489
5490 /**
5491 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5492 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5493 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5494 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5495 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5496 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5497 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5498 */
5499 $this->unregister_uninstall_hook();
5500
5501 $this->clear_module_main_file_cache();
5502
5503 // Update is_premium of latest version.
5504 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5505 }
5506
5507 #endregion
5508
5509 #----------------------------------------------------------------------------------
5510 #region Add-ons
5511 #----------------------------------------------------------------------------------
5512
5513 /**
5514 * Check if add-on installed and activated on site.
5515 *
5516 * @author Vova Feldman (@svovaf)
5517 * @since 1.0.6
5518 *
5519 * @param string|number $id_or_slug
5520 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5521 *
5522 * @return bool
5523 */
5524 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5525 $this->_logger->entrance();
5526
5527 $addon_id = self::get_module_id( $id_or_slug );
5528 $is_activated = self::has_instance( $addon_id );
5529
5530 if ( ! $is_activated ) {
5531 return false;
5532 }
5533
5534 if ( is_bool( $is_premium ) ) {
5535 // Check if the specified code version is activate.
5536 $addon = $this->get_addon_instance( $addon_id );
5537 $is_activated = ( $is_premium === $addon->is_premium() );
5538 }
5539
5540 return $is_activated;
5541 }
5542
5543 /**
5544 * Check if add-on was connected to install
5545 *
5546 * @author Vova Feldman (@svovaf)
5547 * @since 1.1.7
5548 *
5549 * @param string|number $id_or_slug
5550 *
5551 * @return bool
5552 */
5553 function is_addon_connected( $id_or_slug ) {
5554 $this->_logger->entrance();
5555
5556 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5557
5558 $addon_id = self::get_module_id( $id_or_slug );
5559 $addon = $this->get_addon( $addon_id );
5560 $slug = $addon->slug;
5561 if ( ! isset( $sites[ $slug ] ) ) {
5562 return false;
5563 }
5564
5565 $site = $sites[ $slug ];
5566
5567 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5568
5569 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5570 // The given slug do NOT belong to any of the plugin's add-ons.
5571 return false;
5572 }
5573
5574 return ( is_object( $site ) &&
5575 is_numeric( $site->id ) &&
5576 is_numeric( $site->user_id ) &&
5577 FS_Plugin_Plan::is_valid_id( $site->plan_id )
5578 );
5579 }
5580
5581 /**
5582 * Determines if add-on installed.
5583 *
5584 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
5585 *
5586 * @author Vova Feldman (@svovaf)
5587 * @since 1.0.6
5588 *
5589 * @param string|number $id_or_slug
5590 *
5591 * @return bool
5592 */
5593 function is_addon_installed( $id_or_slug ) {
5594 $this->_logger->entrance();
5595
5596 $addon_id = self::get_module_id( $id_or_slug );
5597
5598 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
5599 }
5600
5601 /**
5602 * Get add-on basename.
5603 *
5604 * @author Vova Feldman (@svovaf)
5605 * @since 1.0.6
5606 *
5607 * @param string|number $id_or_slug
5608 *
5609 * @return string
5610 */
5611 function get_addon_basename( $id_or_slug ) {
5612 $addon_id = self::get_module_id( $id_or_slug );
5613
5614 if ( $this->is_addon_activated( $addon_id ) ) {
5615 return self::instance( $addon_id )->get_plugin_basename();
5616 }
5617
5618 $addon = $this->get_addon( $addon_id );
5619 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
5620
5621 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
5622 return $premium_basename;
5623 }
5624
5625 $all_plugins = $this->get_all_plugins();
5626
5627 foreach ( $all_plugins as $basename => $data ) {
5628 if ( $addon->slug === $data['slug'] ||
5629 $addon->premium_slug === $data['slug']
5630 ) {
5631 return $basename;
5632 }
5633 }
5634
5635 $free_basename = "{$addon->slug}/{$addon->slug}.php";
5636
5637 return $free_basename;
5638 }
5639
5640 /**
5641 * Get installed add-ons instances.
5642 *
5643 * @author Vova Feldman (@svovaf)
5644 * @since 1.0.6
5645 *
5646 * @return Freemius[]
5647 */
5648 function get_installed_addons() {
5649 if ( $this->is_addon() ) {
5650 // Add-on cannot have add-ons.
5651 return array();
5652 }
5653
5654 $installed_addons = array();
5655
5656 foreach ( self::$_instances as $instance ) {
5657 if ( $instance->is_addon_of( $this->_plugin->id ) ) {
5658 $installed_addons[] = $instance;
5659 }
5660 }
5661
5662 return $installed_addons;
5663 }
5664
5665 /**
5666 * Check if any add-ons of the plugin are installed.
5667 *
5668 * @author Leo Fajardo (@leorw)
5669 * @since 1.1.1
5670 *
5671 * @return bool
5672 */
5673 function has_installed_addons() {
5674 if ( ! $this->has_addons() ) {
5675 return false;
5676 }
5677
5678 foreach ( self::$_instances as $instance ) {
5679 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
5680 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
5681 return true;
5682 }
5683 }
5684 }
5685
5686 return false;
5687 }
5688
5689 /**
5690 * Tell Freemius that the current plugin is an add-on.
5691 *
5692 * @author Vova Feldman (@svovaf)
5693 * @since 1.0.6
5694 *
5695 * @param number $parent_plugin_id The parent plugin ID
5696 */
5697 function init_addon( $parent_plugin_id ) {
5698 $this->_plugin->parent_plugin_id = $parent_plugin_id;
5699 }
5700
5701 /**
5702 * @author Vova Feldman (@svovaf)
5703 * @since 1.0.6
5704 *
5705 * @return bool
5706 */
5707 function is_addon() {
5708 return (
5709 isset( $this->_plugin->parent_plugin_id ) &&
5710 is_numeric( $this->_plugin->parent_plugin_id )
5711 );
5712 }
5713
5714 /**
5715 * @author Vova Feldman (@svovaf)
5716 * @since 2.3.2
5717 *
5718 * @param number $parent_product_id
5719 *
5720 * @return bool
5721 */
5722 function is_addon_of( $parent_product_id ) {
5723 return (
5724 $this->is_addon() &&
5725 $parent_product_id == $this->_plugin->parent_plugin_id
5726 );
5727 }
5728
5729 /**
5730 * Deactivate add-on if it's premium only and the user does't have a valid license.
5731 *
5732 * @param bool $is_after_trial_cancel
5733 *
5734 * @return bool If add-on was deactivated.
5735 */
5736 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
5737 if ( ! $this->has_free_plan() &&
5738 ! $this->has_features_enabled_license() &&
5739 ! $this->_has_premium_license()
5740 ) {
5741 if ( $this->is_registered() ) {
5742 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
5743 // if (empty($this->_storage->activation_timestamp) ||
5744 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
5745 // ) {
5746 /**
5747 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
5748 *
5749 * Retry syncing the user add-on licenses.
5750 */
5751 // Sync licenses.
5752 $this->_sync_licenses();
5753 // }
5754
5755 // Try to activate premium license.
5756 $this->_activate_license( true );
5757 }
5758
5759 if ( ! $this->has_free_plan() &&
5760 ! $this->has_features_enabled_license() &&
5761 ! $this->_has_premium_license()
5762 ) {
5763 // @todo Check if deactivate plugins also call the deactivation hook.
5764
5765 $this->_parent->_admin_notices->add_sticky(
5766 sprintf(
5767 ( $is_after_trial_cancel ?
5768 $this->_parent->get_text_inline(
5769 '%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.',
5770 'addon-trial-cancelled-message'
5771 ) :
5772 $this->_parent->get_text_inline(
5773 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
5774 'addon-no-license-message'
5775 )
5776 ),
5777 '<b>' . $this->_plugin->title . '</b>'
5778 ) . ' ' . sprintf(
5779 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
5780 $this->_parent->addon_url( $this->_slug ),
5781 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
5782 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
5783 ),
5784 'no_addon_license_' . $this->_slug,
5785 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
5786 ( $is_after_trial_cancel ? 'success' : 'error' )
5787 );
5788
5789 deactivate_plugins( array( $this->_plugin_basename ), true );
5790
5791 return true;
5792 }
5793 }
5794
5795 return false;
5796 }
5797
5798 #endregion
5799
5800 #----------------------------------------------------------------------------------
5801 #region Sandbox
5802 #----------------------------------------------------------------------------------
5803
5804 /**
5805 * Set Freemius into sandbox mode for debugging.
5806 *
5807 * @author Vova Feldman (@svovaf)
5808 * @since 1.0.4
5809 *
5810 * @param string $secret_key
5811 */
5812 function init_sandbox( $secret_key ) {
5813 $this->_plugin->secret_key = $secret_key;
5814
5815 // Update plugin details.
5816 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
5817 }
5818
5819 /**
5820 * Check if running payments in sandbox mode.
5821 *
5822 * @author Vova Feldman (@svovaf)
5823 * @since 1.0.4
5824 *
5825 * @return bool
5826 */
5827 function is_payments_sandbox() {
5828 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
5829 }
5830
5831 #endregion
5832
5833 /**
5834 * Check if running test vs. live plugin.
5835 *
5836 * @author Vova Feldman (@svovaf)
5837 * @since 1.0.5
5838 *
5839 * @return bool
5840 */
5841 function is_live() {
5842 return $this->_plugin->is_live;
5843 }
5844
5845 /**
5846 * Check if super-admin skipped connection for all sites in the network.
5847 *
5848 * @author Vova Feldman (@svovaf)
5849 * @since 2.0.0
5850 */
5851 function is_network_anonymous() {
5852 if ( ! $this->_is_network_active ) {
5853 return false;
5854 }
5855
5856 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
5857
5858 if ( empty( $is_anonymous_ms ) ) {
5859 return false;
5860 }
5861
5862 return $is_anonymous_ms['is'];
5863 }
5864
5865 /**
5866 * Check if super-admin opted-in for all sites in the network.
5867 *
5868 * @author Vova Feldman (@svovaf)
5869 * @since 2.0.0
5870 */
5871 function is_network_connected() {
5872 if ( ! $this->_is_network_active ) {
5873 return false;
5874 }
5875
5876 return $this->_storage->get( 'is_network_connected' );
5877 }
5878
5879 /**
5880 * Check if the user skipped connecting the account with Freemius.
5881 *
5882 * @author Vova Feldman (@svovaf)
5883 * @since 1.0.7
5884 *
5885 * @return bool
5886 */
5887 function is_anonymous() {
5888 if ( ! isset( $this->_is_anonymous ) ) {
5889 if ( $this->is_network_anonymous() ) {
5890 $this->_is_anonymous = true;
5891 } else if ( fs_is_network_admin() ) {
5892 /**
5893 * When not-network-anonymous, yet, running in the network admin, consider as anonymous only when ALL non-delegated sites are set to anonymous.
5894 */
5895 $non_delegated_sites = $this->get_non_delegated_blog_ids();
5896
5897 foreach ( $non_delegated_sites as $blog_id ) {
5898 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
5899
5900 if ( empty( $is_anonymous ) || false === $is_anonymous[ 'is' ] ) {
5901 $this->_is_anonymous = false;
5902 break;
5903 }
5904 }
5905
5906 if ( false !== $this->_is_anonymous ) {
5907 $this->_is_anonymous = true;
5908 }
5909 } else {
5910 if ( ! isset( $this->_storage->is_anonymous ) ) {
5911 // Not skipped.
5912 $this->_is_anonymous = false;
5913 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
5914 // For back compatibility, since the variable was boolean before.
5915 $this->_is_anonymous = $this->_storage->is_anonymous;
5916
5917 // Upgrade stored data format to 1.1.3 format.
5918 $this->set_anonymous_mode( $this->_storage->is_anonymous );
5919 } else {
5920 // Version 1.1.3 and later.
5921 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
5922 }
5923 }
5924 }
5925
5926 return $this->_is_anonymous;
5927 }
5928
5929 /**
5930 * Check if the user skipped the connection of a specified site.
5931 *
5932 * @author Vova Feldman (@svovaf)
5933 * @since 2.0.0
5934 *
5935 * @param int $blog_id
5936 *
5937 * @return bool
5938 */
5939 function is_anonymous_site( $blog_id = 0 ) {
5940 if ( $this->is_network_anonymous() ) {
5941 return true;
5942 }
5943
5944 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
5945
5946 if ( empty( $is_anonymous ) ) {
5947 return false;
5948 }
5949
5950 return $is_anonymous['is'];
5951 }
5952
5953 /**
5954 * Check if user connected his account and install pending email activation.
5955 *
5956 * @author Vova Feldman (@svovaf)
5957 * @since 1.0.7
5958 *
5959 * @return bool
5960 */
5961 function is_pending_activation() {
5962 return $this->_storage->get( 'is_pending_activation', false );
5963 }
5964
5965 /**
5966 * @author Leo Fajardo (@leorw)
5967 * @since 2.5.0
5968 */
5969 private function clear_pending_activation_mode() {
5970 // Remove the pending activation sticky notice (if it still exists).
5971 $this->_admin_notices->remove_sticky( 'activation_pending' );
5972
5973 // Clear the plugin's pending activation mode.
5974 unset( $this->_storage->is_pending_activation );
5975 }
5976
5977 /**
5978 * Check if plugin must be WordPress.org compliant.
5979 *
5980 * @since 1.0.7
5981 *
5982 * @return bool
5983 */
5984 function is_org_repo_compliant() {
5985 return $this->_is_org_compliant;
5986 }
5987
5988 #--------------------------------------------------------------------------------
5989 #region WP Cron Common
5990 #--------------------------------------------------------------------------------
5991
5992 /**
5993 * @author Vova Feldman (@svovaf)
5994 * @since 2.0.0
5995 *
5996 * @param string $name Cron name.
5997 *
5998 * @return object
5999 */
6000 private function get_cron_data( $name ) {
6001 $this->_logger->entrance( $name );
6002
6003 /**
6004 * @var object $cron_data
6005 */
6006 return $this->_storage->get( "{$name}_cron", null );
6007 }
6008
6009 /**
6010 * @author Vova Feldman (@svovaf)
6011 * @since 2.0.0
6012 *
6013 * @param string $name Cron name.
6014 */
6015 private function clear_cron_data( $name ) {
6016 $this->_logger->entrance( $name );
6017
6018 $this->_storage->remove( "{$name}_cron" );
6019 }
6020
6021 /**
6022 * @author Vova Feldman (@svovaf)
6023 * @since 2.0.0
6024 *
6025 * @param string $name Cron name.
6026 * @param int $cron_blog_id The cron executing blog ID.
6027 */
6028 private function set_cron_data( $name, $cron_blog_id = 0 ) {
6029 $this->_logger->entrance( $name );
6030
6031 $this->_storage->store( "{$name}_cron", (object) array(
6032 'version' => $this->get_plugin_version(),
6033 'blog_id' => $cron_blog_id,
6034 'sdk_version' => $this->version,
6035 'timestamp' => WP_FS__SCRIPT_START_TIME,
6036 'on' => true,
6037 ) );
6038 }
6039
6040 /**
6041 * Get the cron's executing blog ID.
6042 *
6043 * @author Vova Feldman (@svovaf)
6044 * @since 2.0.0
6045 *
6046 * @param string $name Cron name.
6047 *
6048 * @return int
6049 */
6050 private function get_cron_blog_id( $name ) {
6051 $this->_logger->entrance( $name );
6052
6053 if ( ! is_multisite() ) {
6054 // Not a multisite.
6055 return 0;
6056 }
6057
6058 $cron_data = $this->get_cron_data( $name );
6059
6060 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6061 $cron_data->blog_id :
6062 0;
6063 }
6064
6065 /**
6066 * @author Vova Feldman (@svovaf)
6067 * @since 2.0.0
6068 *
6069 * @param string $name Cron name.
6070 *
6071 * @return bool
6072 */
6073 private function is_cron_on( $name ) {
6074 $this->_logger->entrance( $name );
6075
6076 /**
6077 * @var object $cron_data
6078 */
6079 $cron_data = $this->get_cron_data( $name );
6080
6081 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6082 }
6083
6084 /**
6085 * Unix timestamp for previous cron execution or false if never executed.
6086 *
6087 * @author Vova Feldman (@svovaf)
6088 * @since 2.0.0
6089 *
6090 * @param string $name Cron name.
6091 *
6092 * @return int|false
6093 */
6094 private function cron_last_execution( $name ) {
6095 $this->_logger->entrance( $name );
6096
6097 return $this->_storage->get( "{$name}_timestamp" );
6098 }
6099
6100 /**
6101 * Set cron execution time to now.
6102 *
6103 * @author Vova Feldman (@svovaf)
6104 * @since 2.0.0
6105 *
6106 * @param string $name Cron name.
6107 */
6108 private function set_cron_execution_timestamp( $name ) {
6109 $this->_logger->entrance( $name );
6110
6111 $this->_storage->store( "{$name}_timestamp", time() );
6112 }
6113
6114 /**
6115 * Sets the keepalive time to now.
6116 *
6117 * @author Leo Fajardo (@leorw)
6118 * @since 2.2.3
6119 *
6120 * @param bool|null $use_network_level_storage
6121 */
6122 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6123 $this->_logger->entrance();
6124
6125 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6126 }
6127
6128 /**
6129 * Check if cron was executed in the last $period of seconds.
6130 *
6131 * @author Vova Feldman (@svovaf)
6132 * @since 2.0.0
6133 *
6134 * @param string $name Cron name.
6135 * @param int $period In seconds
6136 *
6137 * @return bool
6138 */
6139 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6140 $this->_logger->entrance( $name );
6141
6142 $last_execution = $this->cron_last_execution( $name );
6143
6144 if ( ! is_numeric( $last_execution ) ) {
6145 return false;
6146 }
6147
6148 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6149 }
6150
6151 /**
6152 * WP Cron is executed on a site level. When running in a multisite network environment
6153 * with the network integration activated, for optimization reasons, we are consolidating
6154 * the installs data sync cron to be executed only from a single site.
6155 *
6156 * @author Vova Feldman (@svovaf)
6157 * @since 2.0.0
6158 *
6159 * @param int $except_blog_id Target any except the excluded blog ID.
6160 *
6161 * @return int
6162 */
6163 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6164 if ( ! is_multisite() ) {
6165 return 0;
6166 }
6167
6168 if ( $this->_is_network_active ) {
6169 $network_install_blog_id = $this->_storage->network_install_blog_id;
6170
6171 if (
6172 is_numeric( $network_install_blog_id ) &&
6173 $except_blog_id != $network_install_blog_id &&
6174 self::is_site_active( $network_install_blog_id )
6175 ) {
6176 // Try to run cron from the main network blog.
6177 $install = $this->get_install_by_blog_id( $network_install_blog_id );
6178
6179 if (
6180 is_object( $install ) &&
6181 $this->is_tracking_allowed( $network_install_blog_id, $install )
6182 ) {
6183 return $network_install_blog_id;
6184 }
6185 }
6186 }
6187
6188 // Get first opted-in blog ID with active tracking.
6189 $installs = $this->get_blog_install_map();
6190 foreach ( $installs as $blog_id => $install ) {
6191 if ( $except_blog_id != $blog_id &&
6192 self::is_site_active( $blog_id ) &&
6193 $this->is_tracking_allowed( $blog_id, $install )
6194 ) {
6195 return $blog_id;
6196 }
6197 }
6198
6199 return 0;
6200 }
6201
6202 /**
6203 * @author Vova Feldman (@svovaf)
6204 * @since 2.0.0
6205 *
6206 * @param string $name Cron name.
6207 * @param string $action_tag Callback action tag.
6208 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6209 */
6210 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6211 $this->_logger->entrance( $name );
6212
6213 if ( ! $this->is_cron_on( $name ) ) {
6214 return;
6215 }
6216
6217 $clear_cron = true;
6218 if ( ! $is_network_clear && $this->_is_network_active ) {
6219 $installs = $this->get_blog_install_map();
6220
6221 foreach ( $installs as $blog_id => $install ) {
6222 /**
6223 * @var FS_Site $install
6224 */
6225 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6226 $clear_cron = false;
6227 break;
6228 }
6229 }
6230 }
6231
6232 if ( ! $clear_cron ) {
6233 return;
6234 }
6235
6236 $cron_blog_id = $this->get_cron_blog_id( $name );
6237
6238 $this->clear_cron_data( $name );
6239
6240 if ( 0 < $cron_blog_id ) {
6241 switch_to_blog( $cron_blog_id );
6242 }
6243
6244 if ( empty( $action_tag ) ) {
6245 $action_tag = $name;
6246 }
6247
6248 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6249
6250 if ( 0 < $cron_blog_id ) {
6251 restore_current_blog();
6252 }
6253 }
6254
6255 /**
6256 * Unix timestamp for next cron execution or false if not scheduled.
6257 *
6258 * @author Vova Feldman (@svovaf)
6259 * @since 2.0.0
6260 *
6261 * @param string $name Cron name.
6262 * @param string $action_tag Callback action tag.
6263 *
6264 * @return int|false
6265 */
6266 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6267 $this->_logger->entrance( $name );
6268
6269 if ( ! $this->is_cron_on( $name ) ) {
6270 return false;
6271 }
6272
6273 $cron_blog_id = $this->get_cron_blog_id( $name );
6274
6275 if ( 0 < $cron_blog_id ) {
6276 switch_to_blog( $cron_blog_id );
6277 }
6278
6279 if ( empty( $action_tag ) ) {
6280 $action_tag = $name;
6281 }
6282
6283 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6284
6285 if ( 0 < $cron_blog_id ) {
6286 restore_current_blog();
6287 }
6288
6289 return $next_scheduled;
6290 }
6291
6292 /**
6293 * @author Vova Feldman (@svovaf)
6294 * @since 2.0.0
6295 *
6296 * @param string $name Cron name.
6297 * @param string $action_tag Callback action tag.
6298 * @param string $recurrence 'single' or 'daily'.
6299 * @param int $start_at Defaults to now.
6300 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6301 * @param int $except_blog_id Target any except the excluded blog ID.
6302 */
6303 private function schedule_cron(
6304 $name,
6305 $action_tag = '',
6306 $recurrence = 'single',
6307 $start_at = WP_FS__SCRIPT_START_TIME,
6308 $randomize_start = true,
6309 $except_blog_id = 0
6310 ) {
6311 $this->_logger->entrance( $name );
6312
6313 $this->clear_cron( $name, $action_tag, true );
6314
6315 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6316
6317 if ( is_multisite() && 0 == $cron_blog_id ) {
6318 // Don't schedule cron since couldn't find a target blog.
6319 return;
6320 }
6321
6322 if ( 0 < $cron_blog_id ) {
6323 switch_to_blog( $cron_blog_id );
6324 }
6325
6326 if ( 'daily' === $recurrence ) {
6327 if ( $randomize_start ) {
6328 // Schedule first sync with a random 12 hour time range from now.
6329 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6330 }
6331
6332 // Schedule daily WP cron.
6333 wp_schedule_event(
6334 $start_at,
6335 'daily',
6336 $this->get_action_tag( $action_tag )
6337 );
6338 } else if ( 'single' === $recurrence ) {
6339 // Schedule single cron.
6340 wp_schedule_single_event(
6341 $start_at,
6342 $this->get_action_tag( $action_tag )
6343 );
6344 }
6345
6346 $this->set_cron_data( $name, $cron_blog_id );
6347
6348 if ( 0 < $cron_blog_id ) {
6349 restore_current_blog();
6350 }
6351 }
6352
6353 /**
6354 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6355 * that doesn't halt page loading.
6356 *
6357 * @author Vova Feldman (@svovaf)
6358 * @since 2.0.0
6359 *
6360 * @param string $name Cron name.
6361 * @param callable $callable The function that should be executed.
6362 */
6363 private function execute_cron( $name, $callable ) {
6364 $this->_logger->entrance( $name );
6365
6366 // Store the last time data sync was executed.
6367 $this->set_cron_execution_timestamp( $name );
6368
6369 // Check if API is temporary down.
6370 if ( FS_Api::is_temporary_down() ) {
6371 return;
6372 }
6373
6374 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6375
6376 $users_2_blog_ids = array();
6377
6378 if ( ! is_multisite() ) {
6379 // Add dummy blog.
6380 $users_2_blog_ids[0] = array( 0 );
6381 } else {
6382 $installs = $this->get_blog_install_map();
6383 foreach ( $installs as $blog_id => $install ) {
6384 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6385 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6386 $users_2_blog_ids[ $install->user_id ] = array();
6387 }
6388
6389 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6390 }
6391 }
6392 }
6393
6394 $current_blog_id = get_current_blog_id();
6395
6396 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6397 if ( 0 < $blog_ids[0] ) {
6398 $this->switch_to_blog( $blog_ids[0] );
6399 }
6400
6401 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6402
6403 foreach ( $blog_ids as $blog_id ) {
6404 $this->do_action( "after_{$name}_cron", $blog_id );
6405 }
6406 }
6407
6408 if ( is_multisite() ) {
6409 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6410
6411 $this->do_action( "after_{$name}_cron_multisite" );
6412 }
6413 }
6414
6415 #endregion
6416
6417 #----------------------------------------------------------------------------------
6418 #region Daily Sync Cron
6419 #----------------------------------------------------------------------------------
6420
6421
6422 /**
6423 * @author Vova Feldman (@svovaf)
6424 * @since 2.0.0
6425 *
6426 * @return bool
6427 */
6428 private function is_sync_cron_scheduled() {
6429 return $this->is_cron_on( 'sync' );
6430 }
6431
6432 /**
6433 * Get the sync cron's executing blog ID.
6434 *
6435 * @author Vova Feldman (@svovaf)
6436 * @since 2.0.0
6437 *
6438 * @return int
6439 */
6440 private function get_sync_cron_blog_id() {
6441 return $this->get_cron_blog_id( 'sync' );
6442 }
6443
6444 /**
6445 * @author Vova Feldman (@svovaf)
6446 * @since 1.1.7.3
6447 */
6448 private function run_manual_sync() {
6449 if ( ! $this->is_user_admin() ) {
6450 return;
6451 }
6452
6453 // Run manual sync.
6454 $this->_sync_cron();
6455
6456 // Reschedule next cron to run 24 hours from now (performance optimization).
6457 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6458 }
6459
6460 /**
6461 * Data sync cron job. Replaces the background sync non blocking HTTP request
6462 * that doesn't halt page loading.
6463 *
6464 * @author Vova Feldman (@svovaf)
6465 * @since 1.1.7.3
6466 * @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.
6467 */
6468 function _sync_cron() {
6469 $this->_logger->entrance();
6470
6471 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6472 }
6473
6474 /**
6475 * The actual data sync cron logic.
6476 *
6477 * @author Vova Feldman (@svovaf)
6478 * @since 2.0.0
6479 *
6480 * @param int[] $blog_ids
6481 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6482 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6483 * updates for a single site in case `execute_cron` has switched to a different blog.
6484 */
6485 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6486 if ( $this->is_registered() ) {
6487 if ( $this->has_paid_plan() ) {
6488 // Initiate background plan sync.
6489 $this->_sync_license( true, false, $current_blog_id );
6490
6491 if ( $this->is_paying() ) {
6492 // Check for premium plugin updates.
6493 $this->check_updates( true );
6494 }
6495 } else {
6496 // Sync install(s) (only if something changed locally).
6497 if ( 1 < count( $blog_ids ) ) {
6498 $this->sync_installs();
6499 } else {
6500 $this->sync_install();
6501 }
6502
6503 $this->maybe_sync_install_user();
6504 }
6505 }
6506 }
6507
6508 /**
6509 * Check if sync was executed in the last $period of seconds.
6510 *
6511 * @author Vova Feldman (@svovaf)
6512 * @since 1.1.7.3
6513 *
6514 * @param int $period In seconds
6515 *
6516 * @return bool
6517 */
6518 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6519 return $this->is_cron_executed( 'sync', $period );
6520 }
6521
6522 /**
6523 * @author Vova Feldman (@svovaf)
6524 * @since 1.1.7.3
6525 *
6526 * @return bool
6527 */
6528 private function is_sync_cron_on() {
6529 return $this->is_cron_on( 'sync' );
6530 }
6531
6532 /**
6533 * @author Leo Fajardo (@leorw)
6534 * @since 2.5.0
6535 */
6536 private function maybe_schedule_sync_cron() {
6537 $next_schedule = $this->next_sync_cron();
6538
6539 // The event is properly scheduled, so no need to reschedule it.
6540 if ( is_numeric( $next_schedule ) ) {
6541 return;
6542 }
6543
6544 $this->schedule_sync_cron();
6545 }
6546
6547 /**
6548 * Instead of running blocking install sync event, execute non blocking scheduled cron job.
6549 *
6550 * @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 specified blog ID from being the cron job executor.
6551 *
6552 * @author Leo Fajardo (@leorw)
6553 * @since 2.9.1
6554 */
6555 private function maybe_schedule_install_sync_cron( $except_blog_id = 0 ) {
6556 if ( ! $this->is_user_in_admin() ) {
6557 return;
6558 }
6559
6560 if ( $this->is_clone() ) {
6561 return;
6562 }
6563
6564 if (
6565 // The event has been properly scheduled, so no need to reschedule it.
6566 is_numeric( $this->next_install_sync() )
6567 ) {
6568 return;
6569 }
6570
6571 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
6572 }
6573
6574 /**
6575 * @author Vova Feldman (@svovaf)
6576 * @since 1.1.7.3
6577 *
6578 * @param int $start_at Defaults to now.
6579 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6580 * @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.
6581 */
6582 private function schedule_sync_cron(
6583 $start_at = WP_FS__SCRIPT_START_TIME,
6584 $randomize_start = true,
6585 $except_blog_id = 0
6586 ) {
6587 $this->schedule_cron(
6588 'sync',
6589 'data_sync',
6590 'daily',
6591 $start_at,
6592 $randomize_start,
6593 $except_blog_id
6594 );
6595 }
6596
6597 /**
6598 * Add the actual sync function to the cron job hook.
6599 *
6600 * @author Vova Feldman (@svovaf)
6601 * @since 1.1.7.3
6602 */
6603 private function hook_callback_to_sync_cron() {
6604 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6605 }
6606
6607 /**
6608 * @author Vova Feldman (@svovaf)
6609 * @since 1.1.7.3
6610 *
6611 * @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.
6612 */
6613 private function clear_sync_cron( $is_network_clear = false ) {
6614 $this->_logger->entrance();
6615
6616 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6617 }
6618
6619 /**
6620 * Unix timestamp for next sync cron execution or false if not scheduled.
6621 *
6622 * @author Vova Feldman (@svovaf)
6623 * @since 1.1.7.3
6624 *
6625 * @return int|false
6626 */
6627 function next_sync_cron() {
6628 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6629 }
6630
6631 /**
6632 * Unix timestamp for previous sync cron execution or false if never executed.
6633 *
6634 * @author Vova Feldman (@svovaf)
6635 * @since 1.1.7.3
6636 *
6637 * @return int|false
6638 */
6639 function last_sync_cron() {
6640 return $this->cron_last_execution( 'sync' );
6641 }
6642
6643 #endregion Daily Sync Cron ------------------------------------------------------------------
6644
6645 #----------------------------------------------------------------------------------
6646 #region Async Install Sync
6647 #----------------------------------------------------------------------------------
6648
6649 /**
6650 * @author Vova Feldman (@svovaf)
6651 * @since 1.1.7.3
6652 *
6653 * @return bool
6654 */
6655 private function is_install_sync_scheduled() {
6656 return $this->is_cron_on( 'install_sync' );
6657 }
6658
6659 /**
6660 * Get the sync cron's executing blog ID.
6661 *
6662 * @author Vova Feldman (@svovaf)
6663 * @since 2.0.0
6664 *
6665 * @return int
6666 */
6667 private function get_install_sync_cron_blog_id() {
6668 return $this->get_cron_blog_id( 'install_sync' );
6669 }
6670
6671 /**
6672 * Unix timestamp for previous install sync cron execution or false if never executed.
6673 *
6674 * @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.
6675 *
6676 * @author Vova Feldman (@svovaf)
6677 * @since 1.1.7.3
6678 *
6679 * @return int|false
6680 */
6681 function last_install_sync() {
6682 return $this->cron_last_execution( 'install_sync' );
6683 }
6684
6685 /**
6686 * Unix timestamp for next install sync cron execution or false if not scheduled.
6687 *
6688 * @author Vova Feldman (@svovaf)
6689 * @since 1.1.7.3
6690 *
6691 * @return int|false
6692 */
6693 function next_install_sync() {
6694 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
6695 }
6696
6697 /**
6698 * Add the actual install sync function to the cron job hook.
6699 *
6700 * @author Vova Feldman (@svovaf)
6701 * @since 1.1.7.3
6702 */
6703 private function hook_callback_to_install_sync() {
6704 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
6705 }
6706
6707 /**
6708 * @author Vova Feldman (@svovaf)
6709 * @since 1.1.7.3
6710 *
6711 * @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.
6712 */
6713 private function clear_install_sync_cron( $is_network_clear = false ) {
6714 $this->_logger->entrance();
6715
6716 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
6717 }
6718
6719 /**
6720 * @author Vova Feldman (@svovaf)
6721 * @since 1.1.7.3
6722 * @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.
6723 */
6724 public function _run_sync_install() {
6725 $this->_logger->entrance();
6726
6727 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
6728 }
6729
6730 /**
6731 * The actual install(s) sync cron logic.
6732 *
6733 * @author Vova Feldman (@svovaf)
6734 * @since 2.0.0
6735 *
6736 * @param int[] $blog_ids
6737 * @param int|null $current_blog_id
6738 */
6739 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
6740 if ( $this->is_registered() ) {
6741 if ( 1 < count( $blog_ids ) ) {
6742 $this->sync_installs( array(), true );
6743 } else {
6744 $this->sync_install( array(), true );
6745 }
6746
6747 $this->maybe_sync_install_user();
6748 }
6749 }
6750
6751 #endregion Async Install Sync ------------------------------------------------------------------
6752
6753 /**
6754 * Show a notice that activation is currently pending.
6755 *
6756 * @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.
6757 *
6758 * @author Vova Feldman (@svovaf)
6759 * @since 1.0.7
6760 *
6761 * @param bool|string $email_address
6762 * @param bool $is_pending_trial Since 1.2.1.5
6763 * @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.
6764 * @param bool $has_upgrade_context Since 2.5.3
6765 * @param bool $support_email_address Since 2.5.3
6766 */
6767 function _add_pending_activation_notice(
6768 $email_address = false,
6769 $is_pending_trial = false,
6770 $is_suspicious_email = false,
6771 $has_upgrade_context = false,
6772 $support_email_address = false
6773 ) {
6774 if ( ! is_string( $email_address ) ) {
6775 $current_user = self::_get_current_wp_user();
6776 $email_address = $current_user->user_email;
6777 }
6778
6779 $escaped_email_address = esc_html( $email_address );
6780
6781 $formatted_message_args = array(
6782 "<b>{$this->get_plugin_name()}</b>",
6783 "<b>{$escaped_email_address}</b>",
6784 );
6785
6786 if ( ! $has_upgrade_context || ! fs_is_network_admin() ) {
6787 /* translators: %3$s: action (e.g.: "start the trial" or "complete the opt-in") */
6788 $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' );
6789
6790 $formatted_message_args[] = $is_pending_trial ?
6791 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
6792 $this->get_text_inline( 'complete the opt-in', 'complete-the-opt-in' );
6793
6794 $notice_title = $this->get_text_inline( 'Thanks!', 'thanks' );
6795 } else {
6796 /* translators: %3$s: What the user is expected to receive via email (e.g.: "the installation instructions" or "a license key") */
6797 $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.' );
6798
6799 if ( $this->has_release_on_freemius() ) {
6800 $formatted_message_args[] = $this->get_text_x_inline(
6801 'the installation instructions',
6802 'Part of the message telling the user what they should receive via email.',
6803 'the-installation-instructions-phrase'
6804 );
6805 } else {
6806 $formatted_message_args[] = $this->get_text_x_inline(
6807 'a license key',
6808 'Part of the message telling the user what they should receive via email.',
6809 'a-license-key-phrase'
6810 );
6811
6812 $formatted_message .= ( ' ' . sprintf(
6813 /* translators: %s: activation link (e.g.: <a>Click here</a>) */
6814 $this->get_text_inline( '%s to activate the license once you get it.', 'license-activation-link-message' ),
6815 sprintf(
6816 '<b><a href="%s">%s</a></b>',
6817 $this->get_activation_url( array(
6818 'fs_action' => 'reset_pending_activation_mode',
6819 'require_license' => 'true',
6820 'fs_unique_affix' => $this->get_unique_affix(),
6821 ) ),
6822 $this->get_text_x_inline( 'Click here', 'Part of an activation link message.', 'click-here' )
6823 )
6824 ) );
6825 }
6826
6827 $formatted_message_args[] = ( ! empty( $support_email_address ) ) ?
6828 ( "<b>" . esc_html( $support_email_address ) . "</b>" ) :
6829 $this->get_text_x_inline(
6830 "the product's support email address",
6831 'Part of the message that tells the user to check their spam folder for a specific email.',
6832 'product-support-email-address-phrase'
6833 );
6834
6835 $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' ) );
6836
6837 $notice_title = $this->get_text_inline( 'Thanks for upgrading.', 'after-upgrade-thank-you-message' );
6838 }
6839
6840 $this->_admin_notices->add_sticky(
6841 vsprintf( $formatted_message, $formatted_message_args ),
6842 'activation_pending',
6843 $notice_title
6844 );
6845 }
6846
6847 /**
6848 * Check if currently in plugin activation.
6849 *
6850 * @author Vova Feldman (@svovaf)
6851 * @since 1.1.4
6852 *
6853 * @return bool
6854 */
6855 function is_plugin_activation() {
6856 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
6857
6858 return !empty($result);
6859 }
6860
6861 /**
6862 *
6863 * NOTE: admin_menu action executed before admin_init.
6864 *
6865 * @author Vova Feldman (@svovaf)
6866 * @since 1.0.7
6867 */
6868 function _admin_init_action() {
6869 $is_migration = $this->is_migration();
6870
6871 /**
6872 * Automatically redirect to connect/activation page after plugin activation.
6873 *
6874 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
6875 */
6876 if ( $this->is_plugin_activation() ) {
6877 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
6878
6879 if ( isset( $_GET['activate-multi'] ) ) {
6880 /**
6881 * Don't redirect if activating multiple plugins at once (bulk activation).
6882 */
6883 } else if (
6884 self::is_deactivation_snoozed() &&
6885 (
6886 // Either running the free code base.
6887 ! $this->is_premium() ||
6888 // Or if has a free version.
6889 ! $this->is_only_premium() ||
6890 // If premium only, don't redirect if license is activated.
6891 ( $this->is_registered() && ! $this->can_use_premium_code() )
6892 )
6893 ) {
6894 /**
6895 * 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.
6896 */
6897 } else if ( ! $is_migration ) {
6898 $this->_redirect_on_activation_hook();
6899 return;
6900 }
6901 }
6902
6903 if ( $is_migration ) {
6904 return;
6905 }
6906
6907 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
6908 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
6909
6910 $this->skip_connection( fs_is_network_admin() );
6911
6912 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
6913 }
6914
6915 if ( $this->is_network_activation_mode() &&
6916 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
6917 ) {
6918 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
6919
6920 $this->delegate_connection();
6921
6922 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
6923 }
6924
6925 $this->_add_upgrade_action_link();
6926
6927 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
6928 (
6929 ( true === $this->_storage->require_license_activation ) ||
6930 // Not registered nor anonymous.
6931 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
6932 // OR, network level and in network upgrade mode.
6933 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
6934 )
6935 ) {
6936 if ( ! $this->is_pending_activation() ) {
6937 if ( ! $this->is_activation_page() ) {
6938 /**
6939 * If a user visits any other admin page before activating the premium-only theme with a valid
6940 * license, reactivate the previous theme.
6941 *
6942 * @author Leo Fajardo (@leorw)
6943 * @since 1.2.2
6944 */
6945 if ( $this->is_theme() &&
6946 ! $this->has_settings_menu() &&
6947 ! isset( $_REQUEST['fs_action'] ) &&
6948 $this->can_activate_previous_theme()
6949 ) {
6950 if ( $this->is_only_premium() ) {
6951 $this->activate_previous_theme();
6952 return;
6953 }
6954
6955 if ( true === $this->_storage->require_license_activation ) {
6956 $this->_storage->require_license_activation = false;
6957 }
6958 }
6959
6960 if ( ! fs_is_network_admin() &&
6961 $this->is_network_activation_mode() &&
6962 ! $this->is_delegated_connection()
6963 ) {
6964 return;
6965 }
6966
6967 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
6968 if ( ! $this->_anonymous_mode &&
6969 ( ! $this->is_addon() || ! $this->_parent->is_anonymous() ) ) {
6970 // Show notice for new plugin installations.
6971 $this->_admin_notices->add(
6972 sprintf(
6973 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
6974 sprintf( '<b><a href="%s">%s</a></b>',
6975 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
6976 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
6977 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
6978 )
6979 ),
6980 '',
6981 'update-nag'
6982 );
6983 }
6984 } else {
6985 if ( $this->should_add_sticky_optin_notice() ) {
6986 $this->add_sticky_optin_admin_notice();
6987 }
6988
6989 if ( $this->has_filter( 'optin_pointer_element' ) ) {
6990 // Don't show admin nag if plugin update.
6991 wp_enqueue_script( 'wp-pointer' );
6992 wp_enqueue_style( 'wp-pointer' );
6993
6994 $this->_enqueue_connect_essentials();
6995
6996 add_action( 'admin_print_footer_scripts', array(
6997 $this,
6998 '_add_connect_pointer_script'
6999 ) );
7000 }
7001 }
7002 }
7003 }
7004
7005 if ( $this->show_opt_in_on_themes_page() &&
7006 $this->is_activation_page()
7007 ) {
7008 $this->_show_theme_activation_optin_dialog();
7009 }
7010 }
7011 }
7012
7013 /**
7014 * @author Vova Feldman (@svovaf)
7015 * @since 2.0.0
7016 *
7017 * @return bool
7018 */
7019 private function should_add_sticky_optin_notice() {
7020 if ( $this->is_addon() && $this->_parent->is_anonymous() ) {
7021 return false;
7022 }
7023
7024 if ( fs_is_network_admin() ) {
7025 if ( ! $this->_is_network_active ) {
7026 return false;
7027 }
7028
7029 if ( ! $this->is_network_activation_mode() ) {
7030 return false;
7031 }
7032
7033 return ! isset( $this->_storage->sticky_optin_added_ms );
7034 }
7035
7036 if ( ! $this->is_activation_mode() ) {
7037 return false;
7038 }
7039
7040 // If running from a blog admin and delegated the connection.
7041 return ! isset( $this->_storage->sticky_optin_added );
7042 }
7043
7044 /**
7045 * @author Leo Fajardo (@leorw)
7046 * @since 2.0.0
7047 */
7048 private function add_sticky_optin_admin_notice() {
7049 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7050 $this->_storage->sticky_optin_added = true;
7051 } else {
7052 $this->_storage->sticky_optin_added_ms = true;
7053 }
7054
7055 // Show notice for new plugin installations.
7056 $this->_admin_notices->add_sticky(
7057 sprintf(
7058 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7059 $this->_module_type,
7060 sprintf( '<b><a href="%s">%s</a></b>',
7061 $this->get_activation_url(),
7062 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7063 )
7064 ),
7065 'connect_account',
7066 '',
7067 'update-nag'
7068 );
7069 }
7070
7071 /**
7072 * Enqueue connect requires scripts and styles.
7073 *
7074 * @author Vova Feldman (@svovaf)
7075 * @since 1.1.4
7076 */
7077 function _enqueue_connect_essentials() {
7078 wp_enqueue_script( 'jquery' );
7079
7080 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.js' );
7081 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7082 }
7083
7084 /**
7085 * Add connect / opt-in pointer.
7086 *
7087 * @author Vova Feldman (@svovaf)
7088 * @since 1.1.4
7089 */
7090 function _add_connect_pointer_script() {
7091 $vars = array( 'id' => $this->_module_id );
7092 $pointer_content = fs_get_template( 'connect.php', $vars );
7093 ?>
7094 <script type="text/javascript">// <![CDATA[
7095 jQuery(document).ready(function ($) {
7096 if ('undefined' !== typeof(jQuery().pointer)) {
7097
7098 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7099
7100 if (element.length > 0) {
7101 var optin = $(element).pointer($.extend(true, {}, {
7102 content : <?php echo json_encode( $pointer_content ) ?>,
7103 position : {
7104 edge : 'left',
7105 align: 'center'
7106 },
7107 buttons : function () {
7108 // Don't show pointer buttons.
7109 return '';
7110 },
7111 pointerWidth: 482
7112 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7113
7114 <?php
7115 echo $this->apply_filters( 'optin_pointer_execute', "
7116
7117 optin.pointer('open');
7118
7119 // Tag the opt-in pointer with custom class.
7120 $('.wp-pointer #fs_connect')
7121 .parents('.wp-pointer.wp-pointer-top')
7122 .addClass('fs-opt-in-pointer');
7123
7124 ", 'element', 'optin' ) ?>
7125 }
7126 }
7127 });
7128 // ]]></script>
7129 <?php
7130 }
7131
7132 /**
7133 * Return current page's URL.
7134 *
7135 * @author Vova Feldman (@svovaf)
7136 * @since 1.0.7
7137 *
7138 * @return string
7139 */
7140 static function current_page_url() {
7141 $url = 'http';
7142
7143 if ( isset( $_SERVER["HTTPS"] ) ) {
7144 if ( $_SERVER["HTTPS"] == "on" ) {
7145 $url .= "s";
7146 }
7147 }
7148 $url .= "://";
7149 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7150 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7151 } else {
7152 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7153 }
7154
7155 return esc_url( $url );
7156 }
7157
7158 /**
7159 * Check if the current page is the plugin's main admin settings page.
7160 *
7161 * @author Vova Feldman (@svovaf)
7162 * @since 1.0.7
7163 *
7164 * @return bool
7165 */
7166 function _is_plugin_page() {
7167 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7168 fs_is_plugin_page( $this->_slug );
7169 }
7170
7171 /* Events
7172 ------------------------------------------------------------------------------------------------------------------*/
7173 /**
7174 * Delete site install from Database.
7175 *
7176 * @author Vova Feldman (@svovaf)
7177 * @since 1.0.1
7178 *
7179 * @param bool $store
7180 * @param int|null $blog_id Since 2.0.0
7181 *
7182 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7183 */
7184 function _delete_site( $store = true, $blog_id = null ) {
7185 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7186 }
7187
7188 /**
7189 * Delete site install from Database.
7190 *
7191 * @author Vova Feldman (@svovaf)
7192 * @since 1.2.2.7
7193 *
7194 * @param string $slug
7195 * @param string $module_type
7196 * @param bool $store
7197 * @param int|null $blog_id Since 2.0.0
7198 *
7199 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7200 */
7201 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7202 $sites = self::get_all_sites( $module_type, $blog_id );
7203
7204 $install_id = false;
7205
7206 if ( isset( $sites[ $slug ] ) ) {
7207 if ( is_object( $sites[ $slug ] ) ) {
7208 $install_id = $sites[ $slug ]->id;
7209 }
7210
7211 unset( $sites[ $slug ] );
7212
7213 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7214 }
7215
7216 return $install_id;
7217 }
7218
7219 /**
7220 * Delete plugin's plans information.
7221 *
7222 * @param bool $store Flush to Database if true.
7223 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7224 *
7225 * @author Vova Feldman (@svovaf)
7226 * @since 1.0.9
7227 */
7228 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7229 $this->_logger->entrance();
7230
7231 $plans = self::get_all_plans( $this->_module_type );
7232
7233 $plans_to_keep = array();
7234
7235 if ( $keep_associated_plans ) {
7236 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7237 foreach ( $plans_ids_to_keep as $plan_id ) {
7238 $plan = self::_get_plan_by_id( $plan_id );
7239 if ( is_object( $plan ) ) {
7240 $plans_to_keep[] = self::_encrypt_entity( $plan );
7241 }
7242 }
7243 }
7244
7245 if ( ! empty( $plans_to_keep ) ) {
7246 $plans[ $this->_slug ] = $plans_to_keep;
7247 } else {
7248 unset( $plans[ $this->_slug ] );
7249 }
7250
7251 $this->set_account_option( 'plans', $plans, $store );
7252 }
7253
7254 /**
7255 * Delete all plugin licenses.
7256 *
7257 * @author Vova Feldman (@svovaf)
7258 * @since 1.0.9
7259 *
7260 * @param bool $store
7261 */
7262 private function _delete_licenses( $store = true ) {
7263 $this->_logger->entrance();
7264
7265 $all_licenses = self::get_all_licenses();
7266
7267 unset( $all_licenses[ $this->_module_id ] );
7268
7269 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7270 }
7271
7272 /**
7273 * Check if Freemius was added on new plugin installation.
7274 *
7275 * @author Vova Feldman (@svovaf)
7276 * @since 1.1.5
7277 *
7278 * @return bool
7279 */
7280 function is_plugin_new_install() {
7281 return isset( $this->_storage->is_plugin_new_install ) &&
7282 $this->_storage->is_plugin_new_install;
7283 }
7284
7285 /**
7286 * Check if it's the first plugin release that is running Freemius.
7287 *
7288 * @author Vova Feldman (@svovaf)
7289 * @since 1.2.1.5
7290 *
7291 * @return bool
7292 */
7293 function is_first_freemius_powered_version() {
7294 return empty( $this->_storage->plugin_last_version );
7295 }
7296
7297 /**
7298 * @author Leo Fajardo (@leorw)
7299 * @since 1.2.2
7300 *
7301 * @return bool|string
7302 */
7303 private function get_previous_theme_slug() {
7304 return isset( $this->_storage->previous_theme ) ?
7305 $this->_storage->previous_theme :
7306 false;
7307 }
7308
7309 /**
7310 * @author Leo Fajardo (@leorw)
7311 * @since 1.2.2
7312 *
7313 * @return bool
7314 */
7315 private function can_activate_previous_theme() {
7316 return $this->can_activate_theme( $this->get_previous_theme_slug() );
7317 }
7318
7319 /**
7320 * @author Leo Fajardo (@leorw)
7321 * @since 2.5.0
7322 *
7323 * @return bool
7324 */
7325 private function can_activate_theme( $slug ) {
7326 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7327 $theme_instance = wp_get_theme( $slug );
7328
7329 return $theme_instance->exists();
7330 }
7331
7332 return false;
7333 }
7334
7335 /**
7336 * @author Leo Fajardo (@leorw)
7337 * @since 1.2.2
7338 */
7339 private function activate_previous_theme() {
7340 switch_theme( $this->get_previous_theme_slug() );
7341 unset( $this->_storage->previous_theme );
7342
7343 global $pagenow;
7344 if ( 'themes.php' === $pagenow ) {
7345 /**
7346 * Refresh the active theme information.
7347 *
7348 * @author Leo Fajardo (@leorw)
7349 * @since 1.2.2
7350 */
7351 fs_redirect( $this->admin_url( $pagenow ) );
7352 }
7353 }
7354
7355 /**
7356 * @author Leo Fajardo (@leorw)
7357 * @since 1.2.2
7358 *
7359 * @return string
7360 */
7361 function get_previous_theme_activation_url() {
7362 if ( ! $this->can_activate_previous_theme() ) {
7363 return '';
7364 }
7365
7366 /**
7367 * Activation URL
7368 *
7369 * @author Leo Fajardo (@leorw)
7370 * @since 1.2.2
7371 */
7372 return wp_nonce_url(
7373 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7374 'switch-theme_' . $this->get_previous_theme_slug()
7375 );
7376 }
7377
7378 /**
7379 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7380 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7381 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7382 * theme doesn't exist, then there will be no close button.
7383 *
7384 * @author Leo Fajardo (@leorw)
7385 * @since 1.2.2
7386 *
7387 * @param string $slug_or_name Old theme's slug or name.
7388 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7389 */
7390 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7391 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7392 $old_theme->get_stylesheet() :
7393 $slug_or_name;
7394
7395 $this->_activate_plugin_event_hook();
7396 }
7397
7398 /**
7399 * Plugin activated hook.
7400 *
7401 * @author Vova Feldman (@svovaf)
7402 * @since 1.0.1
7403 *
7404 * @uses FS_Api
7405 */
7406 function _activate_plugin_event_hook() {
7407 $this->_logger->entrance( 'slug = ' . $this->_slug );
7408
7409 if ( ! $this->is_user_admin() ) {
7410 return;
7411 }
7412
7413 $this->unregister_uninstall_hook();
7414
7415 // Clear API cache on activation.
7416 FS_Api::clear_cache();
7417
7418 $is_premium_version_activation = $this->is_plugin() ?
7419 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7420 $this->is_premium();
7421
7422 if ( $is_premium_version_activation && $this->is_pending_activation() ) {
7423 $this->clear_pending_activation_mode();
7424 }
7425
7426 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7427
7428 if ( $this->is_plugin() ) {
7429 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7430 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7431 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7432 $other_version_basename = $is_premium_version_activation ?
7433 $this->_free_plugin_basename :
7434 $this->premium_plugin_basename();
7435
7436 if ( ! $this->_is_network_active ) {
7437 /**
7438 * Themes are always network activated, but the ACTUAL activation is per site.
7439 *
7440 * During the activation, the plugin isn't yet active, therefore,
7441 * _is_network_active will be set to false even if it's a network level
7442 * activation. So we need to fix that by looking at the is_network_admin() value.
7443 *
7444 * @author Vova Feldman
7445 */
7446 $this->_is_network_active = (
7447 $this->_is_multisite_integrated &&
7448 fs_is_network_admin()
7449 );
7450 }
7451
7452 /**
7453 * If the other module version is active, deactivate it.
7454 *
7455 * is_plugin_active() checks if the plugin is active on the site or the network level and
7456 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7457 *
7458 * @author Leo Fajardo (@leorw)
7459 * @since 1.2.2
7460 */
7461 if (
7462 is_plugin_active( $other_version_basename ) &&
7463 $this->apply_filters( 'deactivate_on_activation', ! $this->is_parallel_activation() )
7464 ) {
7465 deactivate_plugins( $other_version_basename );
7466 }
7467 }
7468
7469 if ( $this->is_registered() ) {
7470 if ( $is_premium_version_activation ) {
7471 $this->reconnect_locally();
7472 }
7473
7474
7475 // Schedule re-activation event and sync.
7476 // $this->sync_install( array(), true );
7477 $this->maybe_schedule_install_sync_cron();
7478
7479 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7480 if ( $is_premium_version_activation ) {
7481 $this->_admin_notices->add(
7482 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7483 $this->get_text_x_inline( 'W00t',
7484 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7485 );
7486 }
7487 } else if ( $this->is_anonymous() ) {
7488 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7489 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7490 $network = true;
7491 } else {
7492 $plugin_version = isset( $this->_storage->is_anonymous ) ?
7493 $this->_storage->is_anonymous['version'] :
7494 null;
7495 $network = false;
7496 }
7497
7498 /**
7499 * Reset "skipped" click cache on the following:
7500 * 1. Freemius DEV mode.
7501 * 2. WordPress DEBUG mode.
7502 * 3. If a plugin and the user skipped the exact same version before.
7503 *
7504 * @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).
7505 *
7506 * @todo 4. If explicitly asked to retry after every activation.
7507 */
7508 if ( WP_FS__DEV_MODE ||
7509 (
7510 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7511 $this->get_plugin_version() == $plugin_version
7512 )
7513 ) {
7514 $this->reset_anonymous_mode( $network );
7515 }
7516 }
7517
7518 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7519
7520 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7521 /**
7522 * When activating an add-on, try to also activate a license.
7523 *
7524 * @author Leo Fajardo (@leorw)
7525 * @since 2.3.0
7526 */
7527 if ( ! $this->_is_network_active ) {
7528 $this->maybe_activate_addon_license();
7529 } else {
7530 $this->maybe_network_activate_addon_license();
7531 }
7532
7533 /**
7534 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7535 *
7536 * @author Leo Fajardo (@leorw)
7537 * @since 2.3.0
7538 */
7539 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7540
7541 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7542 $this->_storage->require_license_activation = false;
7543 }
7544 }
7545
7546 if (
7547 $is_premium_version_activation &&
7548 (
7549 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7550 (
7551 $this->is_registered() &&
7552 ! $is_trial_or_has_features_enabled_license
7553 )
7554 )
7555 ) {
7556 $this->_storage->require_license_activation = true;
7557 }
7558
7559 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7560 /**
7561 * If no previous version of plugin's version exist, it means that it's either
7562 * the first time that the plugin installed on the site, or the plugin was installed
7563 * before but didn't have Freemius integrated.
7564 *
7565 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7566 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7567 * only after immediate activation.
7568 *
7569 * @since 1.1.4
7570 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7571 */
7572 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7573 }
7574
7575 /**
7576 * Also flush when activating the premium version so that even if Freemius was off before, the API
7577 * connectivity test can be run again.
7578 *
7579 * @author Leo Fajardo (@leorw)
7580 * @since 2.2.3.1
7581 */
7582 $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation );
7583
7584 if ( ! $this->_anonymous_mode &&
7585 ( false !== $has_api_connectivity ) &&
7586 ! $this->_isAutoInstall
7587 ) {
7588 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7589 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7590 }
7591
7592 /**
7593 * Activation hook is executed after the plugin's main file is loaded, therefore,
7594 * after the plugin was loaded. The logic is located at activate_plugin()
7595 * ./wp-admin/includes/plugin.php.
7596 *
7597 * @author Vova Feldman (@svovaf)
7598 * @since 1.1.9
7599 */
7600 $this->_storage->was_plugin_loaded = true;
7601 }
7602
7603 /**
7604 * @author Leo Fajardo (@leorw)
7605 * @since 2.3.0
7606 */
7607 private function maybe_activate_addon_license() {
7608 $parent_fs = $this->get_parent_instance();
7609
7610 if (
7611 ! is_object( $parent_fs ) ||
7612 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7613 ) {
7614 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7615 return;
7616 }
7617
7618 $license = $this->get_active_parent_license();
7619 if ( ! is_object( $license ) ) {
7620 return;
7621 }
7622
7623 if (
7624 $this->is_bundle_license_auto_activation_enabled() &&
7625 ! empty( $license->products )
7626 ) {
7627 $this->activate_bundle_license( $license );
7628
7629 return;
7630 }
7631
7632 if ( ! $this->is_registered() ) {
7633 // Opt in with a license key.
7634 $this->opt_in(
7635 $parent_fs->get_current_or_network_user()->email,
7636 false,
7637 false,
7638 $license->secret_key,
7639 false,
7640 false,
7641 false,
7642 null,
7643 array(),
7644 true,
7645 $license->user_id
7646 );
7647 } else {
7648 // Activate the license.
7649 $install = $this->api_site_call(
7650 '/',
7651 'put',
7652 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7653 );
7654
7655 if ( ! FS_Api::is_api_error( $install ) ) {
7656 $this->_sync_addon_license( $this->get_id(), true );
7657 }
7658 }
7659 }
7660
7661 /**
7662 * @author Leo Fajardo (@leorw)
7663 * @since 2.3.0
7664 *
7665 * @param FS_Plugin_License $license
7666 */
7667 private function maybe_network_activate_addon_license( $license = null ) {
7668 $parent_fs = $this->get_parent_instance();
7669 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7670 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7671 return;
7672 }
7673
7674 $license = ( ! is_null( $license ) ) ?
7675 $license :
7676 $this->get_active_parent_license();
7677
7678 if ( ! is_object( $license ) ) {
7679 return;
7680 }
7681
7682 if (
7683 $this->is_bundle_license_auto_activation_enabled() &&
7684 ! empty( $license->products )
7685 ) {
7686 $this->activate_bundle_license( $license );
7687
7688 return;
7689 }
7690
7691 if ( ! $this->is_network_registered() ) {
7692 $sites = $this->get_sites_for_network_level_optin();
7693
7694 if ( count( $sites ) > $license->left() ) {
7695 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
7696 return;
7697 }
7698
7699 // Opt in with a license key.
7700 $this->opt_in(
7701 $parent_fs->get_user()->email,
7702 false,
7703 false,
7704 $license->secret_key,
7705 false,
7706 false,
7707 false,
7708 null,
7709 $sites,
7710 true,
7711 $license->user_id
7712 );
7713 } else {
7714 $blog_2_install_map = array();
7715 $site_ids = array();
7716
7717 $all_sites = Freemius::get_sites();
7718
7719 foreach ( $all_sites as $site ) {
7720 $blog_id = Freemius::get_site_blog_id( $site );
7721 $install = $this->get_install_by_blog_id( $blog_id );
7722
7723 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
7724 // Skip license activation for installs that are already associated with a license.
7725 continue;
7726 }
7727
7728 if ( is_object( $install ) ) {
7729 $blog_2_install_map[ $blog_id ] = $install;
7730 } else {
7731 $site_ids[] = $blog_id;
7732 }
7733 }
7734
7735 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
7736 return;
7737 }
7738
7739 $user = $this->get_current_or_network_user();
7740
7741 if ( ! empty( $blog_2_install_map ) ) {
7742 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
7743
7744 if ( true !== $result ) {
7745 return;
7746 }
7747 }
7748
7749 if ( ! empty( $site_ids ) ) {
7750 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
7751 }
7752 }
7753 }
7754
7755 /**
7756 * 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.
7757 *
7758 * @author Leo Fajardo (@leorw)
7759 * @since 2.4.0
7760 *
7761 * @param FS_Plugin_License $license
7762 * @param array $sites
7763 * @param int $blog_id
7764 */
7765 private function maybe_activate_bundle_license( $license = null, $sites = array(), $blog_id = 0 ) {
7766 if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
7767 $license = $this->_license;
7768 }
7769
7770 if ( ! is_object( $license ) ) {
7771 return;
7772 }
7773
7774 $parent_license = ( ! empty( $license->products ) ) ?
7775 $license :
7776 $this->get_active_parent_license( $license->secret_key );
7777
7778 if ( is_object( $parent_license ) ) {
7779 $this->activate_bundle_license( $parent_license, $sites, $blog_id );
7780 }
7781 }
7782
7783 /**
7784 * Try to activate a bundle license for all the bundle products installed on the site.
7785 * (1) If a child product install already has a license, the bundle license won't be activated.
7786 * (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.
7787 * (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.
7788 *
7789 * @author Leo Fajardo (@leorw)
7790 * @since 2.4.0
7791 *
7792 * @param FS_Plugin_License $license
7793 * @param array $sites
7794 * @param int $current_blog_id
7795 */
7796 private function activate_bundle_license( $license, $sites = array(), $current_blog_id = 0 ) {
7797 $is_network_admin = fs_is_network_admin();
7798
7799 $installs_by_blog_map = array();
7800 $site_info_by_blog_map = array();
7801
7802 /**
7803 * Try to activate the license for all supported products.
7804 *
7805 * @author Leo Fajardo
7806 */
7807 foreach ( $license->products as $product_id ) {
7808 $fs = self::get_instance_by_id( $product_id );
7809
7810 if ( ! is_object( $fs ) ) {
7811 continue;
7812 }
7813
7814 if ( ! $fs->has_paid_plan() ) {
7815 continue;
7816 }
7817
7818 if (
7819 ! $fs->is_addon() &&
7820 ! FS_Plan_Manager::instance()->has_paid_plan( $fs->_plans )
7821 ) {
7822 /**
7823 * The parent product can be free-only but can have its `has_paid_plan` flag set to `true` when
7824 * there is a context bundle.
7825 */
7826 continue;
7827 }
7828
7829 if ( $current_blog_id > 0 ) {
7830 $fs->switch_to_blog( $current_blog_id );
7831 }
7832
7833 if ( $fs->has_active_valid_license() ) {
7834 continue;
7835 }
7836
7837 if ( ! $is_network_admin || $current_blog_id > 0 ) {
7838 if ( $fs->is_network_active() && ! $fs->is_delegated_connection( $current_blog_id ) ) {
7839 // Do not try to activate the license in the site level if the product is network active and the connection was not delegated.
7840 continue;
7841 }
7842 } else {
7843 if ( ! $fs->is_network_active() ) {
7844 // Do not try to activate the license in the network level if the product is not network active.
7845 continue;
7846 }
7847
7848 if ( $fs->is_network_delegated_connection() ) {
7849 // Do not try to activate the license in the network level if the activation has been delegated to site admins.
7850 continue;
7851 }
7852
7853 $has_install_with_license = false;
7854
7855 // 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.
7856 $filtered_sites = array();
7857
7858 if ( empty( $sites ) ) {
7859 $all_sites = self::get_sites();
7860
7861 foreach ( $all_sites as $site ) {
7862 $sites[] = array( 'blog_id' => self::get_site_blog_id( $site ) );
7863 }
7864 } else {
7865 // Populate the map here to avoid calling `$fs->get_site_info( $site );` in the other `for` loop below.
7866 foreach ( $sites as $site ) {
7867 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
7868 continue;
7869 }
7870
7871 $site_info_by_blog_map[ $site['blog_id'] ] = $site;
7872 }
7873 }
7874
7875 foreach ( $sites as $site ) {
7876 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
7877 continue;
7878 }
7879
7880 $blog_id = $site['blog_id'];
7881
7882 if ( ! isset( $installs_by_blog_map[ $blog_id ] ) ) {
7883 $installs_by_blog_map[ $blog_id ] = self::get_all_sites( $fs->get_module_type(), $blog_id );
7884 }
7885
7886 $installs = $installs_by_blog_map[ $blog_id ];
7887 $install = null;
7888
7889 if ( isset( $installs[ $fs->get_slug() ] ) ) {
7890 $install = $installs[ $fs->get_slug() ];
7891
7892 if (
7893 is_object( $install ) &&
7894 (
7895 ! FS_Site::is_valid_id( $install->id ) ||
7896 ! FS_User::is_valid_id( $install->user_id ) ||
7897 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
7898 )
7899 ) {
7900 $install = null;
7901 }
7902 }
7903
7904 if (
7905 is_object( $install ) &&
7906 FS_Plugin_License::is_valid_id( $install->license_id )
7907 ) {
7908 $has_install_with_license = true;
7909 break;
7910 }
7911
7912 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
7913 // Site activation delegated, don't activate bundle license on the site in the network admin.
7914 continue;
7915 }
7916
7917 if ( ! isset( $site_info_by_blog_map[ $blog_id ] ) ) {
7918 $site_info_by_blog_map[ $blog_id ] = $fs->get_site_info( $site );
7919 }
7920
7921 $filtered_sites[] = $site_info_by_blog_map[ $blog_id ];
7922 }
7923
7924 if ( $has_install_with_license || empty( $filtered_sites ) ) {
7925 // 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.
7926 continue;
7927 }
7928
7929 $sites = $filtered_sites;
7930 }
7931
7932 $fs->activate_migrated_license(
7933 $license->secret_key,
7934 null,
7935 null,
7936 $sites,
7937 ( $current_blog_id > 0 ? $current_blog_id : null ),
7938 $license->user_id
7939 );
7940 }
7941 }
7942
7943 /**
7944 * Returns a parent license that can be activated for the context product.
7945 *
7946 * @author Leo Fajardo (@leorw)
7947 * @since 2.3.0
7948 *
7949 * @param string|null $license_key
7950 * @param bool $flush
7951 *
7952 * @return FS_Plugin_License
7953 */
7954 function get_active_parent_license( $license_key = null, $flush = true ) {
7955 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
7956
7957 $fs = $this;
7958
7959 if ( $this->is_addon() ) {
7960 $parent_instance = $this->get_parent_instance();
7961
7962 if ( is_object( $parent_instance ) && $parent_instance->is_registered() ) {
7963 $fs = $parent_instance;
7964 }
7965 }
7966
7967 $foreign_licenses = $fs->get_foreign_licenses_info(
7968 self::get_all_licenses( $this->get_parent_id() )
7969 );
7970
7971 if ( ! empty ( $foreign_licenses ) ) {
7972 $foreign_licenses = array(
7973 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
7974 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
7975 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
7976 );
7977
7978 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
7979 }
7980
7981 $result = $fs->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, $flush );
7982
7983 if (
7984 ! $this->is_api_result_object( $result, 'licenses' ) ||
7985 ! is_array( $result->licenses ) ||
7986 empty( $result->licenses )
7987 ) {
7988 return null;
7989 }
7990
7991 $parent_license = null;
7992
7993 if ( empty( $license_key ) ) {
7994 $parent_license = $result->licenses[0];
7995 } else {
7996 foreach ( $result->licenses as $license ) {
7997 if ( $license_key === $license->secret_key ) {
7998 $parent_license = $license;
7999 break;
8000 }
8001 }
8002 }
8003
8004 if ( ! is_null( $parent_license ) ) {
8005 $parent_license = new FS_Plugin_License( $parent_license );
8006 }
8007
8008 return $parent_license;
8009 }
8010
8011 /**
8012 * @author Leo Fajardo (@leorw)
8013 * @since 2.3.0
8014 *
8015 * @return array
8016 */
8017 function get_sites_for_network_level_optin() {
8018 $sites = array();
8019 $all_sites = self::get_sites();
8020
8021 foreach ( $all_sites as $site ) {
8022 $blog_id = self::get_site_blog_id( $site );
8023
8024 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
8025 ! $this->is_installed_on_site( $blog_id )
8026 ) {
8027 $sites[] = $this->get_site_info( $site );
8028 }
8029 }
8030
8031 return $sites;
8032 }
8033
8034 /**
8035 * Delete account.
8036 *
8037 * @author Vova Feldman (@svovaf)
8038 * @since 1.0.3
8039 *
8040 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8041 */
8042 function delete_account_event( $check_user = true ) {
8043 $this->_logger->entrance( 'slug = ' . $this->_slug );
8044
8045 if ( $check_user && ! $this->is_user_admin() ) {
8046 return;
8047 }
8048
8049 $this->do_action( 'before_account_delete' );
8050
8051 // Clear all admin notices.
8052 $this->_admin_notices->clear_all_sticky( false );
8053
8054 $this->_delete_site( false );
8055
8056 $delete_network_common_data = true;
8057
8058 if ( $this->_is_network_active ) {
8059 $installs = $this->get_blog_install_map();
8060
8061 // Don't delete common network data unless no other installs left.
8062 $delete_network_common_data = empty( $installs );
8063 }
8064
8065 if ( $delete_network_common_data ) {
8066 $this->_delete_plans( false );
8067
8068 $this->_delete_licenses( false );
8069
8070 // Delete add-ons related to plugin's account.
8071 $this->_delete_account_addons( false );
8072 }
8073
8074 // @todo Delete plans and licenses of add-ons.
8075
8076 self::$_accounts->store();
8077
8078 /**
8079 * IMPORTANT:
8080 * Clear crons must be executed before clearing all storage.
8081 * Otherwise, the cron will not be cleared.
8082 */
8083 if ( $delete_network_common_data ) {
8084 $this->clear_sync_cron();
8085 }
8086
8087 $this->clear_install_sync_cron();
8088
8089 // Clear all storage data.
8090 $this->_storage->clear_all( true, array(
8091 'is_delegated_connection',
8092 'connectivity_test',
8093 'is_on',
8094 ), false );
8095
8096 // Send delete event.
8097 $this->get_api_site_scope()->call( '/', 'delete' );
8098
8099 $this->do_action( 'after_account_delete' );
8100 }
8101
8102 /**
8103 * Delete network level account.
8104 *
8105 * @author Vova Feldman (@svovaf)
8106 * @since 2.0.0
8107 *
8108 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8109 */
8110 function delete_network_account_event( $check_user = true ) {
8111 $this->_logger->entrance( 'slug = ' . $this->_slug );
8112
8113 if ( $check_user && ! $this->is_user_admin() ) {
8114 return;
8115 }
8116
8117 $this->do_action( 'before_network_account_delete' );
8118
8119 // Clear all admin notices.
8120 $this->_admin_notices->clear_all_sticky();
8121
8122 $this->_delete_plans( false, false );
8123
8124 $this->_delete_licenses( false );
8125
8126 // Delete add-ons related to plugin's account.
8127 $this->_delete_account_addons( false );
8128
8129 // @todo Delete plans and licenses of add-ons.
8130
8131 self::$_accounts->store( true );
8132
8133 /**
8134 * IMPORTANT:
8135 * Clear crons must be executed before clearing all storage.
8136 * Otherwise, the cron will not be cleared.
8137 */
8138 $this->clear_sync_cron( true );
8139 $this->clear_install_sync_cron( true );
8140
8141 $sites = self::get_sites();
8142
8143 $install_ids = array();
8144 foreach ( $sites as $site ) {
8145 $blog_id = self::get_site_blog_id( $site );
8146
8147 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8148 continue;
8149 }
8150
8151 $install_id = $this->_delete_site( true, $blog_id );
8152
8153 // Clear all storage data.
8154 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8155
8156 if ( FS_Site::is_valid_id( $install_id ) ) {
8157 $install_ids[] = $install_id;
8158 }
8159
8160 switch_to_blog( $blog_id );
8161
8162 $this->do_action( 'after_account_delete' );
8163
8164 restore_current_blog();
8165 }
8166
8167 $this->_storage->clear_all( true, array(
8168 'connectivity_test',
8169 'is_on',
8170 ), true );
8171
8172 // Send delete event.
8173 if ( ! empty( $install_ids ) ) {
8174 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8175 }
8176
8177 $this->do_action( 'after_network_account_delete' );
8178 }
8179
8180 /**
8181 * Plugin deactivation hook.
8182 *
8183 * @author Vova Feldman (@svovaf)
8184 * @since 1.0.1
8185 */
8186 function _deactivate_plugin_hook() {
8187 $this->_logger->entrance( 'slug = ' . $this->_slug );
8188
8189 if ( ! $this->is_user_admin() ) {
8190 return;
8191 }
8192
8193 $is_network_deactivation = fs_is_network_admin();
8194 $storage_keys_for_removal = array();
8195
8196 $this->_admin_notices->clear_all_sticky();
8197
8198 $storage_keys_for_removal[] = 'sticky_optin_added';
8199 if ( isset( $this->_storage->sticky_optin_added ) ) {
8200 unset( $this->_storage->sticky_optin_added );
8201 }
8202
8203 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8204 // Remember that plugin was already installed.
8205 $this->_storage->is_plugin_new_install = false;
8206 }
8207
8208 // Hook to plugin uninstall.
8209 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8210
8211 $this->clear_module_main_file_cache();
8212 $this->clear_sync_cron( $this->_is_network_active );
8213 $this->clear_install_sync_cron();
8214
8215 if ( $this->is_registered() ) {
8216 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8217 FS_Plugin_Updater::instance( $this )->delete_update_data();
8218 }
8219
8220 if ( $is_network_deactivation ) {
8221 // Send deactivation event.
8222 $this->sync_installs( array(
8223 'is_active' => false,
8224 ) );
8225 } else {
8226 // Send deactivation event.
8227 $this->sync_install( array(
8228 'is_active' => false,
8229 ) );
8230 }
8231 } else {
8232 if ( false === $this->has_api_connectivity() && ! $this->is_premium() ) {
8233 // Reset connectivity test cache.
8234 $this->clear_connectivity_info();
8235
8236 $storage_keys_for_removal[] = 'connectivity_test';
8237 }
8238 }
8239
8240 if ( $is_network_deactivation ) {
8241 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8242 unset( $this->_storage->sticky_optin_added_ms );
8243 }
8244
8245 if ( ! empty( $storage_keys_for_removal ) ) {
8246 $sites = self::get_sites();
8247
8248 foreach ( $sites as $site ) {
8249 $blog_id = self::get_site_blog_id( $site );
8250
8251 foreach ( $storage_keys_for_removal as $key ) {
8252 $this->_storage->remove( $key, false, $blog_id );
8253 }
8254
8255 $this->_storage->save( $blog_id );
8256 }
8257 }
8258 }
8259
8260 // Clear API cache on deactivation.
8261 FS_Api::clear_cache();
8262
8263 $this->remove_sdk_reference();
8264 }
8265
8266 /**
8267 * @author Vova Feldman (@svovaf)
8268 * @since 1.1.6
8269 */
8270 private function remove_sdk_reference() {
8271 global $fs_active_plugins;
8272
8273 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8274 if ( $this->_plugin_basename == $data->plugin_path ) {
8275 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8276 break;
8277 }
8278 }
8279
8280 fs_fallback_to_newest_active_sdk();
8281 }
8282
8283 /**
8284 * @author Vova Feldman (@svovaf)
8285 * @since 1.1.3
8286 *
8287 * @param bool $is_anonymous
8288 * @param bool|int $network_or_blog_id Since 2.0.0
8289 */
8290 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8291 // Store information regarding skip to try and opt-in the user
8292 // again in the future.
8293 $skip_info = array(
8294 'is' => $is_anonymous,
8295 'timestamp' => WP_FS__SCRIPT_START_TIME,
8296 'version' => $this->get_plugin_version(),
8297 );
8298
8299 if ( true === $network_or_blog_id ) {
8300 $this->_storage->is_anonymous_ms = $skip_info;
8301 } else {
8302 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8303 }
8304
8305 $this->network_upgrade_mode_completed();
8306
8307 // Update anonymous mode cache.
8308 $this->_is_anonymous = $is_anonymous;
8309 }
8310
8311 /**
8312 * @author Vova Feldman (@svovaf)
8313 * @since 2.5.1
8314 *
8315 * @param bool|int $network_or_blog_id
8316 */
8317 private function unset_anonymous_mode( $network_or_blog_id = 0 ) {
8318 if ( true === $network_or_blog_id ) {
8319 unset( $this->_storage->is_anonymous_ms );
8320 } else {
8321 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8322 }
8323 }
8324
8325 /**
8326 * @author Vova Feldman (@svovaf)
8327 * @since 2.0.0
8328 *
8329 * @param int $blog_id Site ID.
8330 * @param int $user_id User ID.
8331 * @param string $domain Site domain.
8332 * @param string $path Site path.
8333 * @param int $network_id Network ID. Only relevant on multi-network installations.
8334 * @param array $meta Metadata. Used to set initial site options.
8335 *
8336 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8337 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8338 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8339 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8340 */
8341 public function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8342 $this->_logger->entrance();
8343
8344 if ( ! $this->_is_network_active ) {
8345 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
8346 return;
8347 }
8348
8349 $site = null;
8350 $new_blog_id = $blog_id;
8351
8352 if ( $this->is_premium() &&
8353 $this->is_network_connected() &&
8354 is_object( $this->_license ) &&
8355 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8356 $this->is_license_network_active( $blog_id )
8357 ) {
8358 /**
8359 * 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.
8360 */
8361 $current_blog_id = get_current_blog_id();
8362 $license = clone $this->_license;
8363
8364 $this->switch_to_blog( $blog_id );
8365
8366 // Opt-in with network user.
8367 $this->install_with_user(
8368 $this->get_network_user(),
8369 $license->secret_key,
8370 false,
8371 false,
8372 false
8373 );
8374
8375 if ( is_object( $this->_site ) ) {
8376 if ( $this->_site->license_id == $license->id ) {
8377 /**
8378 * If the license was activated successfully, sync the license data from the remote server.
8379 */
8380 $this->_license = $license;
8381 $this->sync_site_license();
8382 }
8383 }
8384
8385 $site = $this->_site;
8386
8387 $this->switch_to_blog( $current_blog_id );
8388
8389 if ( is_object( $site ) ) {
8390 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id, $site );
8391
8392 // Already connected (with or without a license), so no need to continue.
8393 return;
8394 }
8395 }
8396
8397 if ( $this->is_network_anonymous() ) {
8398 /**
8399 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8400 */
8401 $this->skip_site_connection( $blog_id );
8402 } else if ( $this->is_network_delegated_connection() ) {
8403 /**
8404 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8405 */
8406 $this->delegate_site_connection( $blog_id );
8407 } else if ( $this->is_network_connected() ) {
8408 /**
8409 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8410 */
8411 $current_blog_id = get_current_blog_id();
8412
8413 $this->switch_to_blog( $blog_id );
8414
8415 // Opt-in with network user.
8416 $this->install_with_user(
8417 $this->get_network_user(),
8418 false,
8419 false,
8420 false,
8421 false
8422 );
8423
8424 $site = $this->_site;
8425
8426 $this->switch_to_blog( $current_blog_id );
8427 } else {
8428 /**
8429 * If the super-admin mixed different options (connect, skip, delegated):
8430 * a) If at least one site connection was delegated, then automatically delegate connection.
8431 * 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.
8432 */
8433 $has_delegated_site = false;
8434
8435 $sites = self::get_sites();
8436 foreach ( $sites as $wp_site ) {
8437 $blog_id = self::get_site_blog_id( $wp_site );
8438
8439 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8440 $has_delegated_site = true;
8441 break;
8442 }
8443 }
8444
8445 if ( $has_delegated_site ) {
8446 $this->delegate_site_connection( $blog_id );
8447 } else {
8448 $this->skip_site_connection( $blog_id );
8449 }
8450 }
8451
8452 /**
8453 * 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.
8454 *
8455 * @author Leo Fajardo (@leorw)
8456 * @since 2.5.0
8457 */
8458 FS_Clone_Manager::instance()->store_blog_install_info( $new_blog_id, $site );
8459 }
8460
8461 /**
8462 * @author Vova Feldman (@svovaf)
8463 * @since 2.5.0
8464 *
8465 * @param \WP_Site $new_site
8466 * @param array $args
8467 */
8468 public function _after_wp_initialize_site_callback( WP_Site $new_site, $args ) {
8469 $this->_logger->entrance();
8470
8471 $this->_after_new_blog_callback(
8472 $new_site->id,
8473 // Dummy user ID (not in use).
8474 0,
8475 $new_site->domain,
8476 $new_site->path,
8477 $new_site->network_id,
8478 // Dummy meta, not in use.
8479 array()
8480 );
8481 }
8482
8483 /**
8484 * @author Vova Feldman (@svovaf)
8485 * @since 1.1.3
8486 *
8487 * @param bool|int|int[] $network_or_blog_ids Since 2.0.0.
8488 */
8489 private function reset_anonymous_mode( $network_or_blog_ids = false ) {
8490 if ( true === $network_or_blog_ids ) {
8491 $this->unset_anonymous_mode( true );
8492
8493 if ( fs_is_network_admin() ) {
8494 $this->_is_anonymous = null;
8495 }
8496
8497 // Rest anonymous mode for all non-delegated sub-sites.
8498 $blog_ids = $this->get_non_delegated_blog_ids();
8499 }
8500 else
8501 {
8502 if ( false === $network_or_blog_ids ) {
8503 $network_or_blog_ids = 0;
8504 }
8505
8506 $blog_ids = is_array( $network_or_blog_ids ) ?
8507 $network_or_blog_ids :
8508 array( $network_or_blog_ids );
8509
8510 foreach ( $blog_ids as $blog_id ) {
8511 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8512 $this->_is_anonymous = null;
8513 }
8514 }
8515 }
8516
8517 foreach ( $blog_ids as $blog_id ) {
8518 $this->unset_anonymous_mode( $blog_id );
8519 }
8520
8521 /**
8522 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8523 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8524 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8525 *
8526 * @author Leo Fajardo (@leorw)
8527 * @since 1.2.2
8528 */
8529 if ( ! $this->_is_network_active ) {
8530 $this->_is_anonymous = null;
8531 }
8532 }
8533
8534 /**
8535 * @author Leo Fajardo (@leorw)
8536 * @since 2.5.3
8537 */
8538 private function update_license_required_permissions_if_anonymous() {
8539 if ( ! $this->is_anonymous() ) {
8540 return;
8541 }
8542
8543 $this->reset_anonymous_mode( fs_is_network_admin() );
8544
8545 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
8546 'essentials' => true,
8547 'events' => true,
8548 'diagnostic' => false,
8549 'extensions' => false,
8550 'site' => false,
8551 ) );
8552 }
8553
8554 /**
8555 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8556 * deleting the account in the network level, the URL of the page to redirect to is correct.
8557 *
8558 * @author Leo Fajardo (@leorw)
8559 *
8560 * @since 2.1.3
8561 */
8562 private function maybe_set_slug_and_network_menu_exists_flag() {
8563 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8564 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8565 $this->_menu->get_slug() :
8566 $this->_slug
8567 );
8568 }
8569 }
8570
8571 /**
8572 * Clears the anonymous mode and redirects to the opt-in screen.
8573 *
8574 * @author Vova Feldman (@svovaf)
8575 * @since 1.1.7
8576 */
8577 function connect_again() {
8578 if ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) {
8579 return;
8580 }
8581
8582 if ( $this->is_anonymous() ) {
8583 $this->reset_anonymous_mode( fs_is_network_admin() );
8584 }
8585
8586 $activation_url_params = array();
8587
8588 if ( $this->is_pending_activation() ) {
8589 $this->clear_pending_activation_mode();
8590
8591 if ( fs_request_get_bool( 'require_license' ) ) {
8592 $activation_url_params['require_license'] = true;
8593 }
8594 }
8595
8596 $this->maybe_set_slug_and_network_menu_exists_flag();
8597
8598 fs_redirect( $this->get_activation_url( $activation_url_params ) );
8599 }
8600
8601 /**
8602 * Skip account connect, and set anonymous mode.
8603 *
8604 * @author Vova Feldman (@svovaf)
8605 * @since 1.1.1
8606 *
8607 * @param bool|int|int[] $network_or_blog_ids Since 2.5.1
8608 */
8609 function skip_connection( $network_or_blog_ids = false ) {
8610 $this->_logger->entrance();
8611
8612 $this->_admin_notices->remove_sticky( 'connect_account' );
8613
8614 if ( true === $network_or_blog_ids ) {
8615 $this->set_anonymous_mode( true, true );
8616
8617 if ( fs_is_network_admin() ) {
8618 $this->_is_anonymous = null;
8619 }
8620
8621 // Rest anonymous mode for all non-delegated sub-sites.
8622 $blog_ids = $this->get_non_delegated_blog_ids();
8623 }
8624 else
8625 {
8626 if ( false === $network_or_blog_ids ) {
8627 $network_or_blog_ids = 0;
8628 }
8629
8630 $blog_ids = is_array( $network_or_blog_ids ) ?
8631 $network_or_blog_ids :
8632 array( $network_or_blog_ids );
8633
8634 foreach ( $blog_ids as $blog_id ) {
8635 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8636 $this->_is_anonymous = null;
8637 }
8638 }
8639 }
8640
8641 foreach ( $blog_ids as $blog_id ) {
8642 $this->skip_site_connection( $blog_id );
8643 }
8644
8645 $this->network_upgrade_mode_completed();
8646 }
8647
8648 /**
8649 * Skip connection for specific site in the network.
8650 *
8651 * @author Vova Feldman (@svovaf)
8652 * @since 2.0.0
8653 *
8654 * @param int|null $blog_id
8655 * @param bool $send_skip
8656 */
8657 private function skip_site_connection( $blog_id = null ) {
8658 $this->_logger->entrance();
8659
8660 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8661
8662 $this->set_anonymous_mode( true, $blog_id );
8663 }
8664
8665 /**
8666 * Plugin version update hook.
8667 *
8668 * @author Vova Feldman (@svovaf)
8669 * @since 1.0.4
8670 */
8671 private function update_plugin_version_event() {
8672 $this->_logger->entrance();
8673
8674 if ( ! $this->is_registered() ) {
8675 return;
8676 }
8677
8678 $this->maybe_schedule_install_sync_cron();
8679 // $this->sync_install( array(), true );
8680 }
8681
8682 /**
8683 * Generate an MD5 signature of a plugins collection.
8684 * This helper methods used to identify changes in a plugins collection.
8685 *
8686 * @author Vova Feldman (@svovaf)
8687 * @since 2.0.0
8688 *
8689 * @param array [string]array $plugins
8690 *
8691 * @return string
8692 */
8693 private function get_plugins_thumbprint( $plugins ) {
8694 ksort( $plugins );
8695
8696 $thumbprint = '';
8697 foreach ( $plugins as $basename => $data ) {
8698 $thumbprint .= $data['slug'] . ',' .
8699 $data['Version'] . ',' .
8700 ( $data['is_active'] ? '1' : '0' ) . ';';
8701 }
8702
8703 return md5( $thumbprint );
8704 }
8705
8706 /**
8707 * Return a list of modified plugins since the last sync.
8708 *
8709 * Note:
8710 * There's no point to store a plugins counter since even if the number of
8711 * plugins didn't change, we still need to check if the versions are all the
8712 * same and the activity state is similar.
8713 *
8714 * @author Vova Feldman (@svovaf)
8715 * @since 1.1.8
8716 *
8717 * @return array|false
8718 */
8719 private function get_plugins_data_for_api() {
8720 // Alias.
8721 $site_active_plugins_option_name = 'active_plugins';
8722 $network_plugins_option_name = 'all_plugins';
8723
8724 /**
8725 * Collection of all site level active plugins.
8726 */
8727 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8728
8729 if ( ! is_object( $site_active_plugins_cache ) ) {
8730 $site_active_plugins_cache = (object) array(
8731 'timestamp' => '',
8732 'md5' => '',
8733 'plugins' => array(),
8734 );
8735 }
8736
8737 $time = time();
8738
8739 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
8740 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8741 ) {
8742 // Don't send plugin updates if last update was in the past 5 min.
8743 return false;
8744 }
8745
8746 // Write timestamp to lock the logic.
8747 $site_active_plugins_cache->timestamp = $time;
8748 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8749
8750 // Reload options from DB.
8751 self::$_accounts->load( true );
8752 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8753
8754 if ( $time != $site_active_plugins_cache->timestamp ) {
8755 // If timestamp is different, then another thread captured the lock.
8756 return false;
8757 }
8758
8759 /**
8760 * Collection of all plugins (network level).
8761 */
8762 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
8763
8764 if ( ! is_object( $network_plugins_cache ) ) {
8765 $network_plugins_cache = (object) array(
8766 'timestamp' => '',
8767 'md5' => '',
8768 'plugins' => array(),
8769 );
8770 }
8771
8772 // Check if there's a change in plugins.
8773 $network_plugins = self::get_network_plugins();
8774 $site_active_plugins = self::get_site_active_plugins();
8775
8776 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
8777 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
8778
8779 // Check if plugins status changed (version or active/inactive).
8780 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
8781 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
8782
8783 if ( ! $network_plugins_changed &&
8784 ! $site_active_plugins_changed
8785 ) {
8786 // No changes.
8787 return array();
8788 }
8789
8790 $plugins_update_data = array();
8791
8792 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
8793 if ( ! isset( $network_plugins[ $basename ] ) ) {
8794 // Plugin uninstalled.
8795 $uninstalled_plugin_data = $data;
8796 $uninstalled_plugin_data['is_active'] = false;
8797 $uninstalled_plugin_data['is_uninstalled'] = true;
8798 $plugins_update_data[] = $uninstalled_plugin_data;
8799
8800 unset( $network_plugins[ $basename ] );
8801
8802 unset( $network_plugins_cache->plugins[ $basename ] );
8803 unset( $site_active_plugins_cache->plugins[ $basename ] );
8804
8805 continue;
8806 }
8807
8808 $was_active = $data['is_active'] ||
8809 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8810 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
8811 $is_active = $network_plugins[ $basename ]['is_active'] ||
8812 ( isset( $site_active_plugins[ $basename ] ) &&
8813 $site_active_plugins[ $basename ]['is_active'] );
8814
8815 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8816 isset( $site_active_plugins[ $basename ] )
8817 ) {
8818 // Plugin was site level activated.
8819 $site_active_plugins_cache->plugins[ $basename ] = array(
8820 'slug' => $network_plugins[ $basename ]['slug'],
8821 'version' => $network_plugins[ $basename ]['Version'],
8822 'title' => $network_plugins[ $basename ]['Name'],
8823 'is_active' => $is_active,
8824 'is_uninstalled' => false,
8825 );
8826 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8827 ! isset( $site_active_plugins[ $basename ] )
8828 ) {
8829 // Plugin was site level deactivated.
8830 unset( $site_active_plugins_cache->plugins[ $basename ] );
8831 }
8832
8833 $prev_version = $data['version'];
8834 $current_version = $network_plugins[ $basename ]['Version'];
8835
8836 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
8837 // Plugin activated or deactivated, or version changed.
8838
8839 if ( $was_active !== $is_active ) {
8840 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
8841 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
8842 }
8843 }
8844
8845 if ( $prev_version !== $current_version ) {
8846 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
8847 }
8848
8849 $updated_plugin_data = $data;
8850 $updated_plugin_data['is_active'] = $is_active;
8851 $updated_plugin_data['version'] = $current_version;
8852 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
8853 $plugins_update_data[] = $updated_plugin_data;
8854 }
8855 }
8856
8857 // Find new plugins that weren't yet seen before.
8858 foreach ( $network_plugins as $basename => $data ) {
8859 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
8860 // New plugin.
8861 $new_plugin = array(
8862 'slug' => $data['slug'],
8863 'version' => $data['Version'],
8864 'title' => $data['Name'],
8865 'is_active' => $data['is_active'],
8866 'is_uninstalled' => false,
8867 );
8868
8869 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
8870
8871 $is_site_level_active = (
8872 isset( $site_active_plugins[ $basename ] ) &&
8873 $site_active_plugins[ $basename ]['is_active']
8874 );
8875
8876 /**
8877 * If not network active, set the activity status based on the site-level plugin status.
8878 */
8879 if ( ! $new_plugin['is_active'] ) {
8880 $new_plugin['is_active'] = $is_site_level_active;
8881 }
8882
8883 $plugins_update_data[] = $new_plugin;
8884
8885 if ( isset( $site_active_plugins[ $basename ] ) ) {
8886 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
8887 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
8888 }
8889 }
8890 }
8891
8892 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
8893 $site_active_plugins_cache->timestamp = $time;
8894 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8895
8896 $network_plugins_cache->md5 = $network_plugins_thumbprint;
8897 $network_plugins_cache->timestamp = $time;
8898 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
8899
8900 return $plugins_update_data;
8901 }
8902
8903 /**
8904 * Return a list of modified themes since the last sync.
8905 *
8906 * Note:
8907 * There's no point to store a themes counter since even if the number of
8908 * themes didn't change, we still need to check if the versions are all the
8909 * same and the activity state is similar.
8910 *
8911 * @author Vova Feldman (@svovaf)
8912 * @since 1.1.8
8913 *
8914 * @return array|false
8915 */
8916 private function get_themes_data_for_api() {
8917 // Alias.
8918 $option_name = 'all_themes';
8919
8920 $all_cached_themes = self::$_accounts->get_option( $option_name );
8921
8922 if ( ! is_object( $all_cached_themes ) ) {
8923 $all_cached_themes = (object) array(
8924 'timestamp' => '',
8925 'md5' => '',
8926 'themes' => array(),
8927 );
8928 }
8929
8930 $time = time();
8931
8932 if ( ! empty( $all_cached_themes->timestamp ) &&
8933 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8934 ) {
8935 // Don't send theme updates if last update was in the past 5 min.
8936 return false;
8937 }
8938
8939 // Write timestamp to lock the logic.
8940 $all_cached_themes->timestamp = $time;
8941 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
8942
8943 // Reload options from DB.
8944 self::$_accounts->load( true );
8945 $all_cached_themes = self::$_accounts->get_option( $option_name );
8946
8947 if ( $time != $all_cached_themes->timestamp ) {
8948 // If timestamp is different, then another thread captured the lock.
8949 return false;
8950 }
8951
8952 // Get active theme.
8953 $active_theme = wp_get_theme();
8954 $active_theme_stylesheet = $active_theme->get_stylesheet();
8955
8956 // Check if there's a change in themes.
8957 $all_themes = wp_get_themes();
8958
8959 // Check if themes changed.
8960 ksort( $all_themes );
8961
8962 $themes_signature = '';
8963 foreach ( $all_themes as $slug => $data ) {
8964 $is_active = ( $slug === $active_theme_stylesheet );
8965 $themes_signature .= $slug . ',' .
8966 $data->version . ',' .
8967 ( $is_active ? '1' : '0' ) . ';';
8968 }
8969
8970 // Check if themes status changed (version or active/inactive).
8971 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
8972
8973 $themes_update_data = array();
8974
8975 if ( $themes_changed ) {
8976 // Change in themes, report changes.
8977
8978 // Update existing themes info.
8979 foreach ( $all_cached_themes->themes as $slug => $data ) {
8980 $is_active = ( $slug === $active_theme_stylesheet );
8981
8982 if ( ! isset( $all_themes[ $slug ] ) ) {
8983 // Plugin uninstalled.
8984 $uninstalled_theme_data = $data;
8985 $uninstalled_theme_data['is_active'] = false;
8986 $uninstalled_theme_data['is_uninstalled'] = true;
8987 $themes_update_data[] = $uninstalled_theme_data;
8988
8989 unset( $all_themes[ $slug ] );
8990 unset( $all_cached_themes->themes[ $slug ] );
8991 } else if ( $data['is_active'] !== $is_active ||
8992 $data['version'] !== $all_themes[ $slug ]->version
8993 ) {
8994 // Plugin activated or deactivated, or version changed.
8995
8996 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
8997 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
8998
8999 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
9000 }
9001 }
9002
9003 // Find new themes that weren't yet seen before.
9004 foreach ( $all_themes as $slug => $data ) {
9005 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
9006 $is_active = ( $slug === $active_theme_stylesheet );
9007
9008 // New plugin.
9009 $new_plugin = array(
9010 'slug' => $slug,
9011 'version' => $data->version,
9012 'title' => $data->name,
9013 'is_active' => $is_active,
9014 'is_uninstalled' => false,
9015 );
9016
9017 $themes_update_data[] = $new_plugin;
9018 $all_cached_themes->themes[ $slug ] = $new_plugin;
9019 }
9020 }
9021
9022 $all_cached_themes->md5 = md5( $themes_signature );
9023 $all_cached_themes->timestamp = time();
9024 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9025 }
9026
9027 return $themes_update_data;
9028 }
9029
9030 /**
9031 * Get site data for API install request.
9032 *
9033 * @author Vova Feldman (@svovaf)
9034 * @since 1.1.2
9035 *
9036 * @param string[] $override
9037 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9038 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9039 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, title, and URL).
9040 *
9041 * @return array
9042 */
9043 private function get_install_data_for_api(
9044 array $override,
9045 $include_plugins = true,
9046 $include_themes = true,
9047 $include_blog_data = true
9048 ) {
9049 // Alias.
9050 $permissions = FS_Permission_Manager::instance( $this );
9051
9052 if ( $permissions->is_extensions_tracking_allowed() ) {
9053 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
9054 /**
9055 * @since 1.1.8 Also send plugin updates.
9056 */
9057 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9058 $plugins = $this->get_plugins_data_for_api();
9059 if ( ! empty( $plugins ) ) {
9060 $override['plugins'] = $plugins;
9061 }
9062 }
9063 }
9064
9065 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
9066 /**
9067 * @since 1.1.8 Also send themes updates.
9068 */
9069 if ( $include_themes && ! isset( $override['themes'] ) ) {
9070 $themes = $this->get_themes_data_for_api();
9071 if ( ! empty( $themes ) ) {
9072 $override['themes'] = $themes;
9073 }
9074 }
9075 }
9076 }
9077
9078 $versions = $this->get_versions();
9079
9080 $blog_data = array();
9081 if ( $include_blog_data ) {
9082 $blog_data['url'] = self::get_unfiltered_site_url();
9083
9084 if ( $permissions->is_diagnostic_tracking_allowed() ) {
9085 $blog_data = array_merge( $blog_data, array(
9086 'language' => self::get_sanitized_language(),
9087 'title' => get_bloginfo( 'name' ),
9088 ) );
9089 }
9090 }
9091
9092 return array_merge( $versions, $blog_data, array(
9093 'version' => $this->get_plugin_version(),
9094 'is_premium' => $this->is_premium(),
9095 // Special params.
9096 'is_active' => true,
9097 'is_uninstalled' => false,
9098 ), $override );
9099 }
9100
9101 /**
9102 * Update installs details.
9103 *
9104 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
9105 *
9106 * @author Vova Feldman (@svovaf)
9107 * @since 2.0.0
9108 *
9109 * @param string[] string $override
9110 * @param bool $only_diff
9111 * @param bool $is_keepalive
9112 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9113 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9114 *
9115 * @return array
9116 */
9117 private function get_installs_data_for_api(
9118 array $override,
9119 $only_diff = false,
9120 $is_keepalive = false,
9121 $include_plugins = true,
9122 $include_themes = true
9123 ) {
9124 /**
9125 * @since 1.1.8 Also send plugin updates.
9126 */
9127 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9128 // $plugins = $this->get_plugins_data_for_api();
9129 // if ( ! empty( $plugins ) ) {
9130 // $override['plugins'] = $plugins;
9131 // }
9132 // }
9133 /**
9134 * @since 1.1.8 Also send themes updates.
9135 */
9136 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9137 // $themes = $this->get_themes_data_for_api();
9138 // if ( ! empty( $themes ) ) {
9139 // $override['themes'] = $themes;
9140 // }
9141 // }
9142
9143 // Common properties.
9144 $versions = $this->get_versions();
9145 $common = array_merge( $versions, array(
9146 'version' => $this->get_plugin_version(),
9147 'is_premium' => $this->is_premium(),
9148 ), $override );
9149
9150
9151 $is_common_diff_for_any_site = false;
9152 $common_diff_union = array();
9153
9154 $installs_data = array();
9155
9156 $sites = self::get_sites();
9157
9158 $subsite_data_for_api_by_install_id = array();
9159 $install_url_by_install_id = array();
9160 $subsite_registration_date_by_install_id = array();
9161
9162 foreach ( $sites as $site ) {
9163 $blog_id = self::get_site_blog_id( $site );
9164
9165 $install = $this->get_install_by_blog_id( $blog_id );
9166
9167 if ( is_object( $install ) ) {
9168 if ( $install->user_id != $this->_user->id ) {
9169 // Install belongs to a different owner.
9170 continue;
9171 }
9172
9173 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
9174 // Don't send updates regarding opted-out installs.
9175 continue;
9176 }
9177
9178 $install_data = $this->get_site_info( $site, true );
9179
9180 if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $install_data['blog_id'] ) ) {
9181 continue;
9182 }
9183
9184 $uid = $install_data['uid'];
9185 $url = $install_data['url'];
9186 $registration_date = $install_data['registration_date'];
9187
9188 if ( isset( $subsite_data_for_api_by_install_id[ $install->id ] ) ) {
9189 $clone_subsite_data = $subsite_data_for_api_by_install_id[ $install->id ];
9190 $clone_install_url = $install_url_by_install_id[ $install->id ];
9191 $clone_subsite_registration_date = $subsite_registration_date_by_install_id[ $install->id ];
9192
9193 $skip = false;
9194
9195 if (
9196 ! empty( $install_data['registration_date'] ) &&
9197 ! empty( $clone_subsite_registration_date )
9198 ) {
9199 /**
9200 * 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.
9201 *
9202 * @author Leo Fajardo (@leorw)
9203 * @since 2.5.1
9204 */
9205 $skip = ( strtotime( $install_data['registration_date'] ) > strtotime( $clone_subsite_registration_date ) );
9206 } else if (
9207 /**
9208 * 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.
9209 *
9210 * @author Leo Fajardo (@leorw)
9211 * @since 2.5.0
9212 */
9213 fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) ||
9214 fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $url ) )
9215 ) {
9216 $skip = true;
9217 }
9218
9219 if ( $skip ) {
9220 // 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.
9221 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
9222 continue;
9223 }
9224 }
9225
9226 unset( $install_data['blog_id'] );
9227 unset( $install_data['uid'] );
9228 unset( $install_data['url'] );
9229 unset( $install_data['registration_date'] );
9230
9231 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9232 $install_data['is_uninstalled'] = $install->is_uninstalled;
9233
9234 $common_diff = null;
9235 $is_common_diff = false;
9236 if ( $only_diff ) {
9237 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9238 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9239
9240 $is_common_diff = ! empty( $common_diff );
9241
9242 if ( $is_common_diff ) {
9243 foreach ( $common_diff as $k => $v ) {
9244 if ( ! isset( $common_diff_union[ $k ] ) ) {
9245 $common_diff_union[ $k ] = $v;
9246 }
9247 }
9248 }
9249
9250 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9251 }
9252
9253 if ( ! empty( $install_data ) || $is_common_diff || $is_keepalive ) {
9254 // Add install ID and site unique ID.
9255 $install_data['id'] = $install->id;
9256 $install_data['uid'] = $uid;
9257 $install_data['url'] = $url;
9258
9259 $subsite_data_for_api_by_install_id[ $install->id ] = $install_data;
9260 $install_url_by_install_id[ $install->id ] = $install->url;
9261 $subsite_registration_date_by_install_id[ $install->id ] = $registration_date;
9262 }
9263 }
9264 }
9265
9266 restore_current_blog();
9267
9268 $installs_data = array_merge(
9269 $installs_data,
9270 array_values( $subsite_data_for_api_by_install_id )
9271 );
9272
9273 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9274 if ( ! $only_diff ) {
9275 $installs_data[] = $common;
9276 } else if ( ! empty( $common_diff_union ) ) {
9277 $installs_data[] = $common_diff_union;
9278 }
9279 }
9280
9281 foreach ( $installs_data as &$data ) {
9282 $data = (object) $data;
9283 }
9284
9285 return $installs_data;
9286 }
9287
9288 /**
9289 * Compare site actual data to the stored install data and return the differences for an API data sync.
9290 *
9291 * @author Vova Feldman (@svovaf)
9292 * @since 2.0.0
9293 *
9294 * @param array $site
9295 * @param FS_Site $install
9296 * @param string[] string $override
9297 *
9298 * @return array
9299 */
9300 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9301 $diff = array();
9302 $special = array();
9303 $special_override = false;
9304
9305 foreach ( $site as $p => $v ) {
9306 if ( property_exists( $install, $p ) ) {
9307 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9308 $install->{$p} != $v
9309 ) {
9310 $val = self::get_api_sanitized_property( $p, $v );
9311
9312 if ( $install->{$p} != $val ) {
9313 $install->{$p} = $val;
9314 $diff[ $p ] = $val;
9315 }
9316 }
9317 } else {
9318 $special[ $p ] = $v;
9319
9320 if ( isset( $override[ $p ] ) ||
9321 'plugins' === $p ||
9322 'themes' === $p
9323 ) {
9324 $special_override = true;
9325 }
9326 }
9327 }
9328
9329 if ( $special_override || 0 < count( $diff ) ) {
9330 // Add special params only if has at least one
9331 // standard param, or if explicitly requested to
9332 // override a special param or a param which is not exist
9333 // in the install object.
9334 $diff = array_merge( $diff, $special );
9335 }
9336
9337 return $diff;
9338 }
9339
9340 /**
9341 * @author Leo Fajardo (@leorw)
9342 * @since 2.5.1
9343 */
9344 private function send_pending_clone_update_once() {
9345 $this->_logger->entrance();
9346
9347 if ( ! empty( $this->_storage->clone_id ) ) {
9348 return;
9349 }
9350
9351 $install_clone = $this->get_api_site_scope()->call(
9352 '/clones',
9353 'post',
9354 array( 'site_url' => self::get_unfiltered_site_url() )
9355 );
9356
9357 if ( $this->is_api_result_entity( $install_clone ) ) {
9358 $this->_storage->clone_id = $install_clone->id;
9359 }
9360 }
9361
9362 /**
9363 * @author Leo Fajardo (@leorw)
9364 * @since 2.5.1
9365 *
9366 * @param string $resolution_type
9367 * @param FS_Site $clone_context_install
9368 */
9369 function send_clone_resolution_update( $resolution_type, $clone_context_install ) {
9370 $this->_logger->entrance();
9371
9372 if ( empty( $this->_storage->clone_id ) ) {
9373 return;
9374 }
9375
9376 $new_install_id = null;
9377 $current_site = null;
9378
9379 $flush = false;
9380
9381 /**
9382 * 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.
9383 */
9384 if ( $clone_context_install->id != $this->_site->id ) {
9385 $new_install_id = $this->_site->id;
9386 $current_site = $this->_site;
9387 $this->_site = $clone_context_install;
9388
9389 $flush = true;
9390 }
9391
9392 $this->get_api_site_scope( $flush )->call(
9393 "/clones/{$this->_storage->clone_id}",
9394 'put',
9395 array(
9396 'resolution' => $resolution_type,
9397 'new_install_id' => $new_install_id,
9398 )
9399 );
9400
9401 if ( is_object( $current_site ) ) {
9402 /**
9403 * Ensure that the install scope entity is updated back to the previous install entity.
9404 */
9405 $this->_site = $current_site;
9406
9407 // Restore the previous install scope entity of the API.
9408 $this->get_api_site_scope( true );
9409 }
9410 }
9411
9412 /**
9413 * Update install only if changed.
9414 *
9415 * @author Vova Feldman (@svovaf)
9416 * @since 1.0.9
9417 *
9418 * @param string[] string $override
9419 * @param bool $flush
9420 * @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.
9421 *
9422 * @return false|object|string
9423 */
9424 private function send_install_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9425 $this->_logger->entrance();
9426
9427 $check_properties = $this->get_install_data_for_api( $override );
9428
9429 if ( $flush ) {
9430 $params = $check_properties;
9431 } else {
9432 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9433 }
9434
9435 if ( empty( $params ) ) {
9436 $keepalive_only_update = $this->should_send_keepalive_update();
9437
9438 if ( ! $keepalive_only_update ) {
9439 /**
9440 * There are no updates to send including keepalive.
9441 *
9442 * @author Leo Fajardo (@leorw)
9443 * @since 2.2.3
9444 */
9445 return false;
9446 }
9447 }
9448
9449 if ( $is_two_way_sync ) {
9450 /**
9451 * Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9452 *
9453 * @author Leo Fajardo (@leorw)
9454 * @since 2.2.3
9455 */
9456 if ( ! is_multisite() ) {
9457 // Update last install sync timestamp.
9458 $this->set_cron_execution_timestamp( 'install_sync' );
9459 }
9460
9461 $params['uid'] = $this->get_anonymous_id();
9462 }
9463
9464 $this->set_keepalive_timestamp();
9465
9466 // Send updated values to FS.
9467 $site = $this->api_site_call( '/', 'put', $params, true );
9468
9469 if ( $is_two_way_sync && $this->is_api_result_entity( $site ) ) {
9470 /**
9471 * Clear scheduled install sync after a two-way sync call.
9472 *
9473 * @author Leo Fajardo (@leorw)
9474 * @since 2.2.3
9475 */
9476 if ( ! is_multisite() ) {
9477 // I successfully sent install update, clear scheduled sync if exist.
9478 $this->clear_install_sync_cron();
9479 }
9480 }
9481
9482 return $site;
9483 }
9484
9485 /**
9486 * Update installs only if changed.
9487 *
9488 * @author Vova Feldman (@svovaf)
9489 * @since 2.0.0
9490 *
9491 * @param string[] string $override
9492 * @param bool $flush
9493 * @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.
9494 *
9495 * @return false|object|string
9496 */
9497 private function send_installs_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9498 $this->_logger->entrance();
9499
9500 /**
9501 * Pass `true` to use the network level storage since the update is for many installs.
9502 *
9503 * @author Leo Fajardo (@leorw)
9504 * @since 2.2.3
9505 */
9506 $should_send_keepalive = $this->should_send_keepalive_update( true );
9507
9508 $installs_data = $this->get_installs_data_for_api( $override, ! $flush, $should_send_keepalive );
9509
9510 if ( empty( $installs_data ) ) {
9511 return false;
9512 }
9513
9514 if ( $is_two_way_sync ) {
9515 // Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9516 $this->set_cron_execution_timestamp( 'install_sync' );
9517 }
9518
9519 /**
9520 * Pass `true` to use the network level storage since the update is for many installs.
9521 *
9522 * @author Leo Fajardo (@leorw)
9523 * @since 2.2.3
9524 */
9525 $this->set_keepalive_timestamp( true );
9526
9527 // Send updated values to FS.
9528 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9529
9530 if ( $is_two_way_sync && $this->is_api_result_object( $result, 'installs' ) ) {
9531 // I successfully sent a two-way installs update, clear the scheduled install sync if it exists.
9532 $this->clear_install_sync_cron();
9533 }
9534
9535 return $result;
9536 }
9537
9538 /**
9539 * @author Leo Fajardo (@leorw)
9540 *
9541 * @param bool|null $use_network_level_storage
9542 *
9543 * @return bool
9544 */
9545 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9546 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9547
9548 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9549 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9550 return true;
9551 } else {
9552 // 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.
9553 return ( 7 == rand( 1, 7 ) );
9554 }
9555 }
9556
9557 /**
9558 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9559 *
9560 * @author Leo Fajardo (@leorw)
9561 * @since 2.3.2
9562 */
9563 private function maybe_sync_install_user() {
9564 if ( $this->_user->id == $this->_site->user_id ) {
9565 return;
9566 }
9567
9568 // Fetch user data and store if found.
9569 $this->sync_user_by_current_install();
9570 }
9571
9572 /**
9573 * Update install only if changed.
9574 *
9575 * @author Vova Feldman (@svovaf)
9576 * @since 1.0.9
9577 *
9578 * @param string[] string $override
9579 * @param bool $flush
9580 */
9581 function sync_install( $override = array(), $flush = false ) {
9582 $this->_logger->entrance();
9583
9584 $site = $this->send_install_update( $override, $flush, true );
9585
9586 if ( false === $site ) {
9587 // No sync required.
9588 return;
9589 }
9590
9591 if ( ! $this->is_api_result_entity( $site ) ) {
9592 // Failed to sync, don't update locally.
9593 return;
9594 }
9595
9596 $this->_site = new FS_Site( $site );
9597
9598 $this->_store_site( true );
9599 }
9600
9601 /**
9602 * Update install only if changed.
9603 *
9604 * @author Vova Feldman (@svovaf)
9605 * @since 1.0.9
9606 *
9607 * @param string[] string $override
9608 * @param bool $flush
9609 */
9610 private function sync_installs( $override = array(), $flush = false ) {
9611 $this->_logger->entrance();
9612
9613 $result = $this->send_installs_update( $override, $flush, true );
9614
9615 if ( false === $result ) {
9616 // No sync required.
9617 return;
9618 }
9619
9620 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9621 // Failed to sync, don't update locally.
9622 return;
9623 }
9624
9625 $address_to_blog_map = $this->get_address_to_blog_map();
9626
9627 foreach ( $result->installs as $install ) {
9628 $this->_site = new FS_Site( $install );
9629
9630 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9631 $blog_id = $address_to_blog_map[ $address ];
9632
9633 $this->_store_site( true, $blog_id );
9634 }
9635 }
9636
9637 /**
9638 * Track install's custom event.
9639 *
9640 * IMPORTANT:
9641 * Custom event tracking is currently only supported for specific clients.
9642 * If you are not one of them, please don't use this method. If you will,
9643 * the API will simply ignore your request based on the plugin ID.
9644 *
9645 * Need custom tracking for your plugin or theme?
9646 * If you are interested in custom event tracking please contact [email protected]
9647 * for further details.
9648 *
9649 * @author Vova Feldman (@svovaf)
9650 * @since 1.2.1
9651 *
9652 * @param string $name Event name.
9653 * @param array $properties Associative key/value array with primitive values only
9654 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9655 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9656 *
9657 * @return object|false Event data or FALSE on failure.
9658 *
9659 * @throws \Freemius_InvalidArgumentException
9660 */
9661 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9662 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9663
9664 if ( ! $this->is_registered() ) {
9665 return false;
9666 }
9667
9668 $event = array( 'type' => $name );
9669
9670 if ( is_numeric( $process_at ) && $process_at > time() ) {
9671 $event['process_at'] = $process_at;
9672 }
9673
9674 if ( $once ) {
9675 $event['once'] = true;
9676 }
9677
9678 if ( ! empty( $properties ) ) {
9679 // Verify associative array values are primitive.
9680 foreach ( $properties as $k => $v ) {
9681 if ( ! is_scalar( $v ) ) {
9682 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9683 }
9684 }
9685
9686 $event['properties'] = $properties;
9687 }
9688
9689 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9690
9691 return $this->is_api_error( $result ) ?
9692 false :
9693 $result;
9694 }
9695
9696 /**
9697 * Track install's custom event only once, but it still triggers the API call.
9698 *
9699 * IMPORTANT:
9700 * Custom event tracking is currently only supported for specific clients.
9701 * If you are not one of them, please don't use this method. If you will,
9702 * the API will simply ignore your request based on the plugin ID.
9703 *
9704 * Need custom tracking for your plugin or theme?
9705 * If you are interested in custom event tracking please contact [email protected]
9706 * for further details.
9707 *
9708 * @author Vova Feldman (@svovaf)
9709 * @since 1.2.1
9710 *
9711 * @param string $name Event name.
9712 * @param array $properties Associative key/value array with primitive values only
9713 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9714 *
9715 * @return object|false Event data or FALSE on failure.
9716 *
9717 * @throws \Freemius_InvalidArgumentException
9718 *
9719 * @user Freemius::track_event()
9720 */
9721 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9722 return $this->track_event( $name, $properties, $process_at, true );
9723 }
9724
9725 /**
9726 * Plugin uninstall hook.
9727 *
9728 * @author Vova Feldman (@svovaf)
9729 * @since 1.0.1
9730 *
9731 * @param bool $check_user Enforce checking if user have plugins activation privileges.
9732 */
9733 function _uninstall_plugin_event( $check_user = true ) {
9734 $this->_logger->entrance( 'slug = ' . $this->_slug );
9735
9736 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
9737 return;
9738 }
9739
9740 $params = array();
9741 $uninstall_reason = null;
9742 if ( isset( $this->_storage->uninstall_reason ) ) {
9743 $uninstall_reason = $this->_storage->uninstall_reason;
9744 $params['reason_id'] = $uninstall_reason->id;
9745 $params['reason_info'] = $uninstall_reason->info;
9746 }
9747
9748 if ( ! $this->is_registered() ) {
9749 // Send anonymous uninstall event only if user submitted a feedback.
9750 if ( isset( $uninstall_reason ) ) {
9751 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
9752 $this->opt_in( false, false, false, false, true );
9753 } else {
9754 $params['uid'] = $this->get_anonymous_id();
9755 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
9756 }
9757 }
9758 } else {
9759 $params = array_merge( $params, array(
9760 'is_active' => false,
9761 'is_uninstalled' => true,
9762 ) );
9763
9764 if ( $this->_is_network_active ) {
9765 // Send uninstall event.
9766 $this->send_installs_update( $params );
9767 } else {
9768 // Send uninstall event and handle the result.
9769 $this->sync_install( $params );
9770 }
9771 }
9772
9773 // @todo Decide if we want to delete plugin information from db.
9774 }
9775
9776 /**
9777 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
9778 *
9779 * @author Vova Feldman (@svovaf)
9780 * @since 2.2.1
9781 *
9782 * @param string $is_premium
9783 * @param string $caller
9784 *
9785 * @return void
9786 */
9787 function set_basename( $is_premium, $caller ) {
9788 $basename = plugin_basename( $caller );
9789
9790 $current_basename = $is_premium ?
9791 $this->_premium_plugin_basename :
9792 $this->_free_plugin_basename;
9793
9794 if ( $current_basename == $basename ) {
9795 // Basename value set correctly.
9796 return;
9797 }
9798
9799 if ( $is_premium ) {
9800 $this->_premium_plugin_basename = $basename;
9801 } else {
9802 $this->_free_plugin_basename = $basename;
9803 }
9804
9805 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
9806
9807 register_activation_hook(
9808 $plugin_dir . $basename,
9809 array( &$this, '_activate_plugin_event_hook' )
9810 );
9811 }
9812
9813 /**
9814 * @author Vova Feldman (@svovaf)
9815 * @since 1.1.1
9816 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
9817 *
9818 * @return string
9819 */
9820 function premium_plugin_basename() {
9821 if ( ! isset( $this->_premium_plugin_basename ) ) {
9822 $this->_premium_plugin_basename = $this->is_premium() ?
9823 // The product is premium, so use the current basename.
9824 $this->_plugin_basename :
9825 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
9826 }
9827
9828 return $this->_premium_plugin_basename;
9829 }
9830
9831 /**
9832 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
9833 *
9834 * @author Vova Feldman (@svovaf)
9835 * @since 1.0.2
9836 */
9837 public static function _uninstall_plugin_hook() {
9838 self::_load_required_static();
9839
9840 self::$_static_logger->entrance();
9841
9842 if ( ! current_user_can( 'activate_plugins' ) ) {
9843 return;
9844 }
9845
9846 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
9847
9848 self::$_static_logger->info( 'plugin = ' . $plugin_file );
9849
9850 define( 'WP_FS__UNINSTALL_MODE', true );
9851
9852 $fs = self::get_instance_by_file( $plugin_file );
9853
9854 if ( is_object( $fs ) ) {
9855 $fs->remove_sdk_reference();
9856
9857 self::require_plugin_essentials();
9858
9859 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
9860 is_plugin_active( $fs->premium_plugin_basename() )
9861 ) {
9862 // Deleting Free or Premium plugin version while the other version still installed.
9863 return;
9864 }
9865
9866 if (
9867 ! $fs->is_clone() &&
9868 /**
9869 * 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).
9870 *
9871 * @author Leo Fajardo
9872 */
9873 ( ! is_object( $fs->_site ) || $fs->is_registered() )
9874 ) {
9875 $fs->_uninstall_plugin_event();
9876 }
9877
9878 $fs->do_action( 'after_uninstall' );
9879 }
9880 }
9881
9882 #----------------------------------------------------------------------------------
9883 #region Plugin Information
9884 #----------------------------------------------------------------------------------
9885
9886 /**
9887 * Load WordPress core plugin.php essential module.
9888 *
9889 * @author Vova Feldman (@svovaf)
9890 * @since 1.1.1
9891 */
9892 private static function require_plugin_essentials() {
9893 if ( ! function_exists( 'get_plugins' ) ) {
9894 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
9895
9896 require_once ABSPATH . 'wp-admin/includes/plugin.php';
9897 }
9898 }
9899
9900 /**
9901 * Load WordPress core pluggable.php module.
9902 *
9903 * @author Vova Feldman (@svovaf)
9904 * @since 1.1.2
9905 */
9906 private static function require_pluggable_essentials() {
9907 if ( ! function_exists( 'wp_get_current_user' ) ) {
9908 require_once ABSPATH . 'wp-includes/pluggable.php';
9909 }
9910 }
9911
9912 /**
9913 * Return plugin data.
9914 *
9915 * @author Vova Feldman (@svovaf)
9916 * @since 1.0.1
9917 *
9918 * @param bool $reparse_plugin_metadata
9919 *
9920 * @return array
9921 */
9922 function get_plugin_data( $reparse_plugin_metadata = false ) {
9923 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
9924 self::require_plugin_essentials();
9925
9926 if ( $this->is_plugin() ) {
9927 /**
9928 * @author Vova Feldman (@svovaf)
9929 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
9930 *
9931 * @link https://github.com/Freemius/wordpress-sdk/issues/77
9932 */
9933 $plugin_data = get_plugin_data(
9934 $this->_plugin_main_file_path,
9935 false,
9936 false
9937 );
9938 } else {
9939 $theme_data = wp_get_theme();
9940
9941 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
9942 $parent_theme = $theme_data->parent();
9943
9944 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
9945 $theme_data = $parent_theme;
9946 }
9947 }
9948
9949 $plugin_data = array(
9950 'Name' => $theme_data->get( 'Name' ),
9951 'Version' => $theme_data->get( 'Version' ),
9952 'Author' => $theme_data->get( 'Author' ),
9953 'Description' => $theme_data->get( 'Description' ),
9954 'PluginURI' => $theme_data->get( 'ThemeURI' ),
9955 );
9956 }
9957
9958 $this->_plugin_data = $plugin_data;
9959 }
9960
9961 return $this->_plugin_data;
9962 }
9963
9964 /**
9965 * @author Vova Feldman (@svovaf)
9966 * @since 1.0.1
9967 * @since 1.2.2.5 If slug not set load slug by module ID.
9968 *
9969 * @return string Plugin slug.
9970 */
9971 function get_slug() {
9972 if ( ! isset( $this->_slug ) ) {
9973 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
9974 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
9975 }
9976
9977 return $this->_slug;
9978 }
9979
9980 /**
9981 * @author Leo Fajardo (@leorw)
9982 * @since 2.2.1
9983 *
9984 * @return string
9985 */
9986 function get_premium_slug() {
9987 return ( is_object( $this->_plugin ) && ! empty( $this->_plugin->premium_slug ) ) ?
9988 $this->_plugin->premium_slug :
9989 "{$this->_slug}-premium";
9990 }
9991
9992 /**
9993 * Retrieve the desired folder name for the product.
9994 *
9995 * @author Vova Feldman (@svovaf)
9996 * @since 1.2.1.7
9997 *
9998 * @return string Plugin slug.
9999 */
10000 function get_target_folder_name() {
10001 return $this->can_use_premium_code() ?
10002 $this->_plugin->premium_slug :
10003 $this->_slug;
10004 }
10005
10006 /**
10007 * @author Vova Feldman (@svovaf)
10008 * @since 1.0.1
10009 *
10010 * @return number Plugin ID.
10011 */
10012 function get_id() {
10013 return $this->_plugin->id;
10014 }
10015
10016 /**
10017 * @author Leo Fajardo (@leorw)
10018 * @since 2.2.4
10019 *
10020 * @return number|null Bundle ID.
10021 */
10022 function get_bundle_id() {
10023 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
10024 $this->_plugin->bundle_id :
10025 null;
10026 }
10027
10028 /**
10029 * @author Vova Feldman (@svovaf)
10030 * @since 2.3.1
10031 *
10032 * @return string|null Bundle public key.
10033 */
10034 function get_bundle_public_key() {
10035 return isset( $this->_plugin->bundle_public_key ) ?
10036 $this->_plugin->bundle_public_key :
10037 null;
10038 }
10039
10040 /**
10041 * Get whether the SDK has been initiated in the context of a Bundle.
10042 *
10043 * This will return true, if `bundle_id` is present in the SDK init parameters.
10044 *
10045 * ```php
10046 * $my_fs = fs_dynamic_init( array(
10047 * // ...
10048 * 'bundle_id' => 'XXXX', // Will return true since we have bundle id.
10049 * 'bundle_public_key' => 'pk_XXXX',
10050 * ) );
10051 * ```
10052 *
10053 * @author Swashata Ghosh (@swashata)
10054 * @since 2.5.0
10055 *
10056 * @return bool True if we are running in bundle context, false otherwise.
10057 */
10058 private function has_bundle_context() {
10059 return ! is_null( $this->get_bundle_id() );
10060 }
10061
10062 /**
10063 * @author Vova Feldman (@svovaf)
10064 * @since 1.2.1.5
10065 *
10066 * @return string Freemius SDK version
10067 */
10068 function get_sdk_version() {
10069 return $this->version;
10070 }
10071
10072 /**
10073 * @author Vova Feldman (@svovaf)
10074 * @since 1.2.1.5
10075 *
10076 * @return number Parent plugin ID (if parent exist).
10077 */
10078 function get_parent_id() {
10079 return $this->is_addon() ?
10080 $this->get_parent_instance()->get_id() :
10081 $this->_plugin->id;
10082 }
10083
10084 /**
10085 * @author Vova Feldman (@svovaf)
10086 * @since 2.3.1
10087 *
10088 * @return string
10089 */
10090 function get_usage_tracking_terms_url() {
10091 return $this->apply_filters(
10092 'usage_tracking_terms_url',
10093 "https://freemius.com/product/opt-in/{$this->_plugin->id}/{$this->_slug}/"
10094 );
10095 }
10096
10097 /**
10098 * @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.
10099 * @since 2.5.8
10100 *
10101 * @return string
10102 */
10103 function get_license_activation_terms_url() {
10104 return $this->apply_filters(
10105 'license_activation_terms_url',
10106 "https://freemius.com/product/license-activation/{$this->_plugin->id}/{$this->_slug}/"
10107 );
10108 }
10109
10110 /**
10111 * @author Vova Feldman (@svovaf)
10112 * @since 2.3.1
10113 *
10114 * @return string
10115 */
10116 function get_eula_url() {
10117 return $this->apply_filters(
10118 'eula_url',
10119 "https://freemius.com/product/{$this->_plugin->id}/{$this->_slug}/legal/eula/"
10120 );
10121 }
10122
10123 /**
10124 * @author Vova Feldman (@svovaf)
10125 * @since 1.0.1
10126 *
10127 * @return string Plugin public key.
10128 */
10129 function get_public_key() {
10130 return $this->_plugin->public_key;
10131 }
10132
10133 /**
10134 * Will be available only on sandbox mode.
10135 *
10136 * @author Vova Feldman (@svovaf)
10137 * @since 1.0.4
10138 *
10139 * @return mixed Plugin secret key.
10140 */
10141 function get_secret_key() {
10142 return $this->_plugin->secret_key;
10143 }
10144
10145 /**
10146 * @author Vova Feldman (@svovaf)
10147 * @since 1.1.1
10148 *
10149 * @return bool
10150 */
10151 function has_secret_key() {
10152 return ! empty( $this->_plugin->secret_key );
10153 }
10154
10155 /**
10156 * @author Vova Feldman (@svovaf)
10157 * @since 1.0.9
10158 *
10159 * @param string|bool $premium_suffix
10160 *
10161 * @return string
10162 */
10163 function get_plugin_name( $premium_suffix = false ) {
10164 $this->_logger->entrance();
10165
10166 /**
10167 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10168 *
10169 * @author Vova Feldman
10170 */
10171 if ( ! isset( $this->_plugin_name ) ) {
10172 // Name is not yet set.
10173 $this->set_name( $premium_suffix );
10174 } else if (
10175 ! empty( $premium_suffix ) &&
10176 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10177 ) {
10178 // Name is already set, but there's a change in the premium suffix.
10179 $this->set_name( $premium_suffix );
10180 }
10181
10182 return $this->_plugin_name;
10183 }
10184
10185 /**
10186 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10187 *
10188 * @author Vova Feldman (@svovaf)
10189 * @since 2.2.1
10190 *
10191 * @param string $premium_suffix
10192 */
10193 private function set_name( $premium_suffix = '' ) {
10194 $plugin_data = $this->get_plugin_data();
10195
10196 // Get name.
10197 $this->_plugin_name = $plugin_data['Name'];
10198
10199 if ( is_string( $premium_suffix ) ) {
10200 $premium_suffix = trim( $premium_suffix );
10201
10202 if ( ! empty( $premium_suffix ) ) {
10203 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10204 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10205 $suffix_len = strlen( $suffix );
10206
10207 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10208 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10209 ) {
10210 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10211 }
10212 }
10213 }
10214
10215 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10216 }
10217
10218 /**
10219 * @author Vova Feldman (@svovaf)
10220 * @since 1.0.0
10221 *
10222 * @param bool $reparse_plugin_metadata
10223 *
10224 * @return string
10225 */
10226 function get_plugin_version( $reparse_plugin_metadata = false ) {
10227 $this->_logger->entrance();
10228
10229 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10230
10231 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10232
10233 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10234 }
10235
10236 /**
10237 * @author Vova Feldman (@svovaf)
10238 * @since 1.2.1.7
10239 *
10240 * @return string
10241 */
10242 function get_plugin_title() {
10243 $this->_logger->entrance();
10244
10245 $title = $this->_plugin->title;
10246
10247 return $this->apply_filters( 'plugin_title', $title );
10248 }
10249
10250 /**
10251 * @author Vova Feldman (@svovaf)
10252 * @since 1.2.2.7
10253 *
10254 * @param bool $lowercase
10255 *
10256 * @return string
10257 */
10258 function get_module_label( $lowercase = false ) {
10259 $label = $this->is_addon() ?
10260 $this->get_text_inline( 'Add-On', 'addon' ) :
10261 ( $this->is_plugin() ?
10262 $this->get_text_inline( 'Plugin', 'plugin' ) :
10263 $this->get_text_inline( 'Theme', 'theme' ) );
10264
10265 if ( $lowercase ) {
10266 $label = strtolower( $label );
10267 }
10268
10269 return $label;
10270 }
10271
10272 /**
10273 * @author Vova Feldman (@svovaf)
10274 * @since 1.0.4
10275 *
10276 * @return string
10277 */
10278 function get_plugin_basename() {
10279 if ( ! isset( $this->_plugin_basename ) ) {
10280 if ( $this->is_plugin() ) {
10281 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10282 } else {
10283 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10284 }
10285 }
10286
10287 return $this->_plugin_basename;
10288 }
10289
10290 function get_plugin_folder_name() {
10291 $this->_logger->entrance();
10292
10293 $plugin_folder = $this->_plugin_basename;
10294
10295 while ( '.' !== dirname( $plugin_folder ) ) {
10296 $plugin_folder = dirname( $plugin_folder );
10297 }
10298
10299 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10300
10301 return $plugin_folder;
10302 }
10303
10304 #endregion ------------------------------------------------------------------
10305
10306 /* Account
10307 ------------------------------------------------------------------------------------------------------------------*/
10308
10309 /**
10310 * Find plugin's slug by plugin's basename.
10311 *
10312 * @author Vova Feldman (@svovaf)
10313 * @since 1.0.9
10314 *
10315 * @param string $plugin_base_name
10316 *
10317 * @return false|string
10318 */
10319 private static function find_slug_by_basename( $plugin_base_name ) {
10320 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10321
10322 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10323 return false;
10324 }
10325
10326 return $file_slug_map[ $plugin_base_name ];
10327 }
10328
10329 /**
10330 * Store the map between the plugin's basename to the slug.
10331 *
10332 * @author Vova Feldman (@svovaf)
10333 * @since 1.0.9
10334 */
10335 private function store_file_slug_map() {
10336 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10337
10338 if ( ! array( $file_slug_map ) ) {
10339 $file_slug_map = array();
10340 }
10341
10342 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10343 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10344 ) {
10345 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10346 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10347 }
10348 }
10349
10350 /**
10351 * @return array[number]FS_User
10352 */
10353 static function get_all_users() {
10354 $users = self::maybe_get_entities_account_option( 'users', array() );
10355
10356 if ( ! is_array( $users ) ) {
10357 $users = array();
10358 }
10359
10360 return $users;
10361 }
10362
10363 /**
10364 * @param string $module_type
10365 * @param null|int $blog_id Since 2.0.0
10366 *
10367 * @return array[string]FS_Site
10368 */
10369 public static function get_all_sites(
10370 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10371 $blog_id = null,
10372 $is_backup = false
10373 ) {
10374 $sites = self::get_account_option(
10375 ( $is_backup ? 'prev_' : '' ) . 'sites',
10376 $module_type,
10377 $blog_id
10378 );
10379
10380 if ( ! is_array( $sites ) ) {
10381 $sites = array();
10382 }
10383
10384 return $sites;
10385 }
10386
10387 /**
10388 * @author Leo Fajardo (@leorw)
10389 *
10390 * @since 1.2.2
10391 *
10392 * @param string $option_name
10393 * @param string $module_type
10394 * @param null|int $network_level_or_blog_id Since 2.0.0
10395 *
10396 * @return mixed
10397 */
10398 public static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10399 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10400 $option_name = $module_type . '_' . $option_name;
10401 }
10402
10403 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10404 }
10405
10406 /**
10407 * @author Leo Fajardo (@leorw)
10408 *
10409 * @since 1.2.2
10410 *
10411 * @param string $option_name
10412 * @param mixed $option_value
10413 * @param bool $store
10414 * @param null|int $network_level_or_blog_id Since 2.0.0
10415 */
10416 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10417 self::set_account_option_by_module(
10418 $this->_module_type,
10419 $option_name,
10420 $option_value,
10421 $store,
10422 $network_level_or_blog_id
10423 );
10424 }
10425
10426 /**
10427 * @author Vova Feldman (@svovaf)
10428 *
10429 * @since 1.2.2.7
10430 *
10431 * @param string $module_type
10432 * @param string $option_name
10433 * @param mixed $option_value
10434 * @param bool $store
10435 * @param null|int $network_level_or_blog_id Since 2.0.0
10436 */
10437 private static function set_account_option_by_module(
10438 $module_type,
10439 $option_name,
10440 $option_value,
10441 $store,
10442 $network_level_or_blog_id = null
10443 ) {
10444 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10445 $option_name = $module_type . '_' . $option_name;
10446 }
10447
10448 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10449 }
10450
10451 /**
10452 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10453 *
10454 * @author Leo Fajardo (@leorw)
10455 * @since 2.3.1
10456 *
10457 * @param string $option_name
10458 * @param mixed $default
10459 * @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).
10460 *
10461 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10462 */
10463 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10464 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10465
10466 $class_name = '';
10467
10468 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10469 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10470 }
10471
10472 switch ( $option_name ) {
10473 case 'plugins':
10474 case 'themes':
10475 case 'addons':
10476 $class_name = FS_Plugin::get_class_name();
10477 break;
10478 case 'users':
10479 $class_name = FS_User::get_class_name();
10480 break;
10481 case 'sites':
10482 $class_name = FS_Site::get_class_name();
10483 break;
10484 case 'licenses':
10485 case 'all_licenses':
10486 $class_name = FS_Plugin_License::get_class_name();
10487 break;
10488 case 'plans':
10489 $class_name = FS_Plugin_Plan::get_class_name();
10490 break;
10491 case 'updates':
10492 $class_name = FS_Plugin_Tag::get_class_name();
10493 break;
10494 }
10495
10496 if ( empty( $class_name ) ) {
10497 return $option;
10498 }
10499
10500 return fs_get_entities( $option, $class_name );
10501 }
10502
10503 /**
10504 * @author Vova Feldman (@svovaf)
10505 * @since 1.0.6
10506 *
10507 * @param number|null $module_id
10508 *
10509 * @return FS_Plugin_License[]
10510 */
10511 private static function get_all_licenses( $module_id = null ) {
10512 $licenses = self::get_account_option( 'all_licenses' );
10513
10514 if ( ! is_array( $licenses ) ) {
10515 $licenses = array();
10516 }
10517
10518 if ( is_null( $module_id ) ) {
10519 return $licenses;
10520 }
10521
10522 $licenses = isset( $licenses[ $module_id ] ) ?
10523 $licenses[ $module_id ] :
10524 array();
10525
10526 return $licenses;
10527 }
10528
10529 /**
10530 * @author Leo Fajardo (@leorw)
10531 * @since 2.0.0
10532 *
10533 * @param number $module_id
10534 * @param number|null $user_id
10535 *
10536 * @return array
10537 */
10538 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10539 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10540
10541 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10542 $all_modules_user_id_license_ids_map = array();
10543 }
10544
10545 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10546 $all_modules_user_id_license_ids_map[ $module_id ] :
10547 array();
10548
10549 if ( FS_User::is_valid_id( $user_id ) ) {
10550 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10551 $user_id_license_ids_map[ $user_id ] :
10552 array();
10553 }
10554
10555 return $user_id_license_ids_map;
10556 }
10557
10558 /**
10559 * @author Leo Fajardo (@leorw)
10560 * @since 2.0.0
10561 *
10562 * @param array $new_user_id_license_ids_map
10563 * @param number $module_id
10564 * @param number|null $user_id
10565 */
10566 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10567 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10568 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10569 $all_modules_user_id_license_ids_map = array();
10570 }
10571
10572 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10573 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10574 }
10575
10576 if ( FS_User::is_valid_id( $user_id ) ) {
10577 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10578 } else {
10579 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10580 }
10581
10582 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10583 }
10584
10585 /**
10586 * Get a collection of the user's linked license IDs.
10587 *
10588 * @author Vova Feldman (@svovaf)
10589 * @since 2.0.0
10590 *
10591 * @param number $user_id
10592 *
10593 * @return number[]
10594 */
10595 private function get_user_linked_license_ids( $user_id ) {
10596 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10597 }
10598
10599 /**
10600 * Override the user's linked license IDs with a new IDs collection.
10601 *
10602 * @author Vova Feldman (@svovaf)
10603 * @since 2.0.0
10604 *
10605 * @param number $user_id
10606 * @param number[] $license_ids
10607 */
10608 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10609 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10610 }
10611
10612 /**
10613 * Link a specified license ID to a given user.
10614 *
10615 * @author Vova Feldman (@svovaf)
10616 * @since 2.0.0
10617 *
10618 * @param number $license_id
10619 * @param number $user_id
10620 */
10621 private function link_license_2_user( $license_id, $user_id ) {
10622 $license_ids = $this->get_user_linked_license_ids( $user_id );
10623
10624 if ( in_array( $license_id, $license_ids ) ) {
10625 // License already linked.
10626 return;
10627 }
10628
10629 $license_ids[] = $license_id;
10630
10631 $this->set_user_linked_license_ids( $user_id, $license_ids );
10632 }
10633
10634 /**
10635 * @param string|bool $module_type
10636 *
10637 * @return FS_Plugin_Plan[]
10638 */
10639 private static function get_all_plans( $module_type = false ) {
10640 $plans = self::get_account_option( 'plans', $module_type );
10641
10642 if ( ! is_array( $plans ) ) {
10643 $plans = array();
10644 }
10645
10646 return $plans;
10647 }
10648
10649 /**
10650 * @author Vova Feldman (@svovaf)
10651 * @since 1.0.4
10652 *
10653 * @return FS_Plugin_Tag[]
10654 */
10655 private static function get_all_updates() {
10656 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10657
10658 if ( ! is_array( $updates ) ) {
10659 $updates = array();
10660 }
10661
10662 return $updates;
10663 }
10664
10665 /**
10666 * @author Vova Feldman (@svovaf)
10667 * @since 1.0.6
10668 *
10669 * @return array<number,FS_Plugin[]>|false
10670 */
10671 public static function get_all_addons() {
10672 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10673
10674 if ( ! is_array( $addons ) ) {
10675 $addons = array();
10676 }
10677
10678 return $addons;
10679 }
10680
10681 /**
10682 * @author Vova Feldman (@svovaf)
10683 * @since 1.0.6
10684 *
10685 * @return number[]|false
10686 */
10687 public static function get_all_account_addons() {
10688 $addons = self::$_accounts->get_option( 'account_addons', array() );
10689
10690 if ( ! is_array( $addons ) ) {
10691 $addons = array();
10692 }
10693
10694 return $addons;
10695 }
10696
10697 /**
10698 * Check if user has connected his account (opted-in).
10699 *
10700 * Note:
10701 * If the user opted-in and opted-out on a later stage,
10702 * this will still return true. If you want to check if the
10703 * user is currently opted-in, use:
10704 * `$fs->is_registered() && $fs->is_tracking_allowed()`
10705 *
10706 * @author Vova Feldman (@svovaf)
10707 * @since 1.0.1
10708 *
10709 * @param bool $ignore_anonymous_state Since 2.5.1
10710 *
10711 * @return bool
10712 */
10713 function is_registered( $ignore_anonymous_state = false ) {
10714 return (
10715 is_object( $this->_user ) &&
10716 (
10717 $this->is_premium() ||
10718 $ignore_anonymous_state ||
10719 ! $this->is_anonymous()
10720 )
10721 );
10722 }
10723
10724 /**
10725 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
10726 *
10727 * @author Leo Fajardo (@leorw)
10728 * @since 1.2.1.5
10729 *
10730 * @return bool
10731 */
10732 function is_tracking_allowed( $blog_id = null, $install = null ) {
10733 if ( is_null( $install ) ) {
10734 $install = is_null( $blog_id ) ?
10735 $this->_site :
10736 $this->get_install_by_blog_id( $blog_id );
10737 }
10738
10739 return (
10740 is_object( $install ) &&
10741 FS_Permission_Manager::instance( $this )->is_homepage_url_tracking_allowed( $blog_id )
10742 );
10743 }
10744
10745 /**
10746 * Returns TRUE if the user never opted-in or manually opted-out.
10747 *
10748 * @author Vova Feldman (@svovaf)
10749 * @since 1.2.1.5
10750 *
10751 * @param int|null $blog_id
10752 *
10753 * @return bool
10754 */
10755 function is_tracking_prohibited( $blog_id = null ) {
10756 return (
10757 ! $this->is_registered( true ) ||
10758 ! $this->is_tracking_allowed( $blog_id )
10759 );
10760 }
10761
10762 /**
10763 * @author Leo Fajardo (@leorw)
10764 * @since 2.4.0
10765 *
10766 * @return bool
10767 */
10768 function is_bundle_license_auto_activation_enabled() {
10769 return $this->is_addon() ?
10770 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
10771 $this->_is_bundle_license_auto_activation_enabled;
10772 }
10773
10774 /**
10775 * @author Vova Feldman (@svovaf)
10776 * @since 1.0.4
10777 *
10778 * @return FS_Plugin
10779 */
10780 function get_plugin() {
10781 return $this->_plugin;
10782 }
10783
10784 /**
10785 * @author Vova Feldman (@svovaf)
10786 * @since 1.0.3
10787 *
10788 * @return FS_User
10789 */
10790 function get_user() {
10791 return $this->_user;
10792 }
10793
10794 /**
10795 * @author Vova Feldman (@svovaf)
10796 * @since 1.0.3
10797 *
10798 * @return FS_Site
10799 */
10800 function get_site() {
10801 return $this->_site;
10802 }
10803
10804 /**
10805 * @author Daniele Alessandra (@danielealessandra)
10806 * @return FS_Storage
10807 * @since 2.6.2
10808 *
10809 */
10810 public function get_storage() {
10811 return $this->_storage;
10812 }
10813
10814 /**
10815 * @author Leo Fajardo (@leorw)
10816 * @since 2.5.0
10817 */
10818 function store_site( $site ) {
10819 $this->_site = $site;
10820 $this->_store_site( true );
10821 }
10822
10823 /**
10824 * 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).
10825 *
10826 * @author Leo Fajardo (@leorw)
10827 * @since 2.5.0
10828 */
10829 function delete_current_install( $back_up ) {
10830 // Back up and delete the unique ID.
10831 if ( $back_up ) {
10832 self::$_accounts->set_option( 'prev_unique_id', $this->get_anonymous_id() );
10833 }
10834
10835 self::$_accounts->set_option( 'unique_id', null );
10836
10837 if ( $back_up ) {
10838 // 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).
10839 $this->back_up_site();
10840 }
10841
10842 $this->_delete_site();
10843 $this->_site = null;
10844 }
10845
10846 /**
10847 * @author Leo Fajardo (@leorw)
10848 * @since 2.5.0
10849 */
10850 function restore_backup_site() {
10851 self::$_accounts->set_option(
10852 'unique_id',
10853 self::$_accounts->get_option( 'prev_unique_id' )
10854 );
10855
10856 $sites = self::get_all_sites( $this->_module_type, null, true );
10857 $this->store_site( clone $sites[ $this->_slug ] );
10858 }
10859
10860 /**
10861 * Get plugin add-ons.
10862 *
10863 * @author Vova Feldman (@svovaf)
10864 * @since 1.0.6
10865 *
10866 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
10867 *
10868 * @param bool $flush
10869 *
10870 * @return FS_Plugin[]|false
10871 */
10872 function get_addons( $flush = false ) {
10873 $this->_logger->entrance();
10874
10875 if ( ! $this->_has_addons ) {
10876 return false;
10877 }
10878
10879 $addons = $this->sync_addons( $flush );
10880
10881 return ( ! is_array( $addons ) || empty( $addons ) ) ?
10882 false :
10883 $addons;
10884 }
10885
10886 /**
10887 * @author Vova Feldman (@svovaf)
10888 * @since 1.0.6
10889 *
10890 * @return number[]|false
10891 */
10892 function get_account_addons() {
10893 $this->_logger->entrance();
10894
10895 $addons = self::get_all_account_addons();
10896
10897 if ( ! is_array( $addons ) ||
10898 ! isset( $addons[ $this->_plugin->id ] ) ||
10899 ! is_array( $addons[ $this->_plugin->id ] ) ||
10900 0 === count( $addons[ $this->_plugin->id ] )
10901 ) {
10902 return false;
10903 }
10904
10905 return $addons[ $this->_plugin->id ];
10906 }
10907
10908 /**
10909 * Check if user has any
10910 *
10911 * @author Vova Feldman (@svovaf)
10912 * @since 1.1.6
10913 *
10914 * @return bool
10915 */
10916 function has_account_addons() {
10917 $addons = $this->get_account_addons();
10918
10919 return is_array( $addons ) && ( 0 < count( $addons ) );
10920 }
10921
10922
10923 /**
10924 * Get add-on by ID (from local data).
10925 *
10926 * @author Vova Feldman (@svovaf)
10927 * @since 1.0.6
10928 *
10929 * @param number $id
10930 *
10931 * @return FS_Plugin|false
10932 */
10933 function get_addon( $id ) {
10934 $this->_logger->entrance();
10935
10936 $addons = $this->get_addons();
10937
10938 if ( is_array( $addons ) ) {
10939 foreach ( $addons as $addon ) {
10940 if ( $id == $addon->id ) {
10941 return $addon;
10942 }
10943 }
10944 }
10945
10946 return false;
10947 }
10948
10949 /**
10950 * Get add-on by slug (from local data).
10951 *
10952 * @author Vova Feldman (@svovaf)
10953 * @since 1.0.6
10954 *
10955 * @param string $slug
10956 *
10957 * @param bool $flush
10958 *
10959 * @return FS_Plugin|false
10960 */
10961 function get_addon_by_slug( $slug, $flush = false ) {
10962 $this->_logger->entrance();
10963
10964 $addons = $this->get_addons( $flush );
10965
10966 if ( is_array( $addons ) ) {
10967 foreach ( $addons as $addon ) {
10968 if ( $slug === $addon->slug ) {
10969 return $addon;
10970 }
10971 }
10972 }
10973
10974 return false;
10975 }
10976
10977 /**
10978 * @var array<number,object[]> {
10979 * @key number Add-on ID.
10980 * @val object[] The add-on's plans and prices object.
10981 * }
10982 */
10983 private $plans_and_pricing_by_addon_id;
10984
10985 /**
10986 * @author Leo Fajardo (@leorw)
10987 * @since 2.3.0
10988 *
10989 * @return array<number,object[]> {
10990 * @key number Add-on ID.
10991 * @val object[] The add-on's plans and prices object.
10992 * }
10993 */
10994 function _get_addons_plans_and_pricing_map_by_id() {
10995 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
10996 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
10997
10998 $plans_and_pricing_by_addon_id = array();
10999 if ( $this->is_api_result_object( $result, 'addons' ) ) {
11000 foreach ( $result->addons as $addon ) {
11001 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
11002 }
11003 }
11004
11005 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
11006 }
11007
11008 return $this->plans_and_pricing_by_addon_id;
11009 }
11010
11011 /**
11012 * @author Leo Fajardo (@leorw)
11013 * @since 2.3.0
11014 *
11015 * @param number $addon_id
11016 * @param bool $is_installed
11017 *
11018 * @return array
11019 */
11020 function _get_addon_info( $addon_id, $is_installed ) {
11021 $addon = $this->get_addon( $addon_id );
11022
11023 if ( ! is_object( $addon ) ) {
11024 // Unexpected call.
11025 return array();
11026 }
11027
11028 $slug = $addon->slug;
11029
11030 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
11031
11032 if ( ! fs_is_network_admin() ) {
11033 // Get blog-level activated installations.
11034 $sites = self::maybe_get_entities_account_option( 'sites', array() );
11035 } else {
11036 $sites = null;
11037
11038 if ( $this->is_addon_activated( $addon_id ) &&
11039 $this->get_addon_instance( $addon_id )->is_network_active()
11040 ) {
11041 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
11042 // Get network-level activated installations.
11043 $sites = self::maybe_get_entities_account_option(
11044 'sites',
11045 array(),
11046 $addon_storage->network_install_blog_id
11047 );
11048 }
11049 }
11050 }
11051
11052 $addon_info = array(
11053 'is_connected' => false,
11054 'slug' => $slug,
11055 'title' => $addon->title,
11056 'is_whitelabeled' => $addon_storage->is_whitelabeled
11057 );
11058
11059 if ( ! $is_installed ) {
11060 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
11061
11062 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
11063 $has_paid_plan = false;
11064 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
11065
11066 if ( is_array( $plans ) && count( $plans ) > 0 ) {
11067 foreach ( $plans as $plan ) {
11068 if ( isset( $plan->pricing ) &&
11069 is_array( $plan->pricing ) &&
11070 count( $plan->pricing ) > 0
11071 ) {
11072 $has_paid_plan = true;
11073 break;
11074 }
11075 }
11076 }
11077
11078 $addon_info['has_paid_plan'] = $has_paid_plan;
11079 }
11080 }
11081
11082 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
11083 return $addon_info;
11084 }
11085
11086 $site = $sites[ $slug ];
11087
11088 $addon_info['is_connected'] = (
11089 ( $addon->parent_plugin_id == $this->get_id() ) &&
11090 is_object( $site ) &&
11091 FS_Site::is_valid_id( $site->id ) &&
11092 FS_User::is_valid_id( $site->user_id ) &&
11093 FS_Plugin_Plan::is_valid_id( $site->plan_id )
11094 );
11095
11096 if ( $addon_info['is_connected'] && $is_installed ) {
11097 return $addon_info;
11098 }
11099
11100 $addon_info['site'] = $site;
11101
11102 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
11103 if ( isset( $plugins_data[ $slug ] ) ) {
11104 $plugin_data = $plugins_data[ $slug ];
11105
11106 $addon_info['version'] = $plugin_data->version;
11107 }
11108
11109 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
11110 if ( isset( $all_plans[ $slug ] ) ) {
11111 $plans = $all_plans[ $slug ];
11112
11113 foreach ( $plans as $plan ) {
11114 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
11115 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
11116 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
11117 break;
11118 }
11119 }
11120 }
11121
11122 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
11123 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
11124 foreach ( $licenses[ $addon_id ] as $license ) {
11125 if ( $license->id == $site->license_id ) {
11126 $addon_info['license'] = $license;
11127 break;
11128 }
11129 }
11130 }
11131
11132 if ( isset( $addon_info['license'] ) ) {
11133 if ( isset( $addon_storage->subscriptions ) &&
11134 ! empty( $addon_storage->subscriptions )
11135 ) {
11136 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11137
11138 foreach ( $addon_subscriptions as $subscription ) {
11139 if ( $subscription->license_id == $site->license_id ) {
11140 $addon_info['subscription'] = $subscription;
11141 break;
11142 }
11143 }
11144 }
11145 }
11146
11147 return $addon_info;
11148 }
11149
11150 /**
11151 * @author Vova Feldman (@svovaf)
11152 * @since 2.0.0
11153 *
11154 * @param number $user_id
11155 *
11156 * @return FS_User
11157 */
11158 static function _get_user_by_id( $user_id ) {
11159 self::$_static_logger->entrance( "user_id = {$user_id}" );
11160
11161 $users = self::get_all_users();
11162
11163 if ( is_array( $users ) ) {
11164 if ( isset( $users[ $user_id ] ) &&
11165 $users[ $user_id ] instanceof FS_User &&
11166 $user_id == $users[ $user_id ]->id
11167 ) {
11168 return $users[ $user_id ];
11169 }
11170
11171 // If user wasn't found by the key, iterate over all the users collection.
11172 foreach ( $users as $user ) {
11173 /**
11174 * @var FS_User $user
11175 */
11176 if ( $user_id == $user->id ) {
11177 return $user;
11178 }
11179 }
11180 }
11181
11182 return null;
11183 }
11184
11185 /**
11186 * Checks if a Freemius user_id is associated with a super-admin.
11187 *
11188 * @author Vova Feldman (@svovaf)
11189 * @since 2.0.0
11190 *
11191 * @param number $user_id
11192 *
11193 * @return bool
11194 */
11195 private static function is_super_admin( $user_id ) {
11196 $is_super_admin = false;
11197
11198 $user = self::_get_user_by_id( $user_id );
11199
11200 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11201 self::require_pluggable_essentials();
11202
11203 $wp_user = get_user_by( 'email', $user->email );
11204
11205 if ( $wp_user instanceof WP_User ) {
11206 $super_admins = get_super_admins();
11207 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11208 }
11209 }
11210
11211 return $is_super_admin;
11212 }
11213
11214 #----------------------------------------------------------------------------------
11215 #region Plans & Licensing
11216 #----------------------------------------------------------------------------------
11217
11218 /**
11219 * Check if running premium plugin code.
11220 *
11221 * @author Vova Feldman (@svovaf)
11222 * @since 1.0.5
11223 *
11224 * @return bool
11225 */
11226 function is_premium() {
11227 /**
11228 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11229 * `register_constructor_hooks` method.
11230 *
11231 * @author Leo Fajardo (@leorw)
11232 * @since 2.2.3
11233 */
11234 return is_object( $this->_plugin ) ?
11235 $this->_plugin->is_premium :
11236 false;
11237 }
11238
11239 /**
11240 * Get site's plan ID.
11241 *
11242 * @author Vova Feldman (@svovaf)
11243 * @since 1.0.2
11244 *
11245 * @return number
11246 */
11247 function get_plan_id() {
11248 return $this->_site->plan_id;
11249 }
11250
11251 /**
11252 * Get site's plan title.
11253 *
11254 * @author Vova Feldman (@svovaf)
11255 * @since 1.0.2
11256 *
11257 * @return string
11258 */
11259 function get_plan_title() {
11260 $plan = $this->get_plan();
11261
11262 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11263 }
11264
11265 /**
11266 * Get site's plan name.
11267 *
11268 * @author Vova Feldman (@svovaf)
11269 * @since 2.0.0
11270 *
11271 * @return string
11272 */
11273 function get_plan_name() {
11274 $plan = $this->get_plan();
11275
11276 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11277 }
11278
11279 /**
11280 * @author Vova Feldman (@svovaf)
11281 * @since 1.0.9
11282 *
11283 * @return FS_Plugin_Plan|false
11284 */
11285 function get_plan() {
11286 if ( ! is_object( $this->_site ) ) {
11287 return false;
11288 }
11289
11290 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11291 $this->_get_plan_by_id( $this->_site->plan_id ) :
11292 false;
11293 }
11294
11295 /**
11296 * @author Vova Feldman (@svovaf)
11297 * @since 1.0.3
11298 *
11299 * @return bool
11300 */
11301 function is_trial() {
11302 $this->_logger->entrance();
11303
11304 if ( ! $this->is_registered( true ) || ! is_object( $this->_site ) ) {
11305 return false;
11306 }
11307
11308 return $this->_site->is_trial();
11309 }
11310
11311 /**
11312 * Check if currently in a trial with payment method (credit card or paypal).
11313 *
11314 * @author Vova Feldman (@svovaf)
11315 * @since 1.1.7
11316 *
11317 * @return bool
11318 */
11319 function is_paid_trial() {
11320 $this->_logger->entrance();
11321
11322 if ( ! $this->is_trial() ) {
11323 return false;
11324 }
11325
11326 if ( ! $this->has_active_valid_license() ) {
11327 return false;
11328 }
11329
11330 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11331 return false;
11332 }
11333
11334 /**
11335 * @var FS_Subscription $subscription
11336 */
11337 $subscription = $this->_get_subscription( $this->_license->id );
11338
11339 return ( is_object( $subscription ) && $subscription->is_active() );
11340 }
11341
11342 /**
11343 * Check if trial already utilized.
11344 *
11345 * @since 1.0.9
11346 *
11347 * @return bool
11348 */
11349 function is_trial_utilized() {
11350 $this->_logger->entrance();
11351
11352 if ( ! $this->is_registered() ) {
11353 return false;
11354 }
11355
11356 return $this->_site->is_trial_utilized();
11357 }
11358
11359 /**
11360 * Get trial plan information (if in trial).
11361 *
11362 * @author Vova Feldman (@svovaf)
11363 * @since 1.0.9
11364 *
11365 * @return bool|FS_Plugin_Plan
11366 */
11367 function get_trial_plan() {
11368 $this->_logger->entrance();
11369
11370 if ( ! $this->is_trial() ) {
11371 return false;
11372 }
11373
11374 // Try to load plan from local cache.
11375 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11376
11377 if ( ! is_object( $trial_plan ) ) {
11378 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11379
11380 /**
11381 * If managed to fetch the plan, add it to the plans collection.
11382 */
11383 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11384 if ( ! is_array( $this->_plans ) ) {
11385 $this->_plans = array();
11386 }
11387
11388 $this->_plans[] = $trial_plan;
11389 $this->_store_plans();
11390 }
11391 }
11392
11393 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11394 return $trial_plan;
11395 }
11396
11397 /**
11398 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11399 */
11400 $trial_plan = new FS_Plugin_Plan();
11401 $trial_plan->id = $this->_site->trial_plan_id;
11402 $trial_plan->name = 'pro';
11403 $trial_plan->title = 'Pro';
11404
11405 return $trial_plan;
11406 }
11407
11408 /**
11409 * Check if the user has an activate, non-expired license on current plugin's install.
11410 *
11411 * @since 1.0.9
11412 *
11413 * @return bool
11414 */
11415 function is_paying() {
11416 $this->_logger->entrance();
11417
11418 if ( ! $this->is_registered( true ) ) {
11419 return false;
11420 }
11421
11422 if ( ! $this->has_paid_plan() ) {
11423 return false;
11424 }
11425
11426 return (
11427 ! $this->is_trial() &&
11428 'free' !== $this->get_plan_name() &&
11429 $this->has_active_valid_license()
11430 );
11431 }
11432
11433 /**
11434 * @author Vova Feldman (@svovaf)
11435 * @since 1.0.4
11436 *
11437 * @return bool
11438 */
11439 function is_free_plan() {
11440 if ( ! $this->is_registered() ) {
11441 return true;
11442 }
11443
11444 if ( ! $this->has_paid_plan() ) {
11445 return true;
11446 }
11447
11448 return (
11449 'free' === $this->get_plan_name() ||
11450 ! $this->has_features_enabled_license()
11451 );
11452 }
11453
11454 /**
11455 * @author Vova Feldman (@svovaf)
11456 * @since 1.0.5
11457 *
11458 * @return bool
11459 */
11460 function _has_premium_license() {
11461 $this->_logger->entrance();
11462
11463 $premium_license = $this->_get_available_premium_license();
11464
11465 return ( false !== $premium_license );
11466 }
11467
11468 /**
11469 * Check if user has any licenses associated with the plugin (including expired or blocking).
11470 *
11471 * @author Vova Feldman (@svovaf)
11472 * @since 1.1.7.3
11473 *
11474 * @param bool $including_foreign
11475 *
11476 * @return bool
11477 */
11478 function has_any_license( $including_foreign = true ) {
11479 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11480 return false;
11481 }
11482
11483 if ( $including_foreign ) {
11484 return true;
11485 }
11486
11487 foreach ( $this->_licenses as $license ) {
11488 if ( $this->_user->id == $license->user_id ) {
11489 return true;
11490 }
11491 }
11492
11493 return false;
11494 }
11495
11496 /**
11497 * @author Vova Feldman (@svovaf)
11498 * @since 1.0.5
11499 *
11500 * @param bool|null $is_localhost
11501 *
11502 * @return FS_Plugin_License|false
11503 */
11504 function _get_available_premium_license( $is_localhost = null ) {
11505 $this->_logger->entrance();
11506
11507 $licenses = $this->get_available_premium_licenses( $is_localhost );
11508 if ( ! empty( $licenses ) ) {
11509 return $licenses[0];
11510 }
11511
11512 return false;
11513 }
11514
11515 /**
11516 * @author Vova Feldman (@svovaf)
11517 * @since 1.0.5
11518 *
11519 * @param bool|null $is_localhost
11520 *
11521 * @return FS_Plugin_License[]
11522 */
11523 function get_available_premium_licenses( $is_localhost = null ) {
11524 $this->_logger->entrance();
11525
11526 $licenses = array();
11527 if ( ! $this->has_paid_plan() ) {
11528 return $licenses;
11529 }
11530
11531 if ( is_array( $this->_licenses ) ) {
11532 foreach ( $this->_licenses as $license ) {
11533 if ( ! $license->can_activate( $is_localhost ) ) {
11534 continue;
11535 }
11536
11537 $licenses[] = $license;
11538 }
11539 }
11540
11541 return $licenses;
11542 }
11543
11544 /**
11545 * Sync local plugin plans with remote server.
11546 *
11547 * 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).
11548 *
11549 * @author Vova Feldman (@svovaf)
11550 * @since 1.0.5
11551 *
11552 * @return FS_Plugin_Plan[]|object
11553 */
11554 function _sync_plans() {
11555 $plans = $this->_fetch_plugin_plans();
11556
11557 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11558 $plans_map = array();
11559 foreach ( $plans as $plan ) {
11560 $plans_map[ $plan->id ] = true;
11561 }
11562
11563 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11564
11565 foreach ( $plans_ids_to_keep as $plan_id ) {
11566 if ( isset( $plans_map[ $plan_id ] ) ) {
11567 continue;
11568 }
11569
11570 $missing_plan = self::_get_plan_by_id( $plan_id, false );
11571
11572 if ( is_object( $missing_plan ) ) {
11573 $plans[] = $missing_plan;
11574 }
11575 }
11576
11577 $this->_plans = $plans;
11578 $this->_store_plans();
11579 }
11580
11581 $this->do_action( 'after_plans_sync', $plans );
11582
11583 return $this->_plans;
11584 }
11585
11586 /**
11587 * Check if specified plan exists locally. If not, fetch it and store it.
11588 *
11589 * @author Vova Feldman (@svovaf)
11590 * @since 2.0.0
11591 *
11592 * @param number $plan_id
11593 *
11594 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11595 */
11596 private function sync_plan_if_not_exist( $plan_id ) {
11597 $plan = self::_get_plan_by_id( $plan_id );
11598
11599 if ( is_object( $plan ) ) {
11600 // Plan already exists.
11601 return $plan;
11602 }
11603
11604 $plan = $this->fetch_plan_by_id( $plan_id );
11605
11606 if ( $plan instanceof FS_Plugin_Plan ) {
11607 $this->_plans[] = $plan;
11608 $this->_store_plans();
11609
11610 return $plan;
11611 }
11612
11613 return $plan;
11614 }
11615
11616 /**
11617 * Check if specified license exists locally. If not, fetch it and store it.
11618 *
11619 * @author Vova Feldman (@svovaf)
11620 * @since 2.0.0
11621 *
11622 * @param number $license_id
11623 * @param string $license_key
11624 *
11625 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11626 */
11627 private function sync_license_if_not_exist( $license_id, $license_key ) {
11628 $license = $this->_get_license_by_id( $license_id );
11629
11630 if ( is_object( $license ) ) {
11631 // License already exists.
11632 return $license;
11633 }
11634
11635 $license = $this->fetch_license_by_key( $license_id, $license_key );
11636
11637 if ( $license instanceof FS_Plugin_License ) {
11638 $this->_licenses[] = $license;
11639
11640 $this->set_license( $license );
11641
11642 $this->_store_licenses();
11643
11644 return $license;
11645 }
11646
11647 return $license;
11648 }
11649
11650 /**
11651 * Get a collection of unique plan IDs that are associated with any installs in the network.
11652 *
11653 * @author Leo Fajardo (@leorw)
11654 * @since 2.0.0
11655 *
11656 * @return number[]
11657 */
11658 private function get_plans_ids_associated_with_installs() {
11659 if ( ! is_multisite() ) {
11660 if ( ! is_object( $this->_site ) ||
11661 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11662 ) {
11663 return array();
11664 }
11665
11666 return array( $this->_site->plan_id );
11667 }
11668
11669 $plan_ids = array();
11670 $sites = self::get_sites();
11671 foreach ( $sites as $site ) {
11672 $blog_id = self::get_site_blog_id( $site );
11673 $install = $this->get_install_by_blog_id( $blog_id );
11674
11675 if ( ! is_object( $install ) ||
11676 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11677 ) {
11678 continue;
11679 }
11680
11681 $plan_ids[ $install->plan_id ] = true;
11682 }
11683
11684 return array_keys( $plan_ids );
11685 }
11686
11687 /**
11688 * Get a collection of unique license IDs that are associated with any installs in the network.
11689 *
11690 * @author Leo Fajardo (@leorw)
11691 * @since 2.0.0
11692 *
11693 * @return number[]
11694 */
11695 private function get_license_ids_associated_with_installs() {
11696 if ( ! $this->_is_network_active ) {
11697 if ( ! is_object( $this->_site ) ||
11698 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11699 ) {
11700 return array();
11701 }
11702
11703 return array( $this->_site->license_id );
11704 }
11705
11706 $license_ids = array();
11707 $sites = self::get_sites();
11708 foreach ( $sites as $site ) {
11709 $blog_id = self::get_site_blog_id( $site );
11710 $install = $this->get_install_by_blog_id( $blog_id );
11711
11712 if ( ! is_object( $install ) ||
11713 ! FS_Plugin_License::is_valid_id( $install->license_id )
11714 ) {
11715 continue;
11716 }
11717
11718 $license_ids[ $install->license_id ] = true;
11719 }
11720
11721 return array_keys( $license_ids );
11722 }
11723
11724 /**
11725 * @author Vova Feldman (@svovaf)
11726 * @since 1.0.5
11727 *
11728 * @param number $id
11729 *
11730 * @return FS_Plugin_Plan|false
11731 */
11732 function _get_plan_by_id( $id, $allow_sync = true ) {
11733 $this->_logger->entrance();
11734
11735 if ( $allow_sync && ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) ) {
11736 $this->_sync_plans();
11737 }
11738
11739 foreach ( $this->_plans as $plan ) {
11740 if ( $id == $plan->id ) {
11741 return $plan;
11742 }
11743 }
11744
11745 return false;
11746 }
11747
11748 /**
11749 * @author Vova Feldman (@svovaf)
11750 * @since 1.1.8.1
11751 *
11752 * @param string $name
11753 *
11754 * @return FS_Plugin_Plan|false
11755 */
11756 private function get_plan_by_name( $name ) {
11757 $this->_logger->entrance();
11758
11759 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11760 $this->_sync_plans();
11761 }
11762
11763 foreach ( $this->_plans as $plan ) {
11764 if ( $name == $plan->name ) {
11765 return $plan;
11766 }
11767 }
11768
11769 return false;
11770 }
11771
11772 /**
11773 * Sync local licenses with remote server.
11774 *
11775 * @author Vova Feldman (@svovaf)
11776 * @since 1.0.6
11777 *
11778 * @param number|bool $site_license_id
11779 * @param number|null $blog_id
11780 *
11781 * @return FS_Plugin_License[]|object
11782 */
11783 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
11784 $this->_logger->entrance();
11785
11786 $is_network_admin = fs_is_network_admin();
11787
11788 if ( $is_network_admin && is_null( $blog_id ) ) {
11789 $all_licenses = self::get_all_licenses( $this->_module_id );
11790 } else {
11791 $all_licenses = $this->get_user_licenses( $this->_user->id );
11792 }
11793
11794 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
11795
11796 $all_licenses_map = array();
11797 foreach ( $all_licenses as $license ) {
11798 $all_licenses_map[ $license->id ] = true;
11799 }
11800
11801 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
11802
11803 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
11804 $licenses_map = array();
11805 foreach ( $licenses as $license ) {
11806 $licenses_map[ $license->id ] = true;
11807 }
11808
11809 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
11810 // foreach ( $license_ids_to_keep as $license_id ) {
11811 // if ( isset( $licenses_map[ $license_id ] ) ) {
11812 // continue;
11813 // }
11814 //
11815 // $missing_license = self::_get_license_by_id( $license_id, false );
11816 // if ( is_object( $missing_license ) ) {
11817 // $licenses[] = $missing_license;
11818 // $licenses_map[ $missing_license->id ] = true;
11819 // }
11820 // }
11821
11822 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
11823
11824 foreach ( $user_license_ids as $key => $license_id ) {
11825 if ( ! isset( $licenses_map[ $license_id ] ) ) {
11826 // Remove access to licenses that no longer exist.
11827 unset( $user_license_ids[ $key ] );
11828 }
11829 }
11830
11831 if ( ! empty( $user_license_ids ) ) {
11832 foreach ( $licenses_map as $license_id => $value ) {
11833 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
11834 // Associate new licenses with the user who triggered the license syncing.
11835 $user_license_ids[] = $license_id;
11836 }
11837 }
11838
11839 $user_license_ids = array_unique( $user_license_ids );
11840 } else {
11841 $user_license_ids = array_keys( $licenses_map );
11842 }
11843
11844 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
11845 $user_licenses = array();
11846 foreach ( $licenses as $license ) {
11847 if ( ! in_array( $license->id, $user_license_ids ) ) {
11848 continue;
11849 }
11850
11851 $user_licenses[] = $license;
11852 }
11853
11854 $this->_licenses = $user_licenses;
11855 } else {
11856 $this->_licenses = $licenses;
11857 }
11858
11859 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
11860
11861 $this->_store_licenses( true, $this->_module_id, $licenses );
11862 }
11863
11864 // Update current license.
11865 if ( is_object( $this->_license ) ) {
11866 $license = $this->_get_license_by_id( $this->_license->id );
11867
11868 if ( is_object( $license ) ) {
11869 /**
11870 * `$license` can be `false` in case a user change action has just been completed and this method
11871 * has synced the `$this->_licenses` collection for the new user. In this case, the
11872 * `$this->_licenses` collection may have only the newly activated license that is associated with
11873 * the new user. `set_license` will eventually be called in the same request by the logic that
11874 * follows outside this method which will detect that the install's license has been updated, and
11875 * then `_update_site_license` will be called which in turn will call `set_license`.
11876 *
11877 * @author Leo Fajardo (@leorw)
11878 * @since 2.3.2
11879 */
11880 $this->set_license( $license );
11881 }
11882 }
11883
11884 return $this->_licenses;
11885 }
11886
11887 /**
11888 * @author Vova Feldman (@svovaf)
11889 * @since 1.0.5
11890 *
11891 * @param number $id
11892 * @param bool $sync_licenses
11893 *
11894 * @return FS_Plugin_License|false
11895 */
11896 function _get_license_by_id( $id, $sync_licenses = true ) {
11897 $this->_logger->entrance();
11898
11899 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
11900 return false;
11901 }
11902
11903 /**
11904 * When running from the network level admin and opted-in from the network,
11905 * check if the license exists in the network user licenses collection.
11906 *
11907 * @author Vova Feldman (@svovaf)
11908 * @since 2.0.0
11909 */
11910 if ( fs_is_network_admin() &&
11911 $this->is_network_registered() &&
11912 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
11913 ) {
11914 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
11915
11916 foreach ( $licenses as $license ) {
11917 if ( $id == $license->id ) {
11918 return $license;
11919 }
11920 }
11921 }
11922
11923 if ( ! $this->has_any_license() && $sync_licenses ) {
11924 $this->_sync_licenses( $id );
11925 }
11926
11927 if ( is_array( $this->_licenses ) ) {
11928 foreach ( $this->_licenses as $license ) {
11929 if ( $id == $license->id ) {
11930 return $license;
11931 }
11932 }
11933 }
11934
11935 return false;
11936 }
11937
11938 /**
11939 * 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.
11940 *
11941 * @author Vova Feldman (@svovaf)
11942 * @since 2.0.0
11943 *
11944 * @param number $id
11945 *
11946 * @return FS_Plugin_License
11947 */
11948 private function get_license_by_id( $id ) {
11949 $licenses = self::get_all_licenses( $this->_module_id );
11950
11951 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
11952 foreach ( $licenses as $license ) {
11953 if ( $id == $license->id ) {
11954 return $license;
11955 }
11956 }
11957 }
11958
11959 return null;
11960 }
11961
11962 /**
11963 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
11964 *
11965 * @author Vova Feldman (@svovaf)
11966 * @since 2.0.0
11967 *
11968 * @return \FS_Plugin_License|mixed
11969 */
11970 private function sync_site_license() {
11971 $api = $this->get_api_user_scope();
11972
11973 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
11974
11975 if ( ! $this->is_api_result_entity( $result ) ) {
11976 return $result;
11977 }
11978
11979 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
11980 $this->_store_licenses();
11981
11982 return $license;
11983 }
11984
11985 /**
11986 * Get all user's available licenses for the current module.
11987 *
11988 * @author Vova Feldman (@svovaf)
11989 * @since 2.0.0
11990 *
11991 * @param number $user_id
11992 *
11993 * @return FS_Plugin_License[]
11994 */
11995 private function get_user_licenses( $user_id ) {
11996 $all_licenses = self::get_all_licenses( $this->_module_id );
11997 if ( empty( $all_licenses ) ) {
11998 return array();
11999 }
12000
12001 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
12002 if ( empty( $user_license_ids ) ) {
12003 return array();
12004 }
12005
12006 $licenses = array();
12007 foreach ( $all_licenses as $license ) {
12008 if ( in_array( $license->id, $user_license_ids ) ) {
12009 $licenses[] = $license;
12010 }
12011 }
12012
12013 return $licenses;
12014 }
12015
12016 /**
12017 * Checks if the context license is network activated except on the given blog ID.
12018 *
12019 * @author Vova Feldman (@svovaf)
12020 * @since 2.0.0
12021 *
12022 * @param int $except_blog_id
12023 *
12024 * @return bool
12025 */
12026 private function is_license_network_active( $except_blog_id = 0 ) {
12027 $this->_logger->entrance();
12028
12029 if ( ! is_object( $this->_license ) ) {
12030 return false;
12031 }
12032
12033 $sites = self::get_sites();
12034
12035 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
12036 // There are more sites than the number of activations, so license cannot be network activated.
12037 return false;
12038 }
12039
12040 foreach ( $sites as $site ) {
12041 $blog_id = self::get_site_blog_id( $site );
12042
12043 if ( $except_blog_id == $blog_id ) {
12044 // Skip excluded blog.
12045 continue;
12046 }
12047
12048 $install = $this->get_install_by_blog_id( $blog_id );
12049
12050 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
12051 return false;
12052 }
12053 }
12054
12055 return true;
12056 }
12057
12058 /**
12059 * 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.
12060 *
12061 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
12062 *
12063 * @author Vova Feldman (@svovaf)
12064 * @since 2.0.0
12065 *
12066 * @param \FS_User $user
12067 * @param \FS_Plugin_License $license
12068 *
12069 * @return bool
12070 */
12071 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
12072 $this->_logger->entrance();
12073
12074 $result = $this->can_activate_license_on_network( $license );
12075
12076 if ( false === $result ) {
12077 return false;
12078 }
12079
12080 $installs_without_license = $result['installs'];
12081 if ( ! empty( $installs_without_license ) ) {
12082 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
12083 }
12084
12085 $disconnected_site_ids = $result['sites'];
12086 if ( ! empty( $disconnected_site_ids ) ) {
12087 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
12088 }
12089
12090 $this->link_license_2_user( $license->id, $user->id );
12091
12092 // Sync license after activations.
12093 $license->activated += $result['production_count'];
12094 $license->activated_local += $result['localhost_count'];
12095
12096 // $this->_store_licenses()
12097
12098 return true;
12099 }
12100
12101 /**
12102 * Checks if the given license can be activated on the whole network.
12103 *
12104 * @author Vova Feldman (@svovaf)
12105 * @since 2.0.0
12106 *
12107 * @param \FS_Plugin_License $license
12108 *
12109 * @return false|array {
12110 * @type array[int]FS_Site $installs Blog ID to install map.
12111 * @type int[] $sites Non-connected blog IDs.
12112 * @type int $production_count Production sites count.
12113 * @type int $localhost_count Production sites count.
12114 * }
12115 */
12116 private function can_activate_license_on_network( FS_Plugin_License $license ) {
12117 $sites = self::get_sites();
12118
12119 $production_count = 0;
12120 $localhost_count = 0;
12121
12122 $installs_without_license = array();
12123 $disconnected_site_ids = array();
12124
12125 foreach ( $sites as $site ) {
12126 $blog_id = self::get_site_blog_id( $site );
12127 $install = $this->get_install_by_blog_id( $blog_id );
12128
12129 if ( is_object( $install ) ) {
12130 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
12131 // License already activated on the install.
12132 continue;
12133 }
12134
12135 $url = $install->url;
12136
12137 $installs_without_license[ $blog_id ] = $install;
12138 } else {
12139 $url = is_object( $site ) ?
12140 $site->siteurl :
12141 self::get_unfiltered_site_url( $blog_id );
12142
12143 $disconnected_site_ids[] = $blog_id;
12144 }
12145
12146 if ( FS_Site::is_localhost_by_address( $url ) ) {
12147 $localhost_count ++;
12148 } else {
12149 $production_count ++;
12150 }
12151 }
12152
12153 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12154 return false;
12155 }
12156
12157 return array(
12158 'installs' => $installs_without_license,
12159 'sites' => $disconnected_site_ids,
12160 'production_count' => $production_count,
12161 'localhost_count' => $localhost_count,
12162 );
12163 }
12164
12165 /**
12166 * Activate a given license on a collection of installs.
12167 *
12168 * @author Vova Feldman (@svovaf)
12169 * @since 2.0.0
12170 *
12171 * @param \FS_User $user
12172 * @param string $license_key
12173 * @param array $blog_2_install_map {
12174 * @key int Blog ID.
12175 * @value FS_Site Blog's associated install.
12176 * }
12177 *
12178 * @return mixed|true
12179 */
12180 private function activate_license_on_many_installs(
12181 FS_User $user,
12182 $license_key,
12183 array $blog_2_install_map
12184 ) {
12185 $params = array(
12186 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12187 );
12188
12189 $install_2_blog_map = array();
12190 foreach ( $blog_2_install_map as $blog_id => $install ) {
12191 $params[] = array( 'id' => $install->id, 'url' => $install->url );
12192
12193 $install_2_blog_map[ $install->id ] = $blog_id;
12194 }
12195
12196 $result = $this->get_api_user_scope_by_user( $user )->call(
12197 "plugins/{$this->_plugin->id}/installs.json",
12198 'PUT',
12199 $params
12200 );
12201
12202 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12203 return $result;
12204 }
12205
12206 foreach ( $result->installs as $r_install ) {
12207 $install = new FS_Site( $r_install );
12208 $install->is_disconnected = false;
12209
12210 // Update install.
12211 $this->_store_site(
12212 true,
12213 $install_2_blog_map[ $r_install->id ],
12214 $install
12215 );
12216 }
12217
12218 return true;
12219 }
12220
12221 /**
12222 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12223 *
12224 * @author Vova Feldman (@svovaf)
12225 * @since 2.3.1
12226 *
12227 * @param \FS_User $user
12228 * @param string $license_key
12229 *
12230 * @return true|mixed True if successful, otherwise, the API result.
12231 */
12232 private function activate_license_on_site( FS_User $user, $license_key ) {
12233 return $this->activate_license_on_many_sites( $user, $license_key );
12234 }
12235
12236 /**
12237 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12238 *
12239 * @author Vova Feldman (@svovaf)
12240 * @since 2.0.0
12241 *
12242 * @param \FS_User $user
12243 * @param string $license_key
12244 * @param int[] $site_ids
12245 *
12246 * @return true|mixed True if successful, otherwise, the API result.
12247 */
12248 private function activate_license_on_many_sites(
12249 FS_User $user,
12250 $license_key,
12251 array $site_ids = array()
12252 ) {
12253 $sites = array();
12254 foreach ( $site_ids as $site_id ) {
12255 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12256 }
12257
12258 // Install the plugin.
12259 $result = $this->create_installs_with_user(
12260 $user,
12261 $license_key,
12262 false,
12263 $sites,
12264 false,
12265 true
12266 );
12267
12268 if ( ! $this->is_api_result_entity( $result ) &&
12269 ! $this->is_api_result_object( $result, 'installs' )
12270 ) {
12271 return $result;
12272 }
12273
12274 $installs = array();
12275
12276 if ( $this->is_api_result_entity( $result ) ) {
12277 $install = new FS_Site( $result );
12278
12279 $this->_user = $user;
12280
12281 $this->_store_site( true, null, $install );
12282
12283 $this->_site = $install;
12284
12285 $this->reset_anonymous_mode();
12286 } else {
12287 foreach ( $result->installs as $install ) {
12288 $installs[] = new FS_Site( $install );
12289 }
12290
12291 // Map site addresses to their blog IDs.
12292 $address_to_blog_map = $this->get_address_to_blog_map();
12293
12294 $first_blog_id = null;
12295
12296 foreach ( $installs as $install ) {
12297 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12298 $blog_id = $address_to_blog_map[ $address ];
12299
12300 $this->_store_site( true, $blog_id, $install );
12301
12302 $this->reset_anonymous_mode( $blog_id );
12303
12304 if ( is_null( $first_blog_id ) ) {
12305 $first_blog_id = $blog_id;
12306 }
12307 }
12308
12309 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12310 $this->_storage->network_install_blog_id = $first_blog_id;
12311 }
12312 }
12313
12314 return true;
12315 }
12316
12317 /**
12318 * Sync site's license with user licenses.
12319 *
12320 * @author Vova Feldman (@svovaf)
12321 * @since 1.0.6
12322 *
12323 * @param FS_Plugin_License|null $new_license
12324 *
12325 * @return FS_Plugin_License|null
12326 */
12327 function _update_site_license( $new_license ) {
12328 $this->_logger->entrance();
12329
12330 /**
12331 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12332 * accordingly so that it will also handle the case when an ownership change is done via license
12333 * activation.
12334 *
12335 * @author Leo Fajardo (@leorw)
12336 * @since 2.3.2
12337 */
12338 $this->set_license( $new_license );
12339
12340 if ( ! is_object( $new_license ) ) {
12341 $this->_site->license_id = null;
12342 $this->_sync_site_subscription( null );
12343
12344 return $this->_license;
12345 }
12346
12347 $this->_site->license_id = $this->_license->id;
12348
12349 if ( ! is_array( $this->_licenses ) ) {
12350 $this->_licenses = array();
12351 }
12352
12353 $is_license_found = false;
12354 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12355 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12356 $this->_licenses[ $i ] = $new_license;
12357
12358 $is_license_found = true;
12359 break;
12360 }
12361 }
12362
12363 // If new license just append.
12364 if ( ! $is_license_found ) {
12365 $this->_licenses[] = $new_license;
12366 }
12367
12368 $this->_sync_site_subscription( $new_license );
12369
12370 return $this->_license;
12371 }
12372
12373 /**
12374 * @author Vova Feldman (@svovaf)
12375 * @since 2.3.1
12376 *
12377 * @param \FS_Plugin_License $license
12378 */
12379 private function set_license( $license = null ) {
12380 $this->_license = $license;
12381
12382 $this->maybe_update_whitelabel_flag( $license );
12383 }
12384
12385 /**
12386 * @author Leo Fajardo (@leorw)
12387 * @since 2.3.1
12388 *
12389 * @param FS_Plugin_License $license
12390 */
12391 private function maybe_update_whitelabel_flag( $license ) {
12392 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12393 $this->_storage->is_whitelabeled :
12394 false;
12395
12396 if ( is_object( $license ) ) {
12397 $license_user = self::_get_user_by_id( $license->user_id );
12398
12399 if ( ! is_object( $license_user ) ) {
12400 // If foreign license, do not update the `is_whitelabeled` flag.
12401 return;
12402 }
12403
12404 if ( $this->is_addon() ) {
12405 /**
12406 * Store the last license data to the parent's storage since it's needed only when showing the
12407 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12408 * iterate over the add-ons just to get the last license data.
12409 */
12410 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12411 } else {
12412 $this->store_last_activated_license_data( $license );
12413 }
12414
12415 if ( $license->is_whitelabeled ) {
12416 // Activated a developer license, data should be hidden.
12417 $is_whitelabeled = true;
12418 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12419 // The account owner activated a regular license key, no need to hide the data.
12420 $is_whitelabeled = false;
12421 }
12422 }
12423
12424 $this->_storage->is_whitelabeled = $is_whitelabeled;
12425
12426 // Reset the whitelabeled status after update.
12427 $this->is_whitelabeled = null;
12428 if ( $this->is_addon() ) {
12429 $parent_fs = $this->get_parent_instance();
12430
12431 if ( is_object( $parent_fs ) ) {
12432 $parent_fs->is_whitelabeled = null;
12433 }
12434 }
12435 }
12436
12437 /**
12438 * @author Leo Fajardo (@leorw)
12439 * @since 2.3.1
12440 *
12441 * @param FS_Plugin_License $license
12442 * @param FS_User $license_user
12443 */
12444 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12445 if ( ! is_object( $license_user ) ) {
12446 $this->_storage->last_license_key = md5( $license->secret_key );
12447 $this->_storage->last_license_user_id = null;
12448 } else {
12449 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12450 $this->_storage->last_license_user_id = $license_user->id;
12451 }
12452 }
12453
12454 /**
12455 * @author Leo Fajardo (@leorw)
12456 * @since 2.3.1
12457 *
12458 * @param bool $ignore_data_debug_mode
12459 *
12460 * @return bool
12461 */
12462 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12463 if ( true !== $this->_storage->is_whitelabeled ) {
12464 return false;
12465 } else if ( $ignore_data_debug_mode ) {
12466 return true;
12467 }
12468
12469 $fs = $this->is_addon() ?
12470 $this->get_parent_instance() :
12471 $this;
12472
12473 return ! $fs->is_data_debug_mode();
12474 }
12475
12476 /**
12477 * @author Leo Fajardo (@leorw)
12478 * @since 2.3.1
12479 *
12480 * @return number
12481 */
12482 function get_last_license_user_id() {
12483 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12484 $this->_storage->last_license_user_id :
12485 null;
12486 }
12487
12488 /**
12489 * @author Leo Fajardo (@leorw)
12490 * @since 2.3.1
12491 *
12492 * @param int $blog_id
12493 * @param bool $ignore_data_debug_mode
12494 *
12495 * @return bool
12496 */
12497 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12498 if ( ! is_null( $blog_id ) ) {
12499 $this->switch_to_blog( $blog_id );
12500 }
12501
12502 if ( ! is_null( $this->is_whitelabeled ) ) {
12503 $is_whitelabeled = $this->is_whitelabeled;
12504 } else {
12505 $is_whitelabeled = false;
12506
12507 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12508
12509 if ( ! $this->has_addons() ) {
12510 $is_whitelabeled = $is_whitelabeled_flag;
12511 } else if ( $is_whitelabeled_flag ) {
12512 $is_whitelabeled = true;
12513 } else {
12514 if ( $this->is_registered() || $this->is_premium() ) {
12515 $addon_ids = $this->get_updated_account_addons();
12516 } else {
12517 $addons = self::get_all_addons();
12518
12519 $plugin_addons = isset( $addons[ $this->_plugin->id ] ) ?
12520 $addons[ $this->_plugin->id ] :
12521 array();
12522
12523 $addon_ids = array();
12524 foreach ( $plugin_addons as $addon ) {
12525 $addon_ids[] = $addon->id;
12526 }
12527 }
12528
12529 $installed_addons = $this->get_installed_addons();
12530 foreach ( $installed_addons as $fs_addon ) {
12531 $addon_ids[] = $fs_addon->get_id();
12532 }
12533
12534 if ( ! empty( $addon_ids ) ) {
12535 $addon_ids = array_unique( $addon_ids );
12536
12537 $is_network_level = (
12538 fs_is_network_admin() &&
12539 $this->is_network_active()
12540 );
12541
12542 foreach ( $addon_ids as $addon_id ) {
12543 $addon = $this->get_addon( $addon_id );
12544
12545 if ( ! is_object( $addon ) ) {
12546 continue;
12547 }
12548
12549 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12550 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12551 self::get_addon_instance( $addon_id ) :
12552 null;
12553
12554 $was_addon_network_activated = false;
12555
12556 if ( is_object( $fs_addon ) ) {
12557 $was_addon_network_activated = $fs_addon->is_network_active();
12558 } else if ( $is_network_level ) {
12559 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12560 }
12561
12562 $network_delegated_connection = (
12563 $was_addon_network_activated &&
12564 $addon_storage->get( 'is_delegated_connection', false, true )
12565 );
12566
12567 if (
12568 $is_network_level &&
12569 ( ! $was_addon_network_activated || $network_delegated_connection )
12570 ) {
12571 $sites = self::get_sites();
12572
12573 /**
12574 * If in network admin area and the add-on was not network-activated or network-activated
12575 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12576 */
12577 foreach ( $sites as $site ) {
12578 $site_info = $this->get_site_info( $site );
12579
12580 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12581 $is_whitelabeled = true;
12582 break;
12583 }
12584 }
12585
12586 if ( $is_whitelabeled ) {
12587 break;
12588 }
12589 } else {
12590 /**
12591 * This will be executed when any of the following is met:
12592 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12593 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12594 * 3. Add-on was not network-activated and in site admin area.
12595 */
12596 if ( true === $addon_storage->is_whitelabeled ) {
12597 $is_whitelabeled = true;
12598 break;
12599 }
12600 }
12601 }
12602 }
12603 }
12604
12605 $this->is_whitelabeled = $is_whitelabeled;
12606
12607 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12608 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12609 }
12610
12611 if ( ! is_null( $blog_id ) ) {
12612 $this->restore_current_blog();
12613 }
12614 }
12615
12616 return (
12617 $is_whitelabeled &&
12618 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12619 );
12620 }
12621
12622 /**
12623 * Sync site's subscription.
12624 *
12625 * @author Vova Feldman (@svovaf)
12626 * @since 1.0.9
12627 *
12628 * @param FS_Plugin_License|null $license
12629 *
12630 * @return bool|\FS_Subscription
12631 */
12632 private function _sync_site_subscription( $license ) {
12633 if ( ! is_object( $license ) ) {
12634 $this->delete_unused_subscriptions();
12635
12636 return false;
12637 }
12638
12639 // Load subscription details if not lifetime.
12640 $subscription = $license->is_lifetime() ?
12641 false :
12642 $this->_fetch_site_license_subscription();
12643
12644 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12645 $this->store_subscription( $subscription );
12646 } else {
12647 $this->delete_unused_subscriptions();
12648 }
12649
12650 return $subscription;
12651 }
12652
12653 /**
12654 * @author Vova Feldman (@svovaf)
12655 * @since 1.0.6
12656 *
12657 * @return bool|\FS_Plugin_License
12658 */
12659 function _get_license() {
12660 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12661 return $this->_license;
12662 }
12663
12664 return $this->_get_available_premium_license();
12665 }
12666
12667 /**
12668 * @param number $license_id
12669 *
12670 * @return null|\FS_Subscription
12671 */
12672 function _get_subscription( $license_id ) {
12673 if ( ! isset( $this->_storage->subscriptions ) ||
12674 empty( $this->_storage->subscriptions )
12675 ) {
12676 return null;
12677 }
12678
12679 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12680 if ( $subscription->license_id == $license_id ) {
12681 return $subscription;
12682 }
12683 }
12684
12685 return null;
12686 }
12687
12688 /**
12689 * @author Leo Fajardo (@leorw)
12690 * @since 2.0.0
12691 *
12692 * @param FS_Subscription $subscription
12693 */
12694 function store_subscription( FS_Subscription $subscription ) {
12695 if ( ! isset( $this->_storage->subscriptions ) ) {
12696 $this->_storage->subscriptions = array();
12697 }
12698
12699 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12700 $this->_storage->subscriptions = array( $subscription );
12701
12702 return;
12703 }
12704
12705 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
12706
12707 $updated_subscription = false;
12708 foreach ( $subscriptions as $key => $existing_subscription ) {
12709 if ( $existing_subscription->id == $subscription->id ) {
12710 $subscriptions[ $key ] = $subscription;
12711 $updated_subscription = true;
12712 break;
12713 }
12714 }
12715
12716 if ( ! $updated_subscription ) {
12717 $subscriptions[] = $subscription;
12718 }
12719
12720 $this->_storage->subscriptions = $subscriptions;
12721 }
12722
12723 /**
12724 * @author Leo Fajardo (@leorw)
12725 * @since 2.0.0
12726 */
12727 function delete_unused_subscriptions() {
12728 if ( ! isset( $this->_storage->subscriptions ) ||
12729 empty( $this->_storage->subscriptions ) ||
12730 // Clean up only if there are already at least 3 subscriptions.
12731 ( count( $this->_storage->subscriptions ) < 3 )
12732 ) {
12733 return;
12734 }
12735
12736 if ( ! is_multisite() ) {
12737 // If not multisite, there should only be 1 subscription, so just clear the array.
12738 $this->_storage->subscriptions = array();
12739
12740 return;
12741 }
12742
12743 $subscriptions_to_keep_by_license_id_map = array();
12744 $sites = self::get_sites();
12745 foreach ( $sites as $site ) {
12746 $blog_id = self::get_site_blog_id( $site );
12747 $install = $this->get_install_by_blog_id( $blog_id );
12748
12749 if ( ! is_object( $install ) ||
12750 ! FS_Plugin_License::is_valid_id( $install->license_id )
12751 ) {
12752 continue;
12753 }
12754
12755 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
12756 }
12757
12758 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
12759 $this->_storage->subscriptions = array();
12760
12761 return;
12762 }
12763
12764 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
12765 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
12766 unset( $this->_storage->subscriptions[ $key ] );
12767 }
12768 }
12769 }
12770
12771 /**
12772 * @author Vova Feldman (@svovaf)
12773 * @since 1.0.2
12774 *
12775 * @param string $plan Plan name
12776 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12777 *
12778 * @return bool
12779 */
12780 function is_plan( $plan, $exact = false ) {
12781 $this->_logger->entrance();
12782
12783 if ( ! $this->is_registered() ) {
12784 return false;
12785 }
12786
12787 $plan = strtolower( $plan );
12788
12789 $current_plan_name = $this->get_plan_name();
12790
12791 if ( $current_plan_name === $plan ) {
12792 // Exact plan.
12793 return true;
12794 } else if ( $exact ) {
12795 // Required exact, but plans are different.
12796 return false;
12797 }
12798
12799 $current_plan_order = - 1;
12800 $required_plan_order = PHP_INT_MAX;
12801 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12802 if ( $plan === $this->_plans[ $i ]->name ) {
12803 $required_plan_order = $i;
12804 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
12805 $current_plan_order = $i;
12806 }
12807 }
12808
12809 return ( $current_plan_order > $required_plan_order );
12810 }
12811
12812 /**
12813 * Check if module has only one plan.
12814 *
12815 * @author Vova Feldman (@svovaf)
12816 * @since 1.2.1.7
12817 *
12818 * @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.
12819 *
12820 * @return bool
12821 */
12822 function is_single_plan( $double_check = false ) {
12823 $this->_logger->entrance();
12824
12825 if ( ! $this->is_registered() ||
12826 ! is_array( $this->_plans ) ||
12827 0 === count( $this->_plans )
12828 ) {
12829 return true;
12830 }
12831
12832 $has_free_plan = $this->has_free_plan();
12833
12834 if ( ! $has_free_plan && $double_check ) {
12835 foreach ( $this->_plans as $plan ) {
12836 if ( $plan->is_free() ) {
12837 $has_free_plan = true;
12838 break;
12839 }
12840 }
12841 }
12842
12843 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
12844 }
12845
12846 /**
12847 * Check if plan based on trial. If not in trial mode, should return false.
12848 *
12849 * @since 1.0.9
12850 *
12851 * @param string $plan Plan name
12852 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12853 *
12854 * @return bool
12855 */
12856 function is_trial_plan( $plan, $exact = false ) {
12857 $this->_logger->entrance();
12858
12859 if ( ! $this->is_registered() ) {
12860 return false;
12861 }
12862
12863 if ( ! $this->is_trial() ) {
12864 return false;
12865 }
12866
12867 $trial_plan = $this->get_trial_plan();
12868
12869 if ( $trial_plan->name === $plan ) {
12870 // Exact plan.
12871 return true;
12872 } else if ( $exact ) {
12873 // Required exact, but plans are different.
12874 return false;
12875 }
12876
12877 $current_plan_order = - 1;
12878 $required_plan_order = - 1;
12879 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12880 if ( $plan === $this->_plans[ $i ]->name ) {
12881 $required_plan_order = $i;
12882 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
12883 $current_plan_order = $i;
12884 }
12885 }
12886
12887 return ( $current_plan_order > $required_plan_order );
12888 }
12889
12890 /**
12891 * Check if plugin has any paid plans.
12892 *
12893 * @author Vova Feldman (@svovaf)
12894 * @since 1.0.7
12895 *
12896 * @return bool
12897 */
12898 function has_paid_plan() {
12899 return $this->_has_paid_plans ||
12900 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
12901 }
12902
12903 /**
12904 * Check if plugin has any plan with a trail.
12905 *
12906 * @author Vova Feldman (@svovaf)
12907 * @since 1.0.9
12908 *
12909 * @return bool
12910 */
12911 function has_trial_plan() {
12912 /**
12913 * @author Vova Feldman(@svovaf)
12914 * @since 1.2.1.5
12915 *
12916 * Allow setting a trial from the SDK without calling the API.
12917 * But, if the user did opt-in, continue using the real data from the API.
12918 */
12919 if ( $this->_trial_days >= 0 ) {
12920 return true;
12921 }
12922
12923 return $this->_storage->get( 'has_trial_plan', false );
12924 }
12925
12926 /**
12927 * Check if plugin has any free plan, or is it premium only.
12928 *
12929 * Note: If no plans configured, assume plugin is free.
12930 *
12931 * @author Vova Feldman (@svovaf)
12932 * @since 1.0.7
12933 *
12934 * @return bool
12935 */
12936 function has_free_plan() {
12937 return ! $this->is_only_premium();
12938 }
12939
12940 /**
12941 * Displays a license activation dialog box when the user clicks on the "Activate License"
12942 * or "Change License" link on the plugins
12943 * page.
12944 *
12945 * @author Leo Fajardo (@leorw)
12946 * @since 1.1.9
12947 */
12948 function _add_license_activation_dialog_box() {
12949 $vars = array(
12950 'id' => $this->_module_id,
12951 );
12952
12953 fs_require_template( 'forms/license-activation.php', $vars );
12954 fs_require_template( 'forms/resend-key.php', $vars );
12955 }
12956
12957 /**
12958 * Displays an email address update dialog box when the user clicks on the email address "Edit" button on the "Account" page.
12959 *
12960 * @author Leo Fajardo (@leorw)
12961 * @since 2.5.0
12962 */
12963 function _add_email_address_update_dialog_box() {
12964 $vars = array( 'id' => $this->_module_id );
12965
12966 fs_require_template( 'forms/email-address-update.php', $vars );
12967 }
12968
12969 /**
12970 * @author Leo Fajardo (@leorw)
12971 * @since 2.5.0
12972 */
12973 function _add_email_address_update_option() {
12974 if ( ! $this->should_handle_user_change() ) {
12975 return;
12976 }
12977
12978 // Add email address update AJAX handler.
12979 $this->add_ajax_action( 'update_email_address', array( &$this, '_email_address_update_ajax_handler' ) );
12980 }
12981
12982 /**
12983 * @author Leo Fajardo (@leorw)
12984 * @since 2.5.0
12985 */
12986 function _email_address_update_ajax_handler() {
12987 $this->check_ajax_referer( 'update_email_address' );
12988
12989 $new_email_address = fs_request_get( 'email_address' );
12990 $transfer_type = fs_request_get( 'transfer_type' );
12991
12992 $result = $this->update_email( $new_email_address );
12993
12994 if ( ! FS_Api::is_api_error( $result ) ) {
12995 self::shoot_ajax_success();
12996 }
12997
12998 $error = '';
12999
13000 if ( FS_Api::is_api_error_object( $result ) ) {
13001 switch ( $result->error->code ) {
13002 case 'user_exist':
13003 case 'account_verification_required':
13004 $error = array(
13005 'code' => 'change_ownership',
13006 'url' => $this->get_account_url( 'change_owner', array(
13007 'state' => 'init',
13008 'candidate_email' => $new_email_address,
13009 'transfer_type' => $transfer_type,
13010 ) ),
13011 );
13012
13013 break;
13014 }
13015 }
13016
13017 if ( empty( $error ) ) {
13018 $error = is_object( $result ) ?
13019 var_export( $result->error, true ) :
13020 $result;
13021 }
13022
13023 self::shoot_ajax_failure( $error );
13024 }
13025
13026 /**
13027 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
13028 *
13029 * @author Leo Fajardo (@leorw)
13030 * @since 2.3.2
13031 *
13032 * @return number[]
13033 */
13034 function get_installs_ids_with_foreign_licenses() {
13035 $installs = array();
13036
13037 if (
13038 is_object( $this->_license ) &&
13039 $this->_site->user_id != $this->_license->user_id
13040 ) {
13041 $installs[] = $this->_site->id;
13042 }
13043
13044 /**
13045 * Also try to get foreign licenses for the context product's add-ons.
13046 */
13047 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
13048
13049 foreach ( $installs_by_slug_map as $slug => $install_info ) {
13050 if ( $slug == $this->get_slug() ) {
13051 continue;
13052 }
13053
13054 $install = $install_info['install'];
13055 $license = $install_info['license'];
13056
13057 if (
13058 is_object( $license ) &&
13059 $install->user_id != $license->user_id
13060 ) {
13061 $installs[] = $install->id;
13062 }
13063 }
13064
13065 return $installs;
13066 }
13067
13068 /**
13069 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
13070 *
13071 * @author Leo Fajardo (@leorw)
13072 * @since 2.3.2
13073 *
13074 * @param number[] $install_ids
13075 */
13076 function _add_user_change_dialog_box( $install_ids ) {
13077 $vars = array(
13078 'id' => $this->_module_id,
13079 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
13080 );
13081
13082 fs_require_template( 'forms/user-change.php', $vars );
13083 }
13084
13085 /**
13086 * @author Leo Fajardo (@leorw)
13087 * @since 2.3.1
13088 */
13089 function _add_data_debug_mode_dialog_box() {
13090 $vars = array(
13091 'id' => $this->_module_id,
13092 );
13093
13094 fs_require_template( 'forms/data-debug-mode.php', $vars );
13095 }
13096
13097 /**
13098 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
13099 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
13100 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
13101 * is only activated on a single production site.
13102 *
13103 * @author Leo Fajardo (@leorw)
13104 * @since 2.2.1
13105 *
13106 * @param bool $is_license_deactivation
13107 *
13108 * @return array
13109 */
13110 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
13111 if ( fs_is_network_admin() ) {
13112 // Subscription cancellation dialog box is currently not supported for multisite networks.
13113 return array();
13114 }
13115
13116 if ( $this->is_whitelabeled() ) {
13117 return array();
13118 }
13119
13120 $license = $this->_get_license();
13121
13122 /**
13123 * 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.
13124 *
13125 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
13126 * @since 2.2.1
13127 */
13128 if ( ! is_object( $license ) ||
13129 $license->is_lifetime() ||
13130 ( ! $license->is_single_site() && $license->activated > 1 )
13131 ) {
13132 return array();
13133 }
13134
13135 /**
13136 * @var FS_Subscription $subscription
13137 */
13138 $subscription = $this->_get_subscription( $license->id );
13139 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
13140 return array();
13141 }
13142
13143 return array(
13144 'id' => $this->_module_id,
13145 'license' => $license,
13146 'has_trial' => $this->is_paid_trial(),
13147 'is_license_deactivation' => $is_license_deactivation,
13148 );
13149 }
13150
13151 /**
13152 * @author Leo Fajardo (@leorw)
13153 * @since 2.0.2
13154 */
13155 function _add_premium_version_upgrade_selection_dialog_box() {
13156 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
13157 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
13158 return;
13159 }
13160
13161 $vars = array(
13162 'id' => $this->_module_id,
13163 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
13164 $modules_update->response[ $this->_plugin_basename ]->new_version :
13165 $modules_update->response[ $this->_plugin_basename ]['new_version']
13166 );
13167
13168 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13169 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13170 }
13171
13172 /**
13173 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13174 * page.
13175 *
13176 * @author Leo Fajardo (@leorw)
13177 * @since 1.2.1.5
13178 */
13179 function _add_optout_dialog() {
13180 if ( $this->is_theme() ) {
13181 $vars = null;
13182 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13183 }
13184
13185 $vars = array( 'id' => $this->_module_id );
13186 fs_require_template( 'forms/optout.php', $vars );
13187 }
13188
13189 /**
13190 * Prepare page to include all required UI and logic for the license activation dialog.
13191 *
13192 * @author Vova Feldman (@svovaf)
13193 * @since 1.2.0
13194 */
13195 function _add_license_activation() {
13196 if ( $this->is_migration() ) {
13197 return;
13198 }
13199
13200 if ( ! $this->is_user_admin() ) {
13201 // Only admins can activate a license.
13202 return;
13203 }
13204
13205 if ( ! $this->has_paid_plan() ) {
13206 // Module doesn't have any paid plans.
13207 return;
13208 }
13209
13210 if (
13211 $this->has_premium_version() &&
13212 ! $this->is_premium() &&
13213 /**
13214 * Also handle the case when an upgrade was made using the free version.
13215 *
13216 * @author Leo Fajardo (@leorw)
13217 * @since 2.3.2
13218 */
13219 ! is_object( $this->_get_license() )
13220 ) {
13221 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13222 return;
13223 }
13224
13225 // Add license activation link and AJAX request handler.
13226 if ( self::is_plugins_page() ) {
13227 $is_network_admin = fs_is_network_admin();
13228
13229 if (
13230 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13231 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13232 ) {
13233 if (
13234 $this->is_premium() ||
13235 ( $this->has_paid_plan() && ! $this->has_premium_version() )
13236 ) {
13237 /**
13238 * @since 1.2.0 Add license action link only on plugins page.
13239 */
13240 $this->_add_license_action_link();
13241 }
13242 }
13243 }
13244
13245 // Add license activation AJAX callback.
13246 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13247
13248 // Add resend license AJAX callback.
13249 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13250 }
13251
13252 /**
13253 * Prepares page to include all required UI and logic for the "Change User" dialog.
13254 *
13255 * @author Leo Fajardo (@leorw)
13256 * @since 2.3.2
13257 */
13258 function _add_user_change_option() {
13259 if ( ! $this->should_handle_user_change() ) {
13260 return;
13261 }
13262
13263 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13264
13265 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13266 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13267 return;
13268 }
13269
13270 // Add user change AJAX handler.
13271 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13272 }
13273
13274 /**
13275 * @author Leo Fajardo (@leorw)
13276 * @since 2.3.2
13277 */
13278 function should_handle_user_change() {
13279 if ( ! $this->is_user_admin() ) {
13280 // Only admins can change user.
13281 return false;
13282 }
13283
13284 if ( $this->is_addon() ) {
13285 return false;
13286 }
13287
13288 if ( ! $this->is_registered() ) {
13289 return false;
13290 }
13291
13292 if (
13293 $this->is_network_active() &&
13294 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13295 ) {
13296 // Handle only on site-level "Account" section for now.
13297 return false;
13298 }
13299
13300 return true;
13301 }
13302
13303 /**
13304 * @author Leo Fajardo (@leorw)
13305 * @since 2.0.2
13306 */
13307 function _add_premium_version_upgrade_selection() {
13308 if ( ! $this->is_user_admin() ) {
13309 return;
13310 }
13311
13312 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13313 // This is relevant only to the free versions and premium versions without an active license.
13314 return;
13315 }
13316
13317 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13318 $this->_add_premium_version_upgrade_selection_action();
13319 }
13320 }
13321
13322 /**
13323 * @author Edgar Melkonyan
13324 * @since 2.4.1
13325 *
13326 * @throws Freemius_Exception
13327 */
13328 function _toggle_whitelabel_mode_ajax_handler() {
13329 $this->_logger->entrance();
13330
13331 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13332
13333 if ( ! $this->is_user_admin() ) {
13334 // Only for admins.
13335 self::shoot_ajax_failure();
13336 }
13337
13338 $license = $this->get_api_user_scope()->call(
13339 "/licenses/{$this->_site->license_id}.json",
13340 'put',
13341 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13342 );
13343
13344 if ( ! $this->is_api_result_entity( $license ) ) {
13345 self::shoot_ajax_failure(
13346 FS_Api::is_api_error_object( $license ) ?
13347 $license->error->message :
13348 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13349 );
13350 }
13351
13352 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13353 $this->_store_licenses();
13354
13355 $this->_sync_license();
13356
13357 if ( ! $license->is_whitelabeled ) {
13358 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13359 } else {
13360 $this->_admin_notices->add_sticky(
13361 sprintf(
13362 $this->get_text_inline(
13363 '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.',
13364 'license_whitelabeled'
13365 ),
13366 "<strong>{$this->get_plugin_title()}</strong>",
13367 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13368 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13369 ),
13370 'license_whitelabeled'
13371 );
13372 }
13373
13374 self::shoot_ajax_response( array( 'success' => true ) );
13375 }
13376
13377 /**
13378 * @author Leo Fajardo (@leorw)
13379 * @since 2.3.0
13380 */
13381 function _add_beta_mode_update_handler() {
13382 if ( ! $this->is_user_admin() ) {
13383 return;
13384 }
13385
13386 if ( ! $this->is_premium() ) {
13387 return;
13388 }
13389
13390 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13391 }
13392
13393 /**
13394 * @author Leo Fajardo (@leorw)
13395 * @since 2.3.0
13396 */
13397 function _set_beta_mode_ajax_handler() {
13398 $this->_logger->entrance();
13399
13400 $this->check_ajax_referer( 'set_beta_mode' );
13401
13402 if ( ! $this->is_user_admin() ) {
13403 // Only for admins.
13404 self::shoot_ajax_failure();
13405 }
13406
13407 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13408
13409 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13410 self::shoot_ajax_failure();
13411 }
13412
13413 $site = $this->api_site_call(
13414 '',
13415 'put',
13416 array(
13417 'is_beta' => ( 'true' == $is_beta ),
13418 'fields' => 'is_beta'
13419 )
13420 );
13421
13422 if ( ! $this->is_api_result_entity( $site ) ) {
13423 self::shoot_ajax_failure(
13424 FS_Api::is_api_error_object( $site ) ?
13425 $site->error->message :
13426 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13427 );
13428 }
13429
13430 $this->_site->is_beta = $site->is_beta;
13431 $this->_store_site();
13432
13433 self::shoot_ajax_response( array( 'success' => true ) );
13434 }
13435
13436 /**
13437 * License activation WP AJAX handler.
13438 *
13439 * @author Leo Fajardo (@leorw)
13440 * @since 1.1.9
13441 *
13442 * @uses Freemius::activate_license()
13443 */
13444 function _activate_license_ajax_action() {
13445 $this->_logger->entrance();
13446
13447 $this->check_ajax_referer( 'activate_license' );
13448
13449 $license_key = trim( fs_request_get_raw( 'license_key' ) );
13450
13451 if ( empty( $license_key ) ) {
13452 $license_id = trim( fs_request_get_raw( 'license_id' ) );
13453
13454 if ( FS_Plugin_License::is_valid_id( $license_id ) ) {
13455 $license = $this->_get_license_by_id( $license_id, false );
13456
13457 if ( is_object( $license ) ) {
13458 $license_key = $license->secret_key;
13459 }
13460 }
13461 }
13462
13463 if ( empty( $license_key ) ) {
13464 exit;
13465 }
13466
13467 $sites = fs_is_network_admin() ?
13468 fs_request_get( 'sites', array(), 'post' ) :
13469 array();
13470
13471 $result = $this->activate_license(
13472 $license_key,
13473 $sites,
13474 fs_request_get_bool( 'is_marketing_allowed', null ),
13475 fs_request_get( 'blog_id', null ),
13476 fs_request_get( 'module_id', null, 'post' ),
13477 fs_request_get( 'user_id', null ),
13478 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
13479 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
13480 fs_request_get( 'license_owner_id', null )
13481 );
13482
13483 if (
13484 $result['success'] &&
13485 $this->is_bundle_license_auto_activation_enabled()
13486 ) {
13487 $license = new FS_Plugin_License();
13488 $license->secret_key = $license_key;
13489
13490 $this->maybe_activate_bundle_license( $license, $sites );
13491 }
13492
13493 echo json_encode( $result );
13494
13495 exit;
13496 }
13497
13498 /**
13499 * User change WP AJAX handler.
13500 *
13501 * @author Leo Fajardo (@leorw)
13502 * @since 2.3.2
13503 */
13504 function _user_change_ajax_action() {
13505 $this->_logger->entrance();
13506
13507 $this->check_ajax_referer( 'change_user' );
13508
13509 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13510 $new_user_id = fs_request_get( 'user_id' );
13511
13512 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13513 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13514 }
13515
13516 $params = array();
13517
13518 if ( ! empty( $new_email_address ) ) {
13519 $params['user_email'] = $new_email_address;
13520 } else {
13521 $params['user_id'] = $new_user_id;
13522 }
13523
13524 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13525 $install_ids = array();
13526
13527 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13528 $install_ids[ $slug ] = $install_info['install']->id;
13529 }
13530
13531 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13532
13533 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13534
13535 if ( FS_Api::is_api_error( $install ) ) {
13536 $error = '';
13537
13538 if ( is_object( $install ) ) {
13539 switch ( $install->error->code ) {
13540 case 'user_exist':
13541 $error = (
13542 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13543 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
13544 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>' ) .
13545 sprintf(
13546 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13547 $this->get_account_url( 'change_owner', array(
13548 'state' => 'init',
13549 'candidate_email' => $new_email_address
13550 ) ),
13551 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
13552 )
13553 );
13554 break;
13555 }
13556 }
13557
13558 if ( empty( $error ) ) {
13559 $error = FS_Api::is_api_error_object( $install ) ?
13560 $install->error->message :
13561 var_export( $install->error, true );
13562 }
13563
13564 self::shoot_ajax_failure( $error );
13565 } else {
13566 if (
13567 // If successful ownership change.
13568 $this->get_user()->id != $install->user_id ||
13569 ! empty( $new_email_address )
13570 ) {
13571 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
13572 }
13573 }
13574
13575 self::shoot_ajax_success();
13576 }
13577
13578 /**
13579 * @author Leo Fajardo (@leorw)
13580 * @since 2.3.2.14
13581 */
13582 function starting_migration() {
13583 if ( ! empty( $this->_storage->license_migration ) ) {
13584 // Do not overwrite the data if already set.
13585 return;
13586 }
13587
13588 $this->_storage->license_migration = array(
13589 'is_migrating' => true,
13590 'start_timestamp' => time()
13591 );
13592 }
13593
13594 /**
13595 * @author Leo Fajardo (@leorw)
13596 * @since 2.3.2.14
13597 */
13598 function is_migration() {
13599 if ( $this->is_addon() ) {
13600 return $this->get_parent_instance()->is_migration();
13601 }
13602
13603 if ( empty( $this->_storage->license_migration ) ) {
13604 return false;
13605 }
13606
13607 if ( ! $this->_storage->license_migration['is_migrating'] ) {
13608 return false;
13609 }
13610
13611 return (
13612 // Return `true` if the migration is within 5 minutes from the starting time.
13613 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
13614 );
13615 }
13616
13617 /**
13618 *
13619 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
13620 *
13621 * @author Vova Feldman (@svovaf)
13622 * @since 2.3.0
13623 *
13624 * @param string $license_key
13625 * @param null|bool $is_marketing_allowed
13626 * @param null|number $plugin_id
13627 * @param array $sites
13628 * @param int $blog_id
13629 * @param null|number $license_owner_id
13630 *
13631 * @return array {
13632 * @var bool $success
13633 * @var string $error
13634 * @var string $next_page
13635 * }
13636 *
13637 * @uses Freemius::activate_license()
13638 */
13639 function activate_migrated_license(
13640 $license_key,
13641 $is_marketing_allowed = null,
13642 $plugin_id = null,
13643 $sites = array(),
13644 $blog_id = null,
13645 $license_owner_id = null
13646 ) {
13647 $this->_logger->entrance();
13648
13649 $result = $this->activate_license(
13650 $license_key,
13651 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
13652 $this->get_sites_for_network_level_optin() :
13653 $sites,
13654 $is_marketing_allowed,
13655 $blog_id,
13656 $plugin_id,
13657 null,
13658 null,
13659 null,
13660 $license_owner_id
13661 );
13662
13663 // No need to show the sticky after license activation notice after migrating a license.
13664 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13665
13666 return $result;
13667 }
13668
13669 /**
13670 * @author Leo Fajardo (@leorw)
13671 * @since 2.3.1
13672 *
13673 * @return string
13674 */
13675 function get_pricing_js_path() {
13676 if ( ! isset( $this->_pricing_js_path ) ) {
13677 $default_path = WP_FS__DIR_JS . '/pricing/freemius-pricing.js';
13678
13679 $pricing_js_path = $this->apply_filters( 'freemius_pricing_js_path', $default_path );
13680
13681 // Backward compatibility for people who placed the freemius-pricing inside `includes` directory. Let it take more preference than the default path.
13682 if ( empty( $pricing_js_path ) ) {
13683 global $fs_active_plugins;
13684
13685 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
13686 if ( $data->plugin_path == $this->get_plugin_basename() ) {
13687 $plugin_or_theme_root_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
13688
13689 $pricing_js_path = $plugin_or_theme_root_dir
13690 . '/'
13691 // The basename will be `plugins`, `themes`, or the basename of a custom plugins or themes directory.
13692 . str_replace( '../' . basename( $plugin_or_theme_root_dir ) . '/', '', $sdk_path )
13693 . '/includes/freemius-pricing/freemius-pricing.js';
13694
13695 break;
13696 }
13697 }
13698 }
13699
13700 // If it is still empty, load the default pricing JS.
13701 if ( ! file_exists( $pricing_js_path ) ) {
13702 $pricing_js_path = $default_path;
13703 }
13704
13705 $this->_pricing_js_path = $pricing_js_path;
13706 }
13707
13708 return $this->_pricing_js_path;
13709 }
13710
13711 /**
13712 * @author Leo Fajardo (@leorw)
13713 * @since 2.3.1
13714 *
13715 * @deprecated Since v2.9.0 we have removed the iFrame based pricing. This will always return `false`.
13716 *
13717 * @return bool
13718 */
13719 function should_use_external_pricing() {
13720 return false;
13721 }
13722
13723 /**
13724 * The implementation of this method was previously in `_activate_license_ajax_action()`.
13725 *
13726 * @author Vova Feldman (@svovaf)
13727 * @since 2.2.4
13728 * @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).
13729 * @param string $license_key
13730 * @param array $sites
13731 * @param null|bool $is_marketing_allowed
13732 * @param null|int $blog_id
13733 * @param null|number $plugin_id
13734 * @param null|number $user_id
13735 * @param bool|null $is_extensions_tracking_allowed
13736 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint.
13737 * @param null|number $license_owner_id
13738 *
13739 *
13740 * @return array {
13741 * @var bool $success
13742 * @var string $error
13743 * @var string $next_page
13744 * }
13745 */
13746 private function activate_license(
13747 $license_key,
13748 $sites = array(),
13749 $is_marketing_allowed = null,
13750 $blog_id = null,
13751 $plugin_id = null,
13752 $user_id = null,
13753 $is_extensions_tracking_allowed = null,
13754 $is_diagnostic_tracking_allowed = null,
13755 $license_owner_id = null
13756 ) {
13757 $this->_logger->entrance();
13758
13759 $license_key = trim( $license_key );
13760
13761 $is_network_activation_or_migration = (
13762 fs_is_network_admin() ||
13763 ( ! empty( $sites ) && $this->is_migration() )
13764 );
13765
13766 if ( ! $is_network_activation_or_migration ) {
13767 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
13768 $sites = array();
13769 }
13770
13771 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
13772 $this :
13773 $this->get_addon_instance( $plugin_id );
13774
13775 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
13776 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
13777 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
13778 ) );
13779
13780 $error = false;
13781 $next_page = false;
13782
13783 $has_valid_blog_id = is_numeric( $blog_id );
13784
13785 $user = null;
13786
13787 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
13788 /**
13789 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
13790 *
13791 * @author Vova Feldman (@svovaf)
13792 */
13793 $user = $fs->get_parent_instance()->get_current_or_network_user();
13794 } else if ( $fs->is_registered() ) {
13795 $user = $fs->get_current_or_network_user();
13796 }
13797
13798 if ( $has_valid_blog_id ) {
13799 /**
13800 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
13801 *
13802 * @author Leo Fajardo (@leorw)
13803 */
13804 $fs->switch_to_blog( $blog_id );
13805 }
13806
13807 if ( is_object( $user ) ) {
13808 $result = true;
13809
13810 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
13811 // If no specific blog ID was provided, activate the license for all sites in the network.
13812 $blog_2_install_map = array();
13813 $site_ids = array();
13814
13815 foreach ( $sites as $site ) {
13816 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
13817 continue;
13818 }
13819
13820 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
13821
13822 if ( is_object( $install ) ) {
13823 $blog_2_install_map[ $site['blog_id'] ] = $install;
13824 } else {
13825 $site_ids[] = $site['blog_id'];
13826 }
13827 }
13828
13829 if ( ! empty( $blog_2_install_map ) ) {
13830 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
13831 }
13832
13833 if ( true === $result && ! empty( $site_ids ) ) {
13834 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
13835 }
13836 } else {
13837 if ( $fs->is_registered() ) {
13838 $params = array(
13839 'license_key' => $fs->apply_filters( 'license_key', $license_key )
13840 );
13841
13842 $install_ids = array();
13843
13844 $change_owner = FS_User::is_valid_id( $user_id );
13845
13846 if ( $change_owner ) {
13847 $params['user_id'] = $user_id;
13848
13849 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
13850
13851 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13852 $install_ids[ $slug ] = $install_info['install']->id;
13853 }
13854
13855 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13856 }
13857
13858 $api = $fs->get_api_site_scope();
13859
13860 $result = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
13861
13862 if ( ! FS_Api::is_api_error( $result ) ) {
13863 $install = $result;
13864
13865 $fs->reconnect_locally( $has_valid_blog_id );
13866
13867 if (
13868 $change_owner &&
13869 // If successful ownership change.
13870 $fs->get_user()->id != $install->user_id
13871 ) {
13872 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
13873 }
13874 }
13875 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
13876 $result = $fs->activate_license_on_site( $user, $license_key );
13877 }
13878 }
13879
13880 $is_connected = null;
13881
13882 if ( true !== $result && ! FS_Api::is_api_result_entity( $result ) ) {
13883 if ( FS_Api::is_blocked( $result ) ) {
13884 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
13885
13886 $is_connected = false;
13887 }
13888
13889 $error = FS_Api::is_api_error_object( $result ) ?
13890 $result->error->message :
13891 var_export( $result, true );
13892 } else {
13893 $is_connected = true;
13894
13895 $fs->network_upgrade_mode_completed();
13896
13897 $fs->_user = $user;
13898
13899 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
13900 $fs->_site = $fs->get_network_install();
13901 }
13902
13903 $fs->_sync_license( true, $has_valid_blog_id );
13904
13905 $this->maybe_sync_install_user();
13906
13907 $next_page = $fs->is_addon() ?
13908 $fs->get_parent_instance()->get_account_url() :
13909 $fs->get_after_activation_url( 'after_connect_url' );
13910 }
13911
13912 $fs->update_connectivity_info( $is_connected );
13913 } else {
13914 $next_page = $fs->opt_in(
13915 false,
13916 false,
13917 false,
13918 $license_key,
13919 false,
13920 false,
13921 false,
13922 $is_marketing_allowed,
13923 $sites,
13924 true,
13925 $license_owner_id
13926 );
13927
13928 if ( isset( $next_page->error ) ) {
13929 $error = $next_page->error;
13930 } else {
13931 if ( $is_network_activation_or_migration ) {
13932 /**
13933 * Get the list of sites that were just opted-in (and license activated).
13934 * This is an optimization for the next part below saving some DB queries.
13935 */
13936 $connected_sites = array();
13937 foreach ( $sites as $site ) {
13938 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
13939 $connected_sites[ $site['blog_id'] ] = true;
13940 }
13941 }
13942
13943 $all_sites = self::get_sites();
13944 $pending_blog_ids = array();
13945
13946 /**
13947 * 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.
13948 *
13949 * @author Vova Feldman (@svovaf)
13950 */
13951 foreach ( $all_sites as $site ) {
13952 $blog_id = self::get_site_blog_id( $site );
13953
13954 if ( isset( $connected_sites[ $blog_id ] ) ) {
13955 // Site was just connected.
13956 continue;
13957 }
13958
13959 if ( $fs->is_installed_on_site( $blog_id ) ) {
13960 // Site was already connected before.
13961 continue;
13962 }
13963
13964 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
13965 // Site's connection was delegated.
13966 continue;
13967 }
13968
13969 if ( $fs->is_anonymous_site( $blog_id ) ) {
13970 // Site connection was already skipped.
13971 continue;
13972 }
13973
13974 $pending_blog_ids[] = $blog_id;
13975 }
13976
13977 if ( ! empty( $pending_blog_ids ) ) {
13978 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
13979 $fs->skip_connection( $pending_blog_ids );
13980 } else {
13981 $fs->delegate_connection( $pending_blog_ids );
13982 }
13983 }
13984 }
13985 }
13986 }
13987
13988 if ( false === $error && true === $fs->_storage->require_license_activation ) {
13989 $fs->_storage->require_license_activation = false;
13990 }
13991
13992 $result = array(
13993 'success' => ( false === $error )
13994 );
13995
13996 if ( false !== $error ) {
13997 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
13998 } else {
13999 if ( $fs->is_addon() || $fs->has_addons() ) {
14000 /**
14001 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
14002 * an updated valid user licenses collection will be fetched from the server which is used to also
14003 * update the account add-ons (add-ons the user has licenses for).
14004 *
14005 * @author Leo Fajardo (@leorw)
14006 * @since 2.2.4
14007 */
14008 $fs->purge_valid_user_licenses_cache();
14009 }
14010
14011 $result['next_page'] = $next_page;
14012 }
14013
14014 if ( $result['success'] ) {
14015 $this->do_action( 'after_license_activation' );
14016 }
14017
14018 return $result;
14019 }
14020
14021 /**
14022 * @author Leo Fajardo (@leorw)
14023 * @since 2.3.2
14024 *
14025 * @return array {
14026 * @key string Product slug.
14027 * @value array {
14028 * @property FS_Site $site
14029 * @property FS_Plugin_License $license
14030 * }
14031 * }
14032 */
14033 private function get_parent_and_addons_installs_info() {
14034 $fs = $this->is_addon() ?
14035 $this->get_parent_instance() :
14036 $this;
14037
14038 $installed_addons_ids = array();
14039
14040 $installed_addons_instances = $fs->get_installed_addons();
14041 foreach ( $installed_addons_instances as $instance ) {
14042 $installed_addons_ids[] = $instance->get_id();
14043 }
14044
14045 $addons_ids = array_unique( array_merge(
14046 $installed_addons_ids,
14047 $fs->get_updated_account_addons()
14048 ) );
14049
14050 // Add parent product info.
14051 $installs_info_by_slug_map = array(
14052 $fs->get_slug() => array(
14053 'install' => $fs->get_site(),
14054 'license' => $fs->_get_license()
14055 )
14056 );
14057
14058 foreach ( $addons_ids as $addon_id ) {
14059 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
14060
14061 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
14062
14063 if ( ! isset( $addon_info['is_connected'] ) || ! $addon_info['is_connected'] ) {
14064 // Add-on is not associated with an install entity.
14065 continue;
14066 }
14067
14068 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
14069 'install' => $addon_info['site'],
14070 'license' => isset( $addon_info['license'] ) ?
14071 $addon_info['license'] :
14072 null
14073 );
14074 }
14075
14076 return $installs_info_by_slug_map;
14077 }
14078
14079 /**
14080 * @author Leo Fajardo (@leorw)
14081 * @since 1.2.3.1
14082 */
14083 function _network_activate_ajax_action() {
14084 $this->_logger->entrance();
14085
14086 $this->check_ajax_referer( 'network_activate' );
14087
14088 $plugin_id = fs_request_get( 'module_id', '', 'post' );
14089 $fs = ( $plugin_id == $this->_module_id ) ?
14090 $this :
14091 $this->get_addon_instance( $plugin_id );
14092
14093 $error = false;
14094
14095 $sites = fs_request_get( 'sites', array(), 'post' );
14096 if ( is_array( $sites ) && ! empty( $sites ) ) {
14097 $sites_by_action = array(
14098 'allow' => array(),
14099 'delegate' => array(),
14100 'skip' => array()
14101 );
14102
14103 foreach ( $sites as $site ) {
14104 $sites_by_action[ $site['action'] ][] = $site;
14105 }
14106
14107 $total_sites = count( $sites );
14108 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
14109
14110 $next_page = '';
14111
14112 $has_any_install = fs_request_get_bool( 'has_any_install' );
14113
14114 if ( $total_sites === $total_sites_to_delegate &&
14115 ! $this->is_network_upgrade_mode() &&
14116 ! $has_any_install
14117 ) {
14118 $this->delegate_connection();
14119 } else {
14120 if ( ! empty( $sites_by_action['delegate'] ) ) {
14121 $this->delegate_connection( self::get_sites_blog_ids( $sites_by_action['delegate'] ) );
14122 }
14123
14124 if ( ! empty( $sites_by_action['skip'] ) ) {
14125 $this->skip_connection( self::get_sites_blog_ids( $sites_by_action['skip'] ) );
14126 }
14127
14128 if ( empty( $sites_by_action['allow'] ) ) {
14129 if ( $has_any_install ) {
14130 $first_install = $fs->find_first_install();
14131
14132 if ( ! is_null( $first_install ) ) {
14133 $fs->_site = $first_install['install'];
14134 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
14135
14136 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
14137 $fs->_storage->network_user_id = $fs->_user->id;
14138 }
14139 }
14140 } else {
14141 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
14142 $next_page = $fs->opt_in(
14143 false,
14144 false,
14145 false,
14146 false,
14147 false,
14148 false,
14149 false,
14150 fs_request_get_bool( 'is_marketing_allowed', null ),
14151 $sites_by_action['allow']
14152 );
14153 } else {
14154 $next_page = $fs->install_with_user(
14155 $this->get_network_user(),
14156 false,
14157 false,
14158 false,
14159 true,
14160 $sites_by_action['allow']
14161 );
14162 }
14163
14164 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
14165 $error = $next_page->error;
14166 }
14167 }
14168 }
14169
14170 if ( empty( $next_page ) ) {
14171 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
14172 }
14173 } else {
14174 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
14175 }
14176
14177 $result = array(
14178 'success' => ( false === $error )
14179 );
14180
14181 if ( false !== $error ) {
14182 $result['error'] = $error;
14183 } else {
14184 $result['next_page'] = $next_page;
14185 }
14186
14187 echo json_encode( $result );
14188
14189 exit;
14190 }
14191
14192 /**
14193 * Billing update AJAX callback.
14194 *
14195 * @author Vova Feldman (@svovaf)
14196 * @since 1.2.1.5
14197 */
14198 function _update_billing_ajax_action() {
14199 $this->_logger->entrance();
14200
14201 $this->check_ajax_referer( 'update_billing' );
14202
14203 if ( ! $this->is_user_admin() ) {
14204 // Only for admins.
14205 self::shoot_ajax_failure();
14206 }
14207
14208 $billing = fs_request_get( 'billing' );
14209
14210 $api = $this->get_api_user_scope();
14211 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
14212 'plugin_id' => $this->get_parent_id(),
14213 ) ) );
14214
14215 if ( ! $this->is_api_result_entity( $result ) ) {
14216 self::shoot_ajax_failure();
14217 }
14218
14219 // Purge cached billing.
14220 $this->get_api_user_scope()->purge_cache( 'billing.json' );
14221
14222 self::shoot_ajax_success();
14223 }
14224
14225 /**
14226 * Trial start for anonymous users (AJAX callback).
14227 *
14228 * @author Vova Feldman (@svovaf)
14229 * @since 1.2.1.5
14230 */
14231 function _start_trial_ajax_action() {
14232 $this->_logger->entrance();
14233
14234 $this->check_ajax_referer( 'start_trial' );
14235
14236 if ( ! $this->is_user_admin() ) {
14237 // Only for admins.
14238 self::shoot_ajax_failure();
14239 }
14240
14241 $trial_data = fs_request_get( 'trial' );
14242
14243 $next_page = $this->opt_in(
14244 false,
14245 false,
14246 false,
14247 false,
14248 false,
14249 $trial_data['plan_id']
14250 );
14251
14252 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14253 self::shoot_ajax_failure(
14254 isset( $next_page->error ) ?
14255 $next_page->error->message :
14256 var_export( $next_page, true )
14257 );
14258 }
14259
14260 $this->shoot_ajax_success( array(
14261 'next_page' => $next_page,
14262 ) );
14263 }
14264
14265 /**
14266 * @author Leo Fajardo (@leorw)
14267 * @since 1.2.0
14268 */
14269 function _resend_license_key_ajax_action() {
14270 $this->_logger->entrance();
14271
14272 $this->check_ajax_referer( 'resend_license_key' );
14273
14274 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14275
14276 if ( empty( $email_address ) ) {
14277 exit;
14278 }
14279
14280 $error = false;
14281
14282 $api = $this->get_api_plugin_scope();
14283 $result = $api->call( '/licenses/resend.json', 'post',
14284 array(
14285 'email' => $email_address,
14286 'url' => home_url(),
14287 )
14288 );
14289
14290 if ( is_object( $result ) && isset( $result->error ) ) {
14291 $error = $result->error;
14292
14293 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14294 $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' );
14295 } else if ( 'no_license' === $error->code ) {
14296 $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' );
14297 } else {
14298 $error = $error->message;
14299 }
14300 }
14301
14302 $licenses = array(
14303 'success' => ( false === $error )
14304 );
14305
14306 if ( false !== $error ) {
14307 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14308 }
14309
14310 echo json_encode( $licenses );
14311
14312 exit;
14313 }
14314
14315 /**
14316 * @author Vova Feldman (@svovaf)
14317 * @since 1.2.1.8
14318 *
14319 * @var string
14320 */
14321 private static $_pagenow;
14322
14323 /**
14324 * Get current page or the referer if executing a WP AJAX request.
14325 *
14326 * @author Vova Feldman (@svovaf)
14327 * @since 1.2.1.8
14328 *
14329 * @return string
14330 */
14331 static function get_current_page() {
14332 if ( ! isset( self::$_pagenow ) ) {
14333 global $pagenow;
14334 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14335 /**
14336 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14337 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14338 *
14339 * @author Leo Fajardo (@leorw)
14340 * @since 2.2.3
14341 */
14342 if ( is_network_admin() ) {
14343 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14344 } else if ( is_user_admin() ) {
14345 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14346 } else {
14347 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14348 }
14349
14350 $pagenow = $self_matches[1];
14351 $pagenow = trim( $pagenow, '/' );
14352 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14353 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14354 $pagenow = 'index.php';
14355 } else {
14356 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14357 $pagenow = strtolower( $self_matches[1] );
14358 if ( '.php' !== substr($pagenow, -4, 4) )
14359 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14360 }
14361 }
14362
14363 self::$_pagenow = $pagenow;
14364
14365 if ( self::is_ajax() &&
14366 'admin-ajax.php' === $pagenow
14367 ) {
14368 $referer = fs_get_raw_referer();
14369
14370 if ( is_string( $referer ) ) {
14371 $parts = explode( '?', $referer );
14372
14373 self::$_pagenow = basename( $parts[0] );
14374 }
14375 }
14376 }
14377
14378 return self::$_pagenow;
14379 }
14380
14381 /**
14382 * Helper method to check if user in the plugins page.
14383 *
14384 * @author Vova Feldman (@svovaf)
14385 * @since 1.2.1.5
14386 *
14387 * @return bool
14388 */
14389 static function is_plugins_page() {
14390 return ( 'plugins.php' === self::get_current_page() );
14391 }
14392
14393 /**
14394 * @author Leo Fajardo (@leorw)
14395 * @since 2.2.3
14396 *
14397 * @return bool
14398 */
14399 static function is_plugin_install_page() {
14400 return ( 'plugin-install.php' === self::get_current_page() );
14401 }
14402
14403 /**
14404 * @author Leo Fajardo (@leorw)
14405 * @since 2.0.2
14406 *
14407 * @return bool
14408 */
14409 static function is_updates_page() {
14410 return ( 'update-core.php' === self::get_current_page() );
14411 }
14412
14413 /**
14414 * Helper method to check if user in the themes page.
14415 *
14416 * @author Vova Feldman (@svovaf)
14417 * @since 1.2.2.6
14418 *
14419 * @return bool
14420 */
14421 static function is_themes_page() {
14422 return ( 'themes.php' === self::get_current_page() );
14423 }
14424
14425 #----------------------------------------------------------------------------------
14426 #region Affiliation
14427 #----------------------------------------------------------------------------------
14428
14429 /**
14430 * @author Leo Fajardo (@leorw)
14431 * @since 1.2.3
14432 *
14433 * @return bool
14434 */
14435 function has_affiliate_program() {
14436 if ( ! is_object( $this->_plugin ) ) {
14437 return false;
14438 }
14439
14440 return $this->_plugin->has_affiliate_program();
14441 }
14442
14443 /**
14444 * Get Plugin ID under which we will track affiliate application.
14445 *
14446 * This could either be the Bundle ID or the main plugin ID.
14447 *
14448 * @return number Bundle ID if developer has provided one, else the main plugin ID.
14449 */
14450 private function get_plugin_id_for_affiliate_terms() {
14451 return $this->has_bundle_context() ?
14452 $this->get_bundle_id() :
14453 $this->_plugin->id;
14454 }
14455
14456 /**
14457 * @author Leo Fajardo (@leorw)
14458 * @since 1.2.4
14459 */
14460 private function fetch_affiliate_terms() {
14461 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14462 /**
14463 * In case we have a bundle set in SDK configuration, we would like to use that for affiliates, not the main plugin.
14464 */
14465 $plugins_api = $this->has_bundle_context() ?
14466 $this->get_api_bundle_scope() :
14467 $this->get_api_plugin_scope();
14468
14469 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14470
14471 /**
14472 * 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.
14473 */
14474 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14475 return;
14476 }
14477
14478 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14479 }
14480 }
14481
14482 /**
14483 * @author Leo Fajardo (@leorw)
14484 * @since 1.2.4
14485 */
14486 private function fetch_affiliate_and_custom_terms() {
14487 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14488 $application_data = $this->_storage->affiliate_application_data;
14489 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14490
14491 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14492
14493 $users_api = $this->get_api_user_scope();
14494 $result = $users_api->get( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14495 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14496 if ( ! empty( $result->affiliates ) ) {
14497 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14498
14499 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14500 $application_data['status'] = $affiliate->status;
14501 $this->_storage->affiliate_application_data = $application_data;
14502 }
14503
14504 if ( $affiliate->is_using_custom_terms ) {
14505 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14506 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14507 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14508 }
14509 }
14510
14511 $this->affiliate = $affiliate;
14512 }
14513 }
14514 }
14515 }
14516
14517 /**
14518 * @author Leo Fajardo (@leorw)
14519 * @since 1.2.3
14520 */
14521 private function fetch_affiliate_and_terms() {
14522 $this->_logger->entrance();
14523
14524 $this->fetch_affiliate_terms();
14525 $this->fetch_affiliate_and_custom_terms();
14526 }
14527
14528 /**
14529 * @author Leo Fajardo (@leorw)
14530 * @since 1.2.3
14531 *
14532 * @return FS_Affiliate
14533 */
14534 function get_affiliate() {
14535 return $this->affiliate;
14536 }
14537
14538
14539 /**
14540 * @author Leo Fajardo (@leorw)
14541 * @since 1.2.3
14542 *
14543 * @return FS_AffiliateTerms
14544 */
14545 function get_affiliate_terms() {
14546 return is_object( $this->custom_affiliate_terms ) ?
14547 $this->custom_affiliate_terms :
14548 $this->plugin_affiliate_terms;
14549 }
14550
14551 /**
14552 * @author Leo Fajardo (@leorw)
14553 * @since 1.2.3
14554 */
14555 function _submit_affiliate_application() {
14556 $this->_logger->entrance();
14557
14558 $this->check_ajax_referer( 'submit_affiliate_application' );
14559
14560 if ( ! $this->is_user_admin() ) {
14561 // Only for admins.
14562 self::shoot_ajax_failure();
14563 }
14564
14565 $affiliate = fs_request_get( 'affiliate' );
14566
14567 if ( empty( $affiliate['promotion_methods'] ) ) {
14568 unset( $affiliate['promotion_methods'] );
14569 }
14570
14571 if ( ! empty( $affiliate['additional_domains'] ) ) {
14572 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
14573 }
14574
14575 if ( ! $this->is_registered() ) {
14576 $email_address = isset( $affiliate['email'] ) ? $affiliate['email'] : '';
14577
14578 if ( ! is_email( $email_address ) ) {
14579 self::shoot_ajax_failure('Invalid email address.');
14580 }
14581
14582 // Opt in but don't track usage.
14583 $next_page = $this->opt_in(
14584 $email_address,
14585 false,
14586 false,
14587 false,
14588 false,
14589 false,
14590 true
14591 );
14592
14593 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14594 self::shoot_ajax_failure(
14595 isset( $next_page->error ) ?
14596 $next_page->error->message :
14597 var_export( $next_page, true )
14598 );
14599 } else if ( $this->is_pending_activation() ) {
14600 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' ) );
14601 }
14602 }
14603
14604 $this->fetch_affiliate_terms();
14605
14606 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14607
14608 $api = $this->get_api_user_scope();
14609 $result = $api->call(
14610 ( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
14611 'post',
14612 $affiliate
14613 );
14614
14615 if ( $this->is_api_error( $result ) ) {
14616 self::shoot_ajax_failure(
14617 isset( $result->error ) ?
14618 $result->error->message :
14619 var_export( $result, true )
14620 );
14621 } else {
14622 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
14623 $this->_admin_notices->remove_sticky( 'affiliate_program' );
14624 }
14625
14626 $affiliate_application_data = array(
14627 'status' => 'pending',
14628 'stats_description' => $affiliate['stats_description'],
14629 'promotion_method_description' => $affiliate['promotion_method_description'],
14630 );
14631
14632 if ( ! empty( $affiliate['promotion_methods'] ) ) {
14633 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
14634 }
14635
14636 if ( ! empty( $affiliate['domain'] ) ) {
14637 $affiliate_application_data['domain'] = $affiliate['domain'];
14638 }
14639
14640 if ( ! empty( $affiliate['additional_domains'] ) ) {
14641 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
14642 }
14643
14644 $this->_storage->affiliate_application_data = $affiliate_application_data;
14645 }
14646
14647 // Purge cached affiliate.
14648 $api->purge_cache( 'affiliate.json' );
14649
14650 self::shoot_ajax_success( $result );
14651 }
14652
14653 /**
14654 * @author Leo Fajardo (@leorw)
14655 * @since 1.2.3
14656 *
14657 * @return array|null
14658 */
14659 function get_affiliate_application_data() {
14660 if ( empty( $this->_storage->affiliate_application_data ) ) {
14661 return null;
14662 }
14663
14664 return $this->_storage->affiliate_application_data;
14665 }
14666
14667 #endregion Affiliation ------------------------------------------------------------
14668
14669 #----------------------------------------------------------------------------------
14670 #region URL Generators
14671 #----------------------------------------------------------------------------------
14672
14673 /**
14674 * Alias to pricing_url().
14675 *
14676 * @author Vova Feldman (@svovaf)
14677 * @since 1.0.2
14678 *
14679 * @uses pricing_url()
14680 *
14681 * @param string $period Billing cycle
14682 * @param bool $is_trial
14683 *
14684 * @return string
14685 */
14686 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14687 return $this->pricing_url( $period, $is_trial );
14688 }
14689
14690 /**
14691 * @author Vova Feldman (@svovaf)
14692 * @since 1.0.9
14693 *
14694 * @uses get_upgrade_url()
14695 *
14696 * @return string
14697 */
14698 function get_trial_url() {
14699 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
14700 }
14701
14702 /**
14703 * @author Leo Fajardo (@leorw)
14704 * @since 2.1.4
14705 *
14706 * @param string $new_version
14707 *
14708 * @return string
14709 */
14710 function version_upgrade_checkout_link( $new_version ) {
14711 if ( ! is_object( $this->_license ) ) {
14712 $url = $this->pricing_url();
14713
14714 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
14715 } else {
14716 $subscription = $this->_get_subscription( $this->_license->id );
14717
14718 $url = $this->checkout_url(
14719 is_object( $subscription ) ?
14720 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
14721 WP_FS__PERIOD_LIFETIME,
14722 false,
14723 array( 'licenses' => $this->_license->quota )
14724 );
14725
14726 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
14727 }
14728
14729 return sprintf(
14730 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
14731 sprintf(
14732 '<a href="%s">%s</a>',
14733 $this->apply_filters( 'update_notice_checkout_url', $url ),
14734 $purchase_license_text
14735 ),
14736 $new_version
14737 );
14738 }
14739
14740 /**
14741 * Plugin's pricing URL.
14742 *
14743 * @author Vova Feldman (@svovaf)
14744 * @since 1.0.4
14745 *
14746 * @param string $billing_cycle Billing cycle
14747 *
14748 * @param bool $is_trial
14749 *
14750 * @return string
14751 */
14752 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14753 $this->_logger->entrance();
14754
14755 $params = array(
14756 'billing_cycle' => $billing_cycle
14757 );
14758
14759 if ( $is_trial ) {
14760 $params['trial'] = 'true';
14761 }
14762
14763 $url = $this->is_addon() ?
14764 $this->_parent->addon_url( $this->_slug ) :
14765 $this->_get_admin_page_url( 'pricing', $params );
14766
14767 return $this->get_pricing_url_with_filter( $url );
14768 }
14769
14770 /**
14771 * Retrieves the filtered pricing URL.
14772 *
14773 * @author Leo Fajardo (@leorw)
14774 * @since 2.7.4
14775 *
14776 * @param string $url
14777 *
14778 * @return string
14779 */
14780 private function get_pricing_url_with_filter( $url ) {
14781 return $this->apply_filters( 'pricing_url', $url );
14782 }
14783
14784 /**
14785 * Checkout page URL.
14786 *
14787 * @author Vova Feldman (@svovaf)
14788 * @since 1.0.6
14789 *
14790 * @param string $billing_cycle Billing cycle
14791 * @param bool $is_trial
14792 * @param array $extra (optional) Extra parameters, override other query params.
14793 * @param bool|null $network
14794 *
14795 * @return string
14796 */
14797 function checkout_url(
14798 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14799 $is_trial = false,
14800 $extra = array(),
14801 $network = null
14802 ) {
14803 $this->_logger->entrance();
14804
14805 $params = array(
14806 'checkout' => 'true',
14807 'billing_cycle' => $billing_cycle,
14808 );
14809
14810 if ( $is_trial ) {
14811 $params['trial'] = 'true';
14812 }
14813
14814 /**
14815 * Params in extra override other params.
14816 */
14817 $params = array_merge( $params, $extra );
14818
14819 return $this->apply_filters( 'checkout_url', $this->_get_admin_page_url( 'pricing', $params, $network ) );
14820 }
14821
14822 /**
14823 * Add-on checkout URL.
14824 *
14825 * @author Vova Feldman (@svovaf)
14826 * @since 1.1.7
14827 *
14828 * @param number $addon_id
14829 * @param number $pricing_id
14830 * @param string $billing_cycle
14831 * @param bool $is_trial
14832 * @param bool|null $network
14833 *
14834 * @return string
14835 */
14836 function addon_checkout_url(
14837 $addon_id,
14838 $pricing_id,
14839 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14840 $is_trial = false,
14841 $network = null
14842 ) {
14843 return $this->checkout_url( $billing_cycle, $is_trial, array(
14844 'plugin_id' => $addon_id,
14845 'pricing_id' => $pricing_id,
14846 ), $network );
14847 }
14848
14849 #endregion
14850
14851 #endregion ------------------------------------------------------------------
14852
14853 /**
14854 * Check if plugin has any add-ons.
14855 *
14856 * @author Vova Feldman (@svovaf)
14857 * @since 1.0.5
14858 *
14859 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
14860 *
14861 * @return bool
14862 */
14863 function has_addons() {
14864 $this->_logger->entrance();
14865
14866 return $this->_has_addons;
14867 }
14868
14869 /**
14870 * Check if plugin can work in anonymous mode.
14871 *
14872 * @author Vova Feldman (@svovaf)
14873 * @since 1.0.9
14874 *
14875 * @return bool
14876 *
14877 * @deprecated Please use is_enable_anonymous() instead.
14878 */
14879 function enable_anonymous() {
14880 return $this->_enable_anonymous;
14881 }
14882
14883 /**
14884 * Check if plugin can work in anonymous mode.
14885 *
14886 * @author Vova Feldman (@svovaf)
14887 * @since 1.1.9
14888 *
14889 * @return bool
14890 */
14891 function is_enable_anonymous() {
14892 return $this->_enable_anonymous;
14893 }
14894
14895 /**
14896 * Check if plugin is premium only (no free plans).
14897 *
14898 * @author Vova Feldman (@svovaf)
14899 * @since 1.1.9
14900 *
14901 * @return bool
14902 */
14903 function is_only_premium() {
14904 return $this->_is_premium_only;
14905 }
14906
14907 /**
14908 * Checks if the plugin's type is "plugin". The other type is "theme".
14909 *
14910 * @author Leo Fajardo (@leorw)
14911 * @since 1.2.2
14912 *
14913 * @return bool
14914 */
14915 function is_plugin() {
14916 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
14917 }
14918
14919 /**
14920 * @author Leo Fajardo (@leorw)
14921 * @since 1.2.2
14922 *
14923 * @return string
14924 */
14925 function get_module_type() {
14926 if ( ! isset( $this->_module_type ) ) {
14927 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
14928 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
14929 }
14930
14931 return $this->_module_type;
14932 }
14933
14934 /**
14935 * @author Leo Fajardo (@leorw)
14936 * @since 1.2.2
14937 *
14938 * @return string
14939 */
14940 function get_plugin_main_file_path() {
14941 return $this->_plugin_main_file_path;
14942 }
14943
14944 /**
14945 * Check if module has a premium code version.
14946 *
14947 * Serviceware module might be freemium without any
14948 * premium code version, where the paid features
14949 * are all part of the service.
14950 *
14951 * @author Vova Feldman (@svovaf)
14952 * @since 1.2.1.6
14953 *
14954 * @return bool
14955 */
14956 function has_premium_version() {
14957 return $this->_has_premium_version;
14958 }
14959
14960 /**
14961 * Check if feature supported with current site's plan.
14962 *
14963 * @author Vova Feldman (@svovaf)
14964 * @since 1.0.1
14965 *
14966 * @todo IMPLEMENT
14967 *
14968 * @param number $feature_id
14969 *
14970 * @throws Exception
14971 */
14972 function is_feature_supported( $feature_id ) {
14973 throw new Exception( 'not implemented' );
14974 }
14975
14976 /**
14977 * @author Vova Feldman (@svovaf)
14978 * @since 1.0.1
14979 *
14980 * @return bool Is running in SSL/HTTPS
14981 */
14982 function is_ssl() {
14983 return WP_FS__IS_HTTPS;
14984 }
14985
14986 /**
14987 * @author Vova Feldman (@svovaf)
14988 * @since 1.0.9
14989 *
14990 * @return bool Is running in AJAX call.
14991 *
14992 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
14993 */
14994 static function is_ajax() {
14995 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
14996 }
14997
14998 /**
14999 * Check if it's an AJAX call targeted for the current module.
15000 *
15001 * @author Vova Feldman (@svovaf)
15002 * @since 1.2.0
15003 *
15004 * @param array|string $actions Collection of AJAX actions.
15005 *
15006 * @return bool
15007 */
15008 function is_ajax_action( $actions ) {
15009 // Verify it's an ajax call.
15010 if ( ! self::is_ajax() ) {
15011 return false;
15012 }
15013
15014 // Verify the call is relevant for the plugin.
15015 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
15016 return false;
15017 }
15018
15019 // Verify it's one of the specified actions.
15020 if ( is_string( $actions ) ) {
15021 $actions = explode( ',', $actions );
15022 }
15023
15024 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15025 $ajax_action = fs_request_get( 'action' );
15026
15027 foreach ( $actions as $action ) {
15028 if ( $ajax_action === $this->get_action_tag( $action ) ) {
15029 return true;
15030 }
15031 }
15032 }
15033
15034 return false;
15035 }
15036
15037 /**
15038 * Check if it's an AJAX call targeted for current request.
15039 *
15040 * @author Vova Feldman (@svovaf)
15041 * @since 1.2.0
15042 *
15043 * @param array|string $actions Collection of AJAX actions.
15044 * @param number|null $module_id
15045 *
15046 * @return bool
15047 */
15048 static function is_ajax_action_static( $actions, $module_id = null ) {
15049 // Verify it's an ajax call.
15050 if ( ! self::is_ajax() ) {
15051 return false;
15052 }
15053
15054
15055 if ( ! empty( $module_id ) ) {
15056 // Verify the call is relevant for the plugin.
15057 if ( $module_id != fs_request_get( 'module_id' ) ) {
15058 return false;
15059 }
15060 }
15061
15062 // Verify it's one of the specified actions.
15063 if ( is_string( $actions ) ) {
15064 $actions = explode( ',', $actions );
15065 }
15066
15067 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15068 $ajax_action = fs_request_get( 'action' );
15069
15070 foreach ( $actions as $action ) {
15071 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
15072 return true;
15073 }
15074 }
15075 }
15076
15077 return false;
15078 }
15079
15080 /**
15081 * @author Vova Feldman (@svovaf)
15082 * @since 1.1.7
15083 *
15084 * @return bool
15085 */
15086 static function is_cron() {
15087 return ( defined( 'DOING_CRON' ) && DOING_CRON );
15088 }
15089
15090 /**
15091 * @author Leo Fajardo (@leorw)
15092 * @since 2.5.0
15093 *
15094 * @return bool
15095 */
15096 static function is_admin_post() {
15097 return ( 'admin-post.php' === self::get_current_page() );
15098 }
15099
15100 /**
15101 * Check if a real user is visiting the admin dashboard.
15102 *
15103 * @author Vova Feldman (@svovaf)
15104 * @since 1.1.7
15105 *
15106 * @return bool
15107 */
15108 function is_user_in_admin() {
15109 return (
15110 is_admin() &&
15111 ! self::is_ajax() &&
15112 ! self::is_cron() &&
15113 ! self::is_admin_post()
15114 );
15115 }
15116
15117 /**
15118 * Check if a real user is in the customizer view.
15119 *
15120 * @author Vova Feldman (@svovaf)
15121 * @since 1.2.2.7
15122 *
15123 * @return bool
15124 */
15125 static function is_customizer() {
15126 return is_customize_preview();
15127 }
15128
15129 /**
15130 * Check if running in HTTPS and if site's plan matching the specified plan.
15131 *
15132 * @param string $plan
15133 * @param bool $exact
15134 *
15135 * @return bool
15136 */
15137 function is_ssl_and_plan( $plan, $exact = false ) {
15138 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
15139 }
15140
15141 /**
15142 * Construct plugin's settings page URL.
15143 *
15144 * @author Vova Feldman (@svovaf)
15145 * @since 1.0.4
15146 *
15147 * @param string $page
15148 * @param array $params
15149 * @param bool|null $network
15150 *
15151 * @return string
15152 */
15153 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
15154 if ( is_null( $network ) ) {
15155 $network = (
15156 $this->_is_network_active &&
15157 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
15158 );
15159 }
15160
15161 if ( 0 < count( $params ) ) {
15162 foreach ( $params as $k => $v ) {
15163 $params[ $k ] = urlencode( $v );
15164 }
15165 }
15166
15167 $page_param = $this->_menu->get_slug( $page );
15168
15169 if ( empty( $page ) &&
15170 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
15171 $this->show_opt_in_on_themes_page()
15172 ) {
15173 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
15174
15175 return add_query_arg(
15176 $params,
15177 $this->admin_url( 'themes.php', 'admin', $network )
15178 );
15179 }
15180
15181 if ( ! $this->has_settings_menu() ) {
15182 if ( ! empty( $page ) ) {
15183 // Module doesn't have a setting page, but since the request is for
15184 // a specific Freemius page, use the admin.php path.
15185 return add_query_arg( array_merge( $params, array(
15186 'page' => $page_param,
15187 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15188 } else {
15189 if ( $this->is_activation_mode() ) {
15190 /**
15191 * @author Vova Feldman
15192 * @since 1.2.1.6
15193 *
15194 * If plugin doesn't have a settings page, create one for the opt-in screen.
15195 */
15196 return add_query_arg( array_merge( $params, array(
15197 'page' => $this->_slug,
15198 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15199 } else {
15200 // Plugin without a settings page.
15201 return add_query_arg(
15202 $params,
15203 $this->admin_url( 'plugins.php', 'admin', $network )
15204 );
15205 }
15206 }
15207 }
15208
15209 // Module has a submenu settings page.
15210 if ( ! $this->_menu->is_top_level() ) {
15211 $parent_slug = $this->_menu->get_parent_slug();
15212 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
15213 $parent_slug :
15214 'admin.php';
15215
15216 return add_query_arg( array_merge( $params, array(
15217 'page' => $page_param,
15218 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
15219 }
15220
15221 // Module has a top level CPT settings page.
15222 if ( $this->_menu->is_cpt() ) {
15223 if ( empty( $page ) && $this->is_activation_mode() ) {
15224 return add_query_arg( array_merge( $params, array(
15225 'page' => $page_param
15226 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15227 } else {
15228 if ( ! empty( $page ) ) {
15229 $params['page'] = $page_param;
15230 }
15231
15232 return add_query_arg(
15233 $params,
15234 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
15235 );
15236 }
15237 }
15238
15239 // Module has a custom top level settings page.
15240 return add_query_arg( array_merge( $params, array(
15241 'page' => $page_param,
15242 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15243 }
15244
15245 #--------------------------------------------------------------------------------
15246 #region Multisite
15247 #--------------------------------------------------------------------------------
15248
15249 /**
15250 * @author Leo Fajardo (@leorw)
15251 * @since 2.0.0
15252 *
15253 * @return bool
15254 */
15255 function is_network_active() {
15256 return $this->_is_network_active;
15257 }
15258
15259 /**
15260 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
15261 *
15262 * @author Leo Fajardo (@leorw)
15263 * @since 2.0.0
15264 *
15265 * @param bool|int[] $all_or_blog_ids
15266 */
15267 private function delegate_connection( $all_or_blog_ids = true ) {
15268 $this->_logger->entrance();
15269
15270 $this->_admin_notices->remove_sticky( 'connect_account' );
15271
15272 if ( true === $all_or_blog_ids ) {
15273 // All sites delegation.
15274 $this->_storage->store( 'is_delegated_connection', true, true );
15275 } else {
15276 // Specified sites delegation.
15277 foreach ( $all_or_blog_ids as $blog_id ) {
15278 $this->delegate_site_connection( $blog_id );
15279 }
15280 }
15281
15282 $this->network_upgrade_mode_completed();
15283 }
15284
15285 /**
15286 * Delegate specific network site conncetion to the site admin.
15287 *
15288 * @author Vova Feldman (@svovaf)
15289 * @since 2.0.0
15290 *
15291 * @param int $blog_id
15292 */
15293 private function delegate_site_connection( $blog_id ) {
15294 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
15295 }
15296
15297 /**
15298 * Check if super-admin delegated the connection of ALL sites to the site admins.
15299 *
15300 * @author Vova Feldman (@svovaf)
15301 * @since 2.0.0
15302 *
15303 * @return bool
15304 */
15305 function is_network_delegated_connection() {
15306 if ( ! $this->_is_network_active ) {
15307 return false;
15308 }
15309
15310 return $this->_storage->get( 'is_delegated_connection', false, true );
15311 }
15312
15313 /**
15314 * @author Leo Fajardo (@leorw)
15315 * @since 2.0.0
15316 *
15317 * @param int $blog_id
15318 *
15319 * @return bool
15320 */
15321 function is_site_delegated_connection( $blog_id = 0 ) {
15322 if ( ! $this->_is_network_active ) {
15323 return false;
15324 }
15325
15326 if ( 0 == $blog_id ) {
15327 $blog_id = get_current_blog_id();
15328 }
15329
15330 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15331 }
15332
15333 /**
15334 * Check if delegated the connection. When running within the network admin,
15335 * and haven't specified the blog ID, checks if network level delegated. If running
15336 * within a site admin or specified a blog ID, check if delegated the connection for
15337 * the current context site.
15338 *
15339 * If executed outside the the admin, check if delegated the connection
15340 * for the current context site OR the whole network.
15341 *
15342 * @author Vova Feldman (@svovaf)
15343 * @since 2.0.0
15344 *
15345 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15346 *
15347 * @return bool
15348 */
15349 function is_delegated_connection( $blog_id = 0 ) {
15350 if ( ! $this->_is_network_active ) {
15351 return false;
15352 }
15353
15354 if ( fs_is_network_admin() && 0 == $blog_id ) {
15355 return $this->is_network_delegated_connection();
15356 }
15357
15358 return (
15359 $this->is_network_delegated_connection() ||
15360 $this->is_site_delegated_connection( $blog_id )
15361 );
15362 }
15363
15364 /**
15365 * Check if the current module is active for the site.
15366 *
15367 * @author Vova Feldman (@svovaf)
15368 * @since 2.0.0
15369 *
15370 * @param int $blog_id
15371 *
15372 * @return bool
15373 */
15374 function is_active_for_site( $blog_id ) {
15375 if ( ! is_multisite() ) {
15376 // Not a multisite and this code is executed, means that the plugin is active.
15377 return true;
15378 }
15379
15380 if ( $this->is_theme() ) {
15381 // All themes are site level activated.
15382 return true;
15383 }
15384
15385 if ( $this->_is_network_active ) {
15386 // Plugin was network activated so it's active.
15387 return true;
15388 }
15389
15390 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15391 }
15392
15393 /**
15394 * @todo Implement pagination when accessing the subsites collection.
15395 *
15396 * @author Leo Fajardo (@leorw)
15397 * @since 2.0.0
15398 *
15399 * @param int $limit Default to 1,000
15400 * @param int $offset Default to 0
15401 *
15402 * @return array Active & public sites collection.
15403 */
15404 static function get_sites( $limit = 1000, $offset = 0 ) {
15405 if ( ! is_multisite() ) {
15406 return array();
15407 }
15408
15409 /**
15410 * For consistency with get_blog_list() which only return active public sites.
15411 *
15412 * @author Vova Feldman (@svovaf)
15413 */
15414 $args = array(
15415 /**
15416 * Commented out in order to handle the migration of site options whether the site is public or not.
15417 *
15418 * @author Leo Fajardo (@leorw)
15419 * @since 2.2.1
15420 */
15421 // 'public' => 1,
15422 'archived' => 0,
15423 'mature' => 0,
15424 'spam' => 0,
15425 'deleted' => 0,
15426 'number' => $limit,
15427 'offset' => $offset,
15428 );
15429
15430 return get_sites( $args );
15431 }
15432
15433 /**
15434 * Checks if a given blog is active.
15435 *
15436 * @author Vova Feldman (@svovaf)
15437 * @since 2.0.0
15438 *
15439 * @param $blog_id
15440 *
15441 * @return bool
15442 */
15443 private static function is_site_active( $blog_id ) {
15444 global $wpdb;
15445
15446 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15447
15448 if ( ! is_object( $blog_info ) ) {
15449 return false;
15450 }
15451
15452 return (
15453 true == $blog_info->public &&
15454 false == $blog_info->archived &&
15455 false == $blog_info->mature &&
15456 false == $blog_info->spam &&
15457 false == $blog_info->deleted
15458 );
15459 }
15460
15461 /**
15462 * Get a mapping between the site addresses to their blog IDs.
15463 *
15464 * @author Vova Feldman (@svovaf)
15465 * @since 2.0.0
15466 *
15467 * @return array {
15468 * @key string Site address without protocol with a trailing slash.
15469 * @value int Site's blog ID.
15470 * }
15471 */
15472 private function get_address_to_blog_map() {
15473 $sites = self::get_sites();
15474
15475 // Map site addresses to their blog IDs.
15476 $address_to_blog_map = array();
15477 foreach ( $sites as $site ) {
15478 $blog_id = self::get_site_blog_id( $site );
15479 $address = self::get_unfiltered_site_url( $blog_id, true, true );
15480 $address_to_blog_map[ $address ] = $blog_id;
15481 }
15482
15483 return $address_to_blog_map;
15484 }
15485
15486 /**
15487 * Get a mapping between the site addresses to their blog IDs.
15488 *
15489 * @author Vova Feldman (@svovaf)
15490 * @since 2.0.0
15491 *
15492 * @return array {
15493 * @key int Site's blog ID.
15494 * @value FS_Site Associated install.
15495 * }
15496 */
15497 function get_blog_install_map() {
15498 $sites = self::get_sites();
15499
15500 // Map site blog ID to its install.
15501 $install_map = array();
15502
15503 foreach ( $sites as $site ) {
15504 $blog_id = self::get_site_blog_id( $site );
15505 $install = $this->get_install_by_blog_id( $blog_id );
15506
15507 if ( is_object( $install ) ) {
15508 $install_map[ $blog_id ] = $install;
15509 }
15510 }
15511
15512 return $install_map;
15513 }
15514
15515 /**
15516 * @author Vova Feldman (@svovaf)
15517 * @since 2.5.1
15518 *
15519 * @param bool|null $is_delegated When `true`, returns only connection delegated blog IDs. When `false`, only non-delegated blog IDs.
15520 *
15521 * @return int[]
15522 */
15523 private function get_blog_ids( $is_delegated = null ) {
15524 $blog_ids = array();
15525
15526 $sites = self::get_sites();
15527 foreach ( $sites as $site ) {
15528 $blog_id = self::get_site_blog_id( $site );
15529
15530 if (
15531 is_null( $is_delegated ) ||
15532 $is_delegated === $this->is_site_delegated_connection( $blog_id )
15533 ) {
15534 $blog_ids[] = $blog_id;
15535 }
15536 }
15537
15538 return $blog_ids;
15539 }
15540
15541 /**
15542 * @author Vova Feldman (@svovaf)
15543 * @since 2.5.1
15544 *
15545 * @return int[]
15546 */
15547 private function get_non_delegated_blog_ids() {
15548 return $this->get_blog_ids( false );
15549 }
15550
15551 /**
15552 * Gets a map of module IDs that the given user has opted-in to.
15553 *
15554 * @author Leo Fajardo (@leorw)
15555 * @since 2.1.0
15556 *
15557 * @param number $fs_user_id
15558 *
15559 * @return array {
15560 * @key number $plugin_id
15561 * @value bool Always true.
15562 * }
15563 */
15564 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15565 self::$_static_logger->entrance();
15566
15567 if ( ! is_multisite() ) {
15568 $installs = array_merge(
15569 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15570 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15571 );
15572 } else {
15573 $sites = self::get_sites();
15574
15575 $installs = array();
15576 foreach ( $sites as $site ) {
15577 $blog_id = self::get_site_blog_id( $site );
15578
15579 $installs = array_merge(
15580 $installs,
15581 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15582 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15583 );
15584 }
15585 }
15586
15587 $module_ids_map = array();
15588 foreach ( $installs as $install ) {
15589 if ( is_object( $install ) &&
15590 FS_Site::is_valid_id( $install->id ) &&
15591 FS_User::is_valid_id( $install->user_id ) &&
15592 ( $install->user_id == $fs_user_id )
15593 ) {
15594 $module_ids_map[ $install->plugin_id ] = true;
15595 }
15596 }
15597
15598 return $module_ids_map;
15599 }
15600
15601 /**
15602 * @author Leo Fajardo (@leorw)
15603 *
15604 * @return null|array {
15605 * 'install' => FS_Site Module's install,
15606 * 'blog_id' => string The associated blog ID.
15607 * }
15608 */
15609 function find_first_install() {
15610 $sites = self::get_sites();
15611
15612 foreach ( $sites as $site ) {
15613 $blog_id = self::get_site_blog_id( $site );
15614 $install = $this->get_install_by_blog_id( $blog_id );
15615
15616 if ( is_object( $install ) ) {
15617 return array(
15618 'install' => $install,
15619 'blog_id' => $blog_id
15620 );
15621 }
15622 }
15623
15624 return null;
15625 }
15626
15627 /**
15628 * Switches the Freemius site level context to a specified blog.
15629 *
15630 * @author Vova Feldman (@svovaf)
15631 * @since 2.0.0
15632 *
15633 * @param int $blog_id
15634 * @param FS_Site $install
15635 * @param bool $flush
15636 *
15637 * @return bool Since 2.3.1 returns if a switch was made.
15638 */
15639 function switch_to_blog( $blog_id, $install = null, $flush = false ) {
15640 if ( ! is_numeric( $blog_id ) ) {
15641 return false;
15642 }
15643
15644 if ( ! $flush && $blog_id == $this->_context_is_network_or_blog_id ) {
15645 return false;
15646 }
15647
15648 switch_to_blog( $blog_id );
15649 $this->_context_is_network_or_blog_id = $blog_id;
15650
15651 self::$_accounts->set_site_blog_context( $blog_id );
15652 $this->_storage->set_site_blog_context( $blog_id );
15653 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
15654
15655 $this->_site = is_object( $install ) ?
15656 $install :
15657 $this->get_install_by_blog_id( $blog_id );
15658
15659 $this->_user = false;
15660 $this->_licenses = false;
15661 $this->_license = null;
15662 $this->is_whitelabeled = null;
15663
15664 if ( is_object( $this->_site ) ) {
15665 // Try to fetch user from install.
15666 $this->_user = self::_get_user_by_id( $this->_site->user_id );
15667
15668 if ( ! is_object( $this->_user ) &&
15669 FS_User::is_valid_id( $this->_storage->prev_user_id )
15670 ) {
15671 // Try to fetch previously saved user.
15672 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
15673
15674 if ( ! is_object( $this->_user ) ) {
15675 // Fallback to network's user.
15676 $this->_user = $this->get_network_user();
15677 }
15678 }
15679
15680 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
15681
15682 if ( ! empty( $all_plugin_licenses ) ) {
15683 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
15684 $this->_license = null;
15685 } else {
15686 $license_found = false;
15687 foreach ( $all_plugin_licenses as $license ) {
15688 if ( $license->id == $this->_site->license_id ) {
15689 // License found.
15690 $this->_license = $license;
15691 $license_found = true;
15692 break;
15693 }
15694 }
15695
15696 if ( $license_found ) {
15697 $this->link_license_2_user( $this->_license->id, $this->_user->id );
15698 }
15699 }
15700
15701 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15702 }
15703 }
15704
15705 unset( $this->_site_api );
15706 unset( $this->_user_api );
15707
15708 return true;
15709 }
15710
15711 /**
15712 * Restore the blog context to the blog that originally loaded the module.
15713 *
15714 * @author Vova Feldman (@svovaf)
15715 * @since 2.0.0
15716 */
15717 function restore_current_blog() {
15718 $this->switch_to_blog( $this->_blog_id );
15719 }
15720
15721 /**
15722 * @author Vova Feldman (@svovaf)
15723 * @since 2.0.0
15724 *
15725 * @param array|WP_Site $site
15726 *
15727 * @return int
15728 */
15729 static function get_site_blog_id( &$site ) {
15730 return ( $site instanceof WP_Site ) ?
15731 $site->blog_id :
15732 ( is_object( $site ) && isset( $site->userblog_id ) ?
15733 $site->userblog_id :
15734 $site['blog_id'] );
15735 }
15736
15737 /**
15738 * @author Vova Feldman (@svovaf)
15739 * @since 2.5.1
15740 *
15741 * @param WP_Site[]|array[] $sites
15742 *
15743 * @return int[]
15744 */
15745 static function get_sites_blog_ids( $sites ) {
15746 $blog_ids = array();
15747 foreach ( $sites as $site ) {
15748 $blog_ids[] = self::get_site_blog_id( $site );
15749 }
15750
15751 return $blog_ids;
15752 }
15753
15754 /**
15755 * @author Leo Fajardo (@leorw)
15756 * @since 2.0.0
15757 *
15758 * @param array|WP_Site|null $site
15759 * @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.
15760 *
15761 * @return array
15762 */
15763 function get_site_info( $site = null, $load_registration = false ) {
15764 $this->_logger->entrance();
15765
15766 $fs_hook_snapshot = new FS_Hook_Snapshot();
15767 // Remove all filters from `switch_blog`.
15768 $fs_hook_snapshot->remove( 'switch_blog' );
15769
15770 $switched = false;
15771
15772 $registration_date = null;
15773
15774 if ( is_null( $site ) ) {
15775 $url = self::get_unfiltered_site_url();
15776 $name = get_bloginfo( 'name' );
15777 $blog_id = null;
15778 } else {
15779 $blog_id = self::get_site_blog_id( $site );
15780
15781 if ( get_current_blog_id() != $blog_id ) {
15782 switch_to_blog( $blog_id );
15783 $switched = true;
15784 }
15785
15786 if ( $site instanceof WP_Site ) {
15787 $url = $site->siteurl;
15788 $name = $site->blogname;
15789 $registration_date = $site->registered;
15790 } else {
15791 $url = self::get_unfiltered_site_url( $blog_id );
15792 $name = get_bloginfo( 'name' );
15793 }
15794 }
15795
15796 if ( empty( $registration_date ) && $load_registration ) {
15797 $blog_details = get_blog_details( $blog_id, false );
15798
15799 if ( is_object( $blog_details ) && isset( $blog_details->registered ) ) {
15800 $registration_date = $blog_details->registered;
15801 }
15802 }
15803
15804 $info = array(
15805 'uid' => $this->get_anonymous_id( $blog_id ),
15806 'url' => $url,
15807 );
15808
15809 // Add these diagnostic information only if user allowed to track.
15810 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
15811 $info = array_merge( $info, array(
15812 'title' => $name,
15813 'language' => self::get_sanitized_language(),
15814 ) );
15815 }
15816
15817 if ( is_numeric( $blog_id ) ) {
15818 $info['blog_id'] = $blog_id;
15819 }
15820
15821 if ( ! empty( $registration_date ) ) {
15822 $info[ 'registration_date' ] = $registration_date;
15823 }
15824
15825 if ( $switched ) {
15826 restore_current_blog();
15827 }
15828
15829 // Add the filters back to `switch_blog`.
15830 $fs_hook_snapshot->restore( 'switch_blog' );
15831
15832 return $info;
15833 }
15834
15835 /**
15836 * Load the module's install based on the blog ID.
15837 *
15838 * @author Vova Feldman (@svovaf)
15839 * @since 2.0.0
15840 *
15841 * @param int|null $blog_id
15842 *
15843 * @return FS_Site
15844 */
15845 function get_install_by_blog_id( $blog_id = null ) {
15846 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15847 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15848
15849 if ( is_object( $install ) &&
15850 is_numeric( $install->id ) &&
15851 is_numeric( $install->user_id ) &&
15852 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15853 ) {
15854 // Load site.
15855 $install = clone $install;
15856 }
15857
15858 return $install;
15859 }
15860
15861 /**
15862 * Check if module is installed on a specified site.
15863 *
15864 * @author Vova Feldman (@svovaf)
15865 * @since 2.0.0
15866 *
15867 * @param int|null $blog_id
15868 *
15869 * @return bool
15870 */
15871 function is_installed_on_site( $blog_id = null ) {
15872 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15873 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15874
15875 return (
15876 is_object( $install ) &&
15877 is_numeric( $install->id ) &&
15878 is_numeric( $install->user_id ) &&
15879 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15880 );
15881 }
15882
15883 /**
15884 * Check if super-admin connected at least one site via the network opt-in.
15885 *
15886 * @author Vova Feldman (@svovaf)
15887 * @since 2.0.0
15888 *
15889 * @return bool
15890 */
15891 function is_network_registered() {
15892 if ( ! $this->_is_network_active ) {
15893 return false;
15894 }
15895
15896 return FS_User::is_valid_id( $this->_storage->network_user_id );
15897 }
15898
15899 /**
15900 * Returns the main user associated with the network.
15901 *
15902 * @author Vova Feldman (@svovaf)
15903 * @since 2.0.0
15904 *
15905 * @return FS_User
15906 */
15907 function get_network_user() {
15908 if ( ! $this->_is_network_active ) {
15909 return null;
15910 }
15911
15912 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
15913 self::_get_user_by_id( $this->_storage->network_user_id ) :
15914 null;
15915 }
15916
15917 /**
15918 * Returns the current context user or the network's main user.
15919 *
15920 * @author Vova Feldman (@svovaf)
15921 * @since 2.0.0
15922 *
15923 * @return FS_User
15924 */
15925 function get_current_or_network_user() {
15926 return ( $this->_user instanceof FS_User ) ?
15927 $this->_user :
15928 $this->get_network_user();
15929 }
15930
15931 /**
15932 * Returns the main install associated with the network.
15933 *
15934 * @author Vova Feldman (@svovaf)
15935 * @since 2.0.0
15936 *
15937 * @return FS_Site
15938 */
15939 function get_network_install() {
15940 if ( ! $this->_is_network_active ) {
15941 return null;
15942 }
15943
15944 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15945 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
15946 null;
15947 }
15948
15949 /**
15950 * Returns the blog ID that is associated with the main install.
15951 *
15952 * @author Leo Fajardo (@leorw)
15953 * @since 2.0.0
15954 *
15955 * @return int|null
15956 */
15957 function get_network_install_blog_id() {
15958 if ( ! $this->_is_network_active ) {
15959 return null;
15960 }
15961
15962 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15963 $this->_storage->network_install_blog_id :
15964 null;
15965 }
15966
15967 /**
15968 * Returns the current context install or the network's main install.
15969 *
15970 * @author Vova Feldman (@svovaf)
15971 * @since 2.0.0
15972 *
15973 * @return FS_Site
15974 */
15975 function get_current_or_network_install() {
15976 return ( $this->_site instanceof FS_Site ) ?
15977 $this->_site :
15978 $this->get_network_install();
15979 }
15980
15981 /**
15982 * Check if executing a site level action from the network level admin.
15983 *
15984 * @author Vova Feldman (@svovaf)
15985 * @since 2.0.0
15986 *
15987 * @return false|int If yes, return the requested blog ID.
15988 */
15989 private function is_network_level_site_specific_action() {
15990 if ( ! $this->_is_network_active ) {
15991 return false;
15992 }
15993
15994 if ( ! fs_is_network_admin() ) {
15995 return false;
15996 }
15997
15998 $blog_id = fs_request_get( 'blog_id', '' );
15999
16000 return is_numeric( $blog_id ) ? $blog_id : false;
16001 }
16002
16003 /**
16004 * Check if executing an action from the network level admin.
16005 *
16006 * @author Vova Feldman (@svovaf)
16007 * @since 2.0.0
16008 *
16009 * @return bool
16010 */
16011 private function is_network_level_action() {
16012 return ( $this->_is_network_active && fs_is_network_admin() );
16013 }
16014
16015 /**
16016 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
16017 * 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.
16018 *
16019 * @author Vova Feldman (@svovaf)
16020 * @since 2.0.0
16021 *
16022 * @param int $context_blog_id
16023 */
16024 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
16025 $this->_logger->entrance();
16026
16027 if ( $this->_is_network_active ) {
16028 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
16029 $installs_map = $this->get_blog_install_map();
16030
16031 foreach ( $installs_map as $blog_id => $install ) {
16032 /**
16033 * @var FS_Site $install
16034 */
16035 if ( $context_blog_id == $blog_id ) {
16036 continue;
16037 }
16038
16039 if ( $install->user_id != $this->_storage->network_user_id ) {
16040 continue;
16041 }
16042
16043 // Switch reference to a blog that is opted-in and belong to the same super-admin.
16044 $this->_storage->network_install_blog_id = $blog_id;
16045 break;
16046 }
16047 }
16048 }
16049
16050 if ( ! $this->is_registered() ) {
16051 return;
16052 }
16053
16054 if ( $this->is_sync_cron_scheduled() &&
16055 $context_blog_id == $this->get_sync_cron_blog_id()
16056 ) {
16057 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
16058 }
16059
16060 if ( $this->is_install_sync_scheduled() &&
16061 $context_blog_id == $this->get_install_sync_cron_blog_id()
16062 ) {
16063 $this->maybe_schedule_install_sync_cron( $context_blog_id );
16064 }
16065 }
16066
16067 /**
16068 * Executed after site deactivation, archive, or flag as spam.
16069 *
16070 * @author Vova Feldman (@svovaf)
16071 * @since 2.0.0
16072 *
16073 * @param int $context_blog_id
16074 */
16075 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
16076 $this->_logger->entrance();
16077
16078 $install = $this->get_install_by_blog_id( $context_blog_id );
16079
16080 if ( ! is_object( $install ) ) {
16081 // Site not connected.
16082 return;
16083 }
16084
16085 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16086
16087 if ( ! $this->is_registered() ) {
16088 return;
16089 }
16090
16091 $current_blog_id = get_current_blog_id();
16092
16093 $this->switch_to_blog( $context_blog_id );
16094
16095 // Send deactivation event.
16096 $this->sync_install( array(
16097 'is_active' => false,
16098 ) );
16099
16100 $this->switch_to_blog( $current_blog_id );
16101 }
16102
16103 /**
16104 * Executed after site deletion.
16105 *
16106 * @author Vova Feldman (@svovaf)
16107 * @since 2.0.0
16108 *
16109 * @param int $context_blog_id
16110 * @param bool $drop True if site's database tables should be dropped. Default is false.
16111 */
16112 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
16113 $this->_logger->entrance();
16114
16115 $install = $this->get_install_by_blog_id( $context_blog_id );
16116
16117 if ( ! is_object( $install ) ) {
16118 // Site not connected.
16119 return;
16120 }
16121
16122 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16123
16124 if ( ! $this->is_registered() ) {
16125 return;
16126 }
16127
16128 $current_blog_id = get_current_blog_id();
16129
16130 $this->switch_to_blog( $context_blog_id );
16131
16132 if ( $drop ) {
16133 // Delete install if dropping site DB.
16134 $this->delete_account_event();
16135 } else {
16136 // Send deactivation event.
16137 $this->sync_install( array(
16138 'is_active' => false,
16139 ) );
16140 }
16141
16142 $this->switch_to_blog( $current_blog_id );
16143 }
16144
16145 /**
16146 * Executed after site deletion, called from wp_delete_site
16147 *
16148 * @author Dario Curvino (@dudo)
16149 * @since 2.5.0
16150 *
16151 * @param WP_Site $old_site
16152 */
16153 public function _after_wpsite_deleted_callback( WP_Site $old_site ) {
16154 $this->_logger->entrance();
16155
16156 $this->_after_site_deleted_callback( $old_site->blog_id, true );
16157 }
16158
16159 /**
16160 * Executed after site re-activation.
16161 *
16162 * @author Vova Feldman (@svovaf)
16163 * @since 2.0.0
16164 *
16165 * @param int $context_blog_id
16166 */
16167 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
16168 $this->_logger->entrance();
16169
16170 $install = $this->get_install_by_blog_id( $context_blog_id );
16171
16172 if ( ! is_object( $install ) ) {
16173 // Site not connected.
16174 return;
16175 }
16176
16177 if ( ! self::is_site_active( $context_blog_id ) ) {
16178 // Site not yet active (can be in spam mode, archived, deleted...).
16179 return;
16180 }
16181
16182 $current_blog_id = get_current_blog_id();
16183
16184 $this->switch_to_blog( $context_blog_id );
16185
16186 // Send re-activation event.
16187 $this->sync_install( array(
16188 'is_active' => true,
16189 ) );
16190
16191 $this->switch_to_blog( $current_blog_id );
16192 }
16193
16194 #endregion Multisite
16195
16196 /**
16197 * @author Leo Fajardo (@leorw)
16198 *
16199 * @param string $path
16200 * @param string $scheme
16201 * @param bool $network
16202 *
16203 * @return string
16204 */
16205 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
16206 return ( $this->_is_network_active && $network ) ?
16207 network_admin_url( $path, $scheme ) :
16208 admin_url( $path, $scheme );
16209 }
16210
16211 /**
16212 * Check if currently in a specified admin page.
16213 *
16214 * @author Vova Feldman (@svovaf)
16215 * @since 1.2.2.7
16216 *
16217 * @param string $page
16218 *
16219 * @return bool
16220 */
16221 function is_admin_page( $page ) {
16222 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
16223 }
16224
16225 /**
16226 * Check if currently in the product's main admin page.
16227 *
16228 * @author Vova Feldman (@svovaf)
16229 * @since 2.3.1
16230 *
16231 * @return bool
16232 */
16233 function is_main_admin_page() {
16234 return $this->is_admin_page( '' );
16235 }
16236
16237 /**
16238 * Get module's main admin setting page URL.
16239 *
16240 * @author Vova Feldman (@svovaf)
16241 * @since 1.2.2.7
16242 *
16243 * @return string
16244 */
16245 function main_menu_url() {
16246 return $this->_menu->main_menu_url();
16247 }
16248
16249 /**
16250 * Check if currently on the theme's setting page or
16251 * on any of the Freemius added pages (via tabs).
16252 *
16253 * @author Vova Feldman (@svovaf)
16254 * @since 1.2.2.7
16255 *
16256 * @return bool
16257 *
16258 * @deprecated Please use is_product_settings_page() instead;
16259 */
16260 function is_theme_settings_page() {
16261 return $this->is_product_settings_page();
16262 }
16263
16264 /**
16265 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
16266 *
16267 * @author Vova Feldman (@svovaf)
16268 * @since 1.2.2.7
16269 *
16270 * @return bool
16271 */
16272 function is_product_settings_page() {
16273 $page = fs_request_get( 'page', '', 'get' );
16274 $menu_slug = $this->_menu->get_slug();
16275
16276 if ( $page === $menu_slug ) {
16277 return true;
16278 }
16279
16280 return fs_starts_with(
16281 // e.g., {$menu_slug}-account, {$menu_slug}-affiliation, etc.
16282 $page,
16283 ( $menu_slug . '-' )
16284 );
16285 }
16286
16287 /**
16288 * Plugin's account page + sync license URL.
16289 *
16290 * @author Vova Feldman (@svovaf)
16291 * @since 1.1.9.1
16292 *
16293 * @param bool|number $plugin_id
16294 * @param bool $add_action_nonce
16295 * @param array $params
16296 *
16297 * @return string
16298 */
16299 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
16300 if ( is_numeric( $plugin_id ) ) {
16301 $params['plugin_id'] = $plugin_id;
16302 }
16303
16304 return $this->get_account_url(
16305 $this->get_unique_affix() . '_sync_license',
16306 $params,
16307 $add_action_nonce
16308 );
16309 }
16310
16311 /**
16312 * Plugin's account URL.
16313 *
16314 * @author Vova Feldman (@svovaf)
16315 * @since 1.0.4
16316 *
16317 * @param bool|string $action
16318 * @param array $params
16319 *
16320 * @param bool $add_action_nonce
16321 *
16322 * @return string
16323 */
16324 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
16325 if ( is_string( $action ) ) {
16326 $params['fs_action'] = $action;
16327 }
16328
16329 self::require_pluggable_essentials();
16330
16331 return ( $add_action_nonce && is_string( $action ) ) ?
16332 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
16333 $this->_get_admin_page_url( 'account', $params );
16334 }
16335
16336 /**
16337 * @author Vova Feldman (@svovaf)
16338 * @since 1.2.0
16339 *
16340 * @param string $tab
16341 * @param bool $action
16342 * @param array $params
16343 * @param bool $add_action_nonce
16344 *
16345 * @return string
16346 *
16347 * @uses get_account_url()
16348 */
16349 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
16350 $params['tab'] = $tab;
16351
16352 return $this->get_account_url( $action, $params, $add_action_nonce );
16353 }
16354
16355 /**
16356 * Plugin's account URL.
16357 *
16358 * @author Vova Feldman (@svovaf)
16359 * @since 1.0.4
16360 *
16361 * @param bool|string $topic
16362 * @param bool|string $message
16363 * @param bool|string $summary Since 2.5.1.
16364 *
16365 * @return string
16366 */
16367 function contact_url( $topic = false, $message = false, $summary = false ) {
16368 $params = array();
16369 if ( is_string( $topic ) ) {
16370 $params['topic'] = $topic;
16371 }
16372 if ( is_string( $message ) ) {
16373 $params['message'] = $message;
16374 }
16375
16376 if ( is_string( $summary ) ) {
16377 $params['summary'] = $summary;
16378 }
16379
16380 if ( $this->is_addon() ) {
16381 $params['addon_id'] = $this->get_id();
16382
16383 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
16384 } else {
16385 return $this->_get_admin_page_url( 'contact', $params );
16386 }
16387 }
16388
16389 /**
16390 * Add-on direct info URL.
16391 *
16392 * @author Vova Feldman (@svovaf)
16393 * @since 1.1.0
16394 *
16395 * @param string $slug
16396 *
16397 * @return string
16398 */
16399 function addon_url( $slug ) {
16400 return $this->_get_admin_page_url( 'addons', array(
16401 'slug' => $slug
16402 ) );
16403 }
16404
16405 /**
16406 * Add-ons URL.
16407 *
16408 * @author Vova Feldman (@svovaf)
16409 * @since 2.4.5
16410 *
16411 * @return string
16412 */
16413 function get_addons_url() {
16414 return $this->_get_admin_page_url( 'addons' );
16415 }
16416
16417 /* Logger
16418 ------------------------------------------------------------------------------------------------------------------*/
16419 /**
16420 * @param string $id
16421 * @param bool $prefix_slug
16422 *
16423 * @return FS_Logger
16424 */
16425 function get_logger( $id = '', $prefix_slug = true ) {
16426 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16427 }
16428
16429 /**
16430 * Note: This method is used externally so don't delete it.
16431 *
16432 * @param $id
16433 * @param bool $load_options
16434 * @param bool $prefix_slug
16435 *
16436 * @return FS_Option_Manager
16437 */
16438 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16439 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16440 }
16441
16442 /* Security
16443 ------------------------------------------------------------------------------------------------------------------*/
16444 private static function _encrypt( $str ) {
16445 if ( is_null( $str ) ) {
16446 return null;
16447 }
16448
16449 /**
16450 * The encrypt/decrypt functions are used to protect
16451 * the user from messing up with some of the sensitive
16452 * data stored for the module as a JSON in the database.
16453 *
16454 * I used the same suggested hack by the theme review team.
16455 * For more details, look at the function `Base64UrlDecode()`
16456 * in `./sdk/FreemiusBase.php`.
16457 *
16458 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16459 *
16460 * @author Vova Feldman (@svovaf)
16461 * @since 1.2.2
16462 */
16463 $fn = 'base64' . '_encode';
16464
16465 return $fn( $str );
16466 }
16467
16468 static function _decrypt( $str ) {
16469 if ( is_null( $str ) ) {
16470 return null;
16471 }
16472
16473 /**
16474 * The encrypt/decrypt functions are used to protect
16475 * the user from messing up with some of the sensitive
16476 * data stored for the module as a JSON in the database.
16477 *
16478 * I used the same suggested hack by the theme review team.
16479 * For more details, look at the function `Base64UrlDecode()`
16480 * in `./sdk/FreemiusBase.php`.
16481 *
16482 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16483 *
16484 * @author Vova Feldman (@svovaf)
16485 * @since 1.2.2
16486 */
16487 $fn = 'base64' . '_decode';
16488
16489 return $fn( $str );
16490 }
16491
16492 /**
16493 * @author Vova Feldman (@svovaf)
16494 * @since 1.0.5
16495 *
16496 * @param FS_Entity $entity
16497 *
16498 * @return FS_Entity Return an encrypted clone entity.
16499 */
16500 private static function _encrypt_entity( FS_Entity $entity ) {
16501 $clone = clone $entity;
16502 $props = get_object_vars( $entity );
16503
16504 foreach ( $props as $key => $val ) {
16505 $clone->{$key} = self::_encrypt( $val );
16506 }
16507
16508 return $clone;
16509 }
16510
16511 /**
16512 * @author Vova Feldman (@svovaf)
16513 * @since 1.0.5
16514 *
16515 * @param FS_Entity $entity
16516 *
16517 * @return FS_Entity Return an decrypted clone entity.
16518 */
16519 private static function decrypt_entity( FS_Entity $entity ) {
16520 $clone = clone $entity;
16521 $props = get_object_vars( $entity );
16522
16523 foreach ( $props as $key => $val ) {
16524 $clone->{$key} = self::_decrypt( $val );
16525 }
16526
16527 return $clone;
16528 }
16529
16530 /**
16531 * @author Vova Feldman (@svovaf)
16532 * @since 1.0.7
16533 *
16534 * @param string $email
16535 *
16536 * @return FS_User|false
16537 */
16538 public static function _get_user_by_email( $email ) {
16539 self::$_static_logger->entrance();
16540
16541 $email = trim( strtolower( $email ) );
16542
16543 $users = self::get_all_users();
16544
16545 if ( is_array( $users ) ) {
16546 foreach ( $users as $user ) {
16547 if ( $email === trim( strtolower( $user->email ) ) ) {
16548 return $user;
16549 }
16550 }
16551 }
16552
16553 return false;
16554 }
16555
16556 #----------------------------------------------------------------------------------
16557 #region Account (Loading, Updates & Activation)
16558 #----------------------------------------------------------------------------------
16559
16560 /***
16561 * Load account information (user + site).
16562 *
16563 * @author Vova Feldman (@svovaf)
16564 * @since 1.0.1
16565 */
16566 private function _load_account() {
16567 $this->_logger->entrance();
16568
16569 $this->do_action( 'before_account_load' );
16570
16571 $users = self::get_all_users();
16572 $plans = self::get_all_plans( $this->_module_type );
16573
16574 if ( $this->_logger->is_on() && is_admin() ) {
16575 $this->_logger->log( 'users = ' . var_export( $users, true ) );
16576 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
16577 }
16578
16579 $site = fs_is_network_admin() ?
16580 $this->get_network_install() :
16581 $this->get_install_by_blog_id();
16582
16583 if ( fs_is_network_admin() &&
16584 $this->is_network_active() &&
16585 ! is_object( $site ) &&
16586 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
16587 ) {
16588 $first_install = $this->find_first_install();
16589
16590 if ( is_null( $first_install ) ) {
16591 unset( $this->_storage->network_install_blog_id );
16592 } else {
16593 $site = $first_install['install'];
16594 $this->_storage->network_install_blog_id = $first_install['blog_id'];
16595 }
16596 }
16597
16598 if ( is_object( $site ) &&
16599 is_numeric( $site->id ) &&
16600 is_numeric( $site->user_id ) &&
16601 FS_Plugin_Plan::is_valid_id( $site->plan_id )
16602 ) {
16603 // Load site.
16604 $this->_site = $site;
16605 }
16606
16607 $user = null;
16608 if ( fs_is_network_admin() && $this->_is_network_active ) {
16609 $user = $this->get_network_user();
16610 }
16611
16612 if ( is_object( $user ) ) {
16613 $this->_user = clone $user;
16614 } else if ( $this->_site ) {
16615 $user = self::_get_user_by_id( $this->_site->user_id );
16616
16617 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
16618 /**
16619 * Try to load the previous owner. This recovery is used for the following use-case:
16620 * 1. Opt-in
16621 * 2. Cloning site1 to site2
16622 * 3. Ownership switch in site1 (same applies for site2)
16623 * 4. Install data sync on site2
16624 * 5. Now site2's install is associated with the new owner which does not exists locally.
16625 */
16626 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
16627 }
16628
16629 if ( ! is_object( $user ) ) {
16630 /**
16631 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
16632 */
16633 if (
16634 ! isset( $this->_storage->user_recovery_from_install_last_attempt_timestamp ) ||
16635 time() > ( $this->_storage->user_recovery_from_install_last_attempt_timestamp + FS_Clone_Manager::CLONE_RESOLUTION_MAX_EXECUTION_TIME )
16636 ) {
16637 $user = $this->sync_user_by_current_install();
16638 } else {
16639 return;
16640 }
16641
16642 if ( is_object( $user ) ) {
16643 $this->_storage->user_was_recovered_from_install = true;
16644 } else {
16645 $this->_storage->user_recovery_from_install_attempts = isset( $this->_storage->user_recovery_from_install_attempts ) ?
16646 ( $this->_storage->user_recovery_from_install_attempts + 1 ) :
16647 1;
16648
16649 if ( $this->_storage->user_recovery_from_install_attempts >= 3 ) {
16650 $this->delete_current_install( false );
16651 } else {
16652 $this->_storage->user_recovery_from_install_last_attempt_timestamp = time();
16653
16654 return;
16655 }
16656 }
16657 }
16658
16659 $this->_user = ( $user instanceof FS_User ) ?
16660 clone $user :
16661 null;
16662 }
16663
16664 if ( is_object( $this->_user ) ) {
16665 // Load licenses.
16666 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16667 }
16668
16669 if ( is_object( $this->_site ) ) {
16670 // Load plans.
16671 $this->_plans = isset( $plans[ $this->_slug ] ) ?
16672 $plans[ $this->_slug ] :
16673 array();
16674
16675 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
16676 $this->_sync_plans();
16677 } else {
16678 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16679 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
16680 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
16681 } else {
16682 unset( $this->_plans[ $i ] );
16683 }
16684 }
16685 }
16686
16687 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
16688
16689 if ( $this->_site->version != $this->get_plugin_version() ) {
16690 // If stored install version is different than current installed plugin version,
16691 // then update plugin version event.
16692 $this->update_plugin_version_event();
16693 }
16694 }
16695
16696 if ( true === $this->_storage->require_license_activation &&
16697 ! fs_request_get_bool( 'require_license', true )
16698 ) {
16699 $this->_storage->require_license_activation = false;
16700 }
16701
16702 if ( $this->is_theme() ) {
16703 $this->_register_account_hooks();
16704 }
16705
16706 if ( $this->is_user_in_admin() && $this->is_clone() ) {
16707 if ( empty( FS_Clone_Manager::instance()->get_clone_identification_timestamp() ) ) {
16708 FS_Clone_Manager::instance()->store_clone_identification_timestamp();
16709 }
16710
16711 FS_Clone_Manager::instance()->maybe_update_clone_resolution_support_flag( $this->_storage->sdk_last_version );
16712 $this->send_pending_clone_update_once();
16713 }
16714 }
16715
16716 /**
16717 * Special user recovery mechanism.
16718 *
16719 * @author Vova Feldman (@svovaf)
16720 * @since 2.0.0
16721 *
16722 * @param number|null $site_user_id
16723 *
16724 * @return \FS_User|mixed
16725 */
16726 private function sync_user_by_current_install( $site_user_id = null ) {
16727 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
16728 $site_user_id :
16729 $this->_site->user_id;
16730
16731 $api = $this->get_api_site_scope();
16732
16733 $uid = $this->get_anonymous_id();
16734 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
16735
16736 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
16737
16738 if ( $this->is_api_result_entity( $result ) ) {
16739 $user = new FS_User( $result );
16740 $this->_user = $user;
16741 $this->_store_user();
16742
16743 return $user;
16744 }
16745
16746 $error_code = FS_Api::get_error_code( $result );
16747
16748 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
16749 /**
16750 * Those API errors will continue coming and are not recoverable with the
16751 * current site's data. Therefore, extend the API call's cached result to 7 days.
16752 */
16753 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
16754 }
16755
16756 return $result;
16757 }
16758
16759 /**
16760 * @author Vova Feldman (@svovaf)
16761 * @since 1.0.1
16762 *
16763 * @param FS_User $user
16764 * @param FS_Site $site
16765 * @param bool|array $plans
16766 */
16767 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
16768 $site->user_id = $user->id;
16769
16770 $this->_site = $site;
16771 $this->_user = $user;
16772 if ( false !== $plans ) {
16773 $this->_plans = $plans;
16774 }
16775
16776 $this->send_install_update();
16777
16778 $this->_store_account();
16779
16780 }
16781
16782 /**
16783 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
16784 * Each version is trimmed after the 16th char.
16785 *
16786 * @author Vova Feldman (@svovaf)
16787 * @since 2.2.1
16788 *
16789 * @return array
16790 */
16791 private function get_versions() {
16792 $versions = array();
16793 $versions['sdk_version'] = $this->version;
16794
16795 // Collect these diagnostic information only if it's allowed.
16796 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
16797 $versions['platform_version'] = get_bloginfo( 'version' );
16798 $versions['programming_language_version'] = phpversion();
16799 }
16800
16801 foreach ( $versions as $k => $version ) {
16802 $versions[ $k ] = self::get_api_sanitized_property( $k, $version );
16803 }
16804
16805 return $versions;
16806 }
16807
16808 /**
16809 * Get sanitized site language.
16810 *
16811 * @param string $language
16812 * @param int $max_len
16813 *
16814 * @since 2.5.1
16815 * @author Vova Feldman (@svovaf)
16816 *
16817 * @return string
16818 */
16819 private static function get_sanitized_language( $language = '', $max_len = self::LANGUAGE_MAX_CHARS ) {
16820 if ( empty( $language ) ) {
16821 $language = get_bloginfo( 'language' );
16822 }
16823
16824 return substr( $language, 0, $max_len );
16825 }
16826
16827 /**
16828 * Get core version stripped from pre-release and build.
16829 *
16830 * @since 2.5.1
16831 * @author Vova Feldman (@svovaf)
16832 *
16833 * @param string $version
16834 * @param int $parts
16835 * @param int $max_len
16836 * @param bool $include_pre_release
16837 *
16838 * @return string
16839 */
16840 private static function get_core_version(
16841 $version,
16842 $parts = 3,
16843 $max_len = self::VERSION_MAX_CHARS,
16844 $include_pre_release = false
16845 ) {
16846 if ( empty( $version ) ) {
16847 // Version is empty.
16848 return '';
16849 }
16850
16851 if ( is_numeric( $version ) ) {
16852 $is_float_version = is_float( $version );
16853
16854 $version = (string) $version;
16855
16856 /**
16857 * 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.
16858 */
16859 if ( $is_float_version && false === strpos( $version, '.' ) ) {
16860 $version .= '.0';
16861 }
16862 }
16863
16864 if ( ! is_string( $version ) ) {
16865 return '';
16866 }
16867
16868 if ( $parts < 1 ) {
16869 return '';
16870 }
16871
16872 $pre_release_regex = $include_pre_release ?
16873 '(\-(alpha|beta|RC)([0-9]+)?)?' :
16874 '';
16875
16876 if ( 0 === preg_match( '/^([0-9]+(\.[0-9]+){0,' . ( $parts - 1 ) . '}' . $pre_release_regex . ')/i', $version, $matches ) ) {
16877 // Version is not starting with a digit.
16878 return '';
16879 }
16880
16881 return substr( $matches[1], 0, $max_len );
16882 }
16883
16884 /**
16885 * @param string $prop
16886 * @param mixed $val
16887 *
16888 * @return mixed
16889 *@author Vova Feldman (@svovaf)
16890 *
16891 * @since 2.5.1
16892 */
16893 private static function get_api_sanitized_property( $prop, $val ) {
16894 if ( ! is_string( $val ) || empty( $val ) ) {
16895 return $val;
16896 }
16897
16898 switch ( $prop ) {
16899 case 'programming_language_version':
16900 // Get core PHP version, which can have up to 3 parts (ignore pre-releases).
16901 return self::get_core_version( $val );
16902 case 'platform_version':
16903 // Get the exact WordPress version, which can have up to 3 parts (including pre-releases).
16904 return self::get_core_version( $val, 3, self::VERSION_MAX_CHARS, true );
16905 case 'sdk_version':
16906 // Get the exact SDK version, which can have up to 4 parts.
16907 return self::get_core_version( $val, 4 );
16908 case 'version':
16909 // Get the entire version but just limited in length.
16910 return substr( $val, 0, self::VERSION_MAX_CHARS );
16911 case 'language':
16912 return self::get_sanitized_language( $val );
16913 default:
16914 return $val;
16915 }
16916 }
16917
16918 /**
16919 * @author Leo Fajardo (@leorw)
16920 * @since 2.3.0
16921 *
16922 * @return bool
16923 */
16924 function has_beta_update() {
16925 return (
16926 ! empty( $this->_storage->beta_data ) &&
16927 ( true === $this->_storage->beta_data['is_beta'] ) &&
16928 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
16929 );
16930 }
16931
16932 /**
16933 * @author Leo Fajardo (@leorw)
16934 * @since 2.3.0
16935 *
16936 * @return bool
16937 */
16938 function is_beta() {
16939 return (
16940 ! empty( $this->_storage->beta_data ) &&
16941 ( true === $this->_storage->beta_data['is_beta'] ) &&
16942 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
16943 );
16944 }
16945
16946 /**
16947 * @author Vova Feldman (@svovaf)
16948 * @since 1.1.7.4
16949 *
16950 * @param array $override_with
16951 * @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.
16952 * @param bool $skip_user_info
16953 *
16954 * @return array
16955 */
16956 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null, $skip_user_info = false ) {
16957 $this->_logger->entrance();
16958
16959 $activation_action = $this->get_unique_affix() . '_activate_new';
16960 $return_url = $this->is_anonymous() ?
16961 // If skipped already, then return to the account page.
16962 $this->get_account_url( $activation_action, array(), false ) :
16963 // Return to the module's main page.
16964 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
16965
16966 $versions = $this->get_versions();
16967
16968 $params = array_merge( $versions, array(
16969 'plugin_slug' => $this->_slug,
16970 'plugin_id' => $this->get_id(),
16971 'plugin_public_key' => $this->get_public_key(),
16972 'plugin_version' => $this->get_plugin_version(),
16973 'return_url' => fs_nonce_url( $return_url, $activation_action ),
16974 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
16975 'account',
16976 array( 'fs_action' => 'sync_user' )
16977 ), 'sync_user' ),
16978 'is_premium' => $this->is_premium(),
16979 'is_active' => true,
16980 'is_uninstalled' => false,
16981 'is_localhost' => WP_FS__IS_LOCALHOST,
16982 ) );
16983
16984 if (
16985 ! $skip_user_info &&
16986 (
16987 empty( $override_with['user_firstname'] ) ||
16988 empty( $override_with['user_lastname'] ) ||
16989 empty( $override_with['user_email'] )
16990 )
16991 ) {
16992 $current_user = self::_get_current_wp_user();
16993
16994 $params['user_firstname'] = $current_user->user_firstname;
16995 $params['user_lastname'] = $current_user->user_lastname;
16996 $params['user_email'] = $current_user->user_email;
16997 }
16998
16999 if ( $this->is_addon() ) {
17000 $parent_fs = $this->get_parent_instance();
17001
17002 $params['parent_plugin_slug'] = $parent_fs->_slug;
17003 $params['parent_plugin_id'] = $parent_fs->get_id();
17004 }
17005
17006 if ( true === $network_level_or_blog_id ) {
17007 if ( ! isset( $override_with['sites'] ) ) {
17008 $params['sites'] = $this->get_sites_for_network_level_optin();
17009 }
17010 } else {
17011 $site = is_numeric( $network_level_or_blog_id ) ?
17012 array( 'blog_id' => $network_level_or_blog_id ) :
17013 null;
17014
17015 $site = $this->get_site_info( $site );
17016
17017 $diagnostic_info = array();
17018 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17019 $diagnostic_info = array(
17020 'site_name' => $site['title'],
17021 'language' => self::get_sanitized_language( $site['language'] ),
17022 );
17023 }
17024
17025 $params = array_merge( $params, $diagnostic_info, array(
17026 'site_uid' => $site['uid'],
17027 'site_url' => $site['url'],
17028 ) );
17029 }
17030
17031 if ( $this->is_pending_activation() &&
17032 ! empty( $this->_storage->pending_license_key )
17033 ) {
17034 $params['license_key'] = $this->_storage->pending_license_key;
17035 }
17036
17037 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
17038 // Even though rand() is known for its security issues,
17039 // the timestamp adds another layer of protection.
17040 // It would be very hard for an attacker to get the secret key form here.
17041 // Plus, this should never run in production since the secret should never
17042 // be included in the production version.
17043 $params['ts'] = WP_FS__SCRIPT_START_TIME;
17044 $params['salt'] = md5( uniqid( rand() ) );
17045 $params['secure'] = md5(
17046 $params['ts'] .
17047 $params['salt'] .
17048 $this->get_secret_key()
17049 );
17050 }
17051
17052 if ( is_multisite() && function_exists( 'get_network' ) ) {
17053 $params['network_uid'] = $this->get_anonymous_network_id();
17054 }
17055
17056 return array_merge( $params, $override_with );
17057 }
17058
17059 /**
17060 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
17061 * 2. If there was an API error, return the API result.
17062 *
17063 * @author Vova Feldman (@svovaf)
17064 * @since 1.1.7.4
17065 *
17066 * @param string|bool $email
17067 * @param string|bool $first
17068 * @param string|bool $last
17069 * @param string|bool $license_key
17070 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
17071 * In this case, the user and site info will be sent to the server but no
17072 * data will be saved to the WP installation's database.
17073 * @param number|bool $trial_plan_id
17074 * @param bool $is_disconnected Whether to opt in without tracking.
17075 * @param null|bool $is_marketing_allowed
17076 * @param array $sites If network-level opt-in, an array of containing details of sites.
17077 * @param bool $redirect
17078 * @param null|number $license_owner_id
17079 *
17080 * @return string|object
17081 * @use WP_Error
17082 */
17083 function opt_in(
17084 $email = false,
17085 $first = false,
17086 $last = false,
17087 $license_key = false,
17088 $is_uninstall = false,
17089 $trial_plan_id = false,
17090 $is_disconnected = false,
17091 $is_marketing_allowed = null,
17092 $sites = array(),
17093 $redirect = true,
17094 $license_owner_id = null
17095 ) {
17096 $this->_logger->entrance();
17097
17098 /**
17099 * @since 1.2.1 If activating with license key, ignore the context-user
17100 * since the user will be automatically loaded from the license.
17101 */
17102 if ( empty( $license_key ) ) {
17103 // Clean up pending license if opt-ing in again.
17104 $this->_storage->remove( 'pending_license_key' );
17105
17106 if ( ! $is_uninstall ) {
17107 if ( false === $email ) {
17108 $current_user = self::_get_current_wp_user();
17109 $email = $current_user->user_email;
17110 }
17111
17112 $fs_user = Freemius::_get_user_by_email( $email );
17113 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
17114 return $this->install_with_user(
17115 $fs_user,
17116 false,
17117 $trial_plan_id,
17118 $redirect,
17119 true,
17120 $sites
17121 );
17122 }
17123 }
17124 }
17125
17126 $skip_user_info = ( ! empty( $license_key ) && FS_User::is_valid_id( $license_owner_id ) );
17127
17128 $user_info = array();
17129
17130 if ( ! $skip_user_info ) {
17131 if ( ! empty( $email ) ) {
17132 $user_info['user_email'] = $email;
17133 }
17134
17135 if ( ! empty( $first ) ) {
17136 $user_info['user_firstname'] = $first;
17137 }
17138
17139 if ( ! empty( $last ) ) {
17140 $user_info['user_lastname'] = $last;
17141 }
17142 }
17143
17144 if ( ! empty( $sites ) ) {
17145 $is_network = true;
17146
17147 $user_info['sites'] = $sites;
17148 } else {
17149 $is_network = false;
17150 }
17151
17152 $params = $this->get_opt_in_params( $user_info, $is_network, $skip_user_info );
17153
17154 $filtered_license_key = false;
17155 if ( is_string( $license_key ) ) {
17156 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
17157 $params['license_key'] = $filtered_license_key;
17158 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17159 $params['trial_plan_id'] = $trial_plan_id;
17160 }
17161
17162 if ( $is_uninstall ) {
17163 $params['uninstall_params'] = array(
17164 'reason_id' => $this->_storage->uninstall_reason->id,
17165 'reason_info' => $this->_storage->uninstall_reason->info
17166 );
17167 }
17168
17169 if ( isset( $params['license_key'] ) ) {
17170 $fs_user = Freemius::_get_user_by_email( $email );
17171
17172 if ( is_object( $fs_user ) ) {
17173 /**
17174 * If opting in with a context license and the context WP Admin user already opted in
17175 * before from the current site, add the user context security params to avoid the
17176 * unnecessary email activation when the context license is owned by the same context user.
17177 *
17178 * @author Leo Fajardo (@leorw)
17179 * @since 1.2.3
17180 */
17181 $params = array_merge( $params, FS_Security::instance()->get_context_params(
17182 $fs_user,
17183 false,
17184 'install_with_existing_user'
17185 ) );
17186 }
17187 }
17188
17189 if ( is_bool( $is_marketing_allowed ) ) {
17190 $params['is_marketing_allowed'] = $is_marketing_allowed;
17191 }
17192
17193 $params['is_disconnected'] = $is_disconnected;
17194 $params['format'] = 'json';
17195 $params['is_extensions_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed();
17196 $params['is_diagnostic_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed();
17197
17198 $request = array(
17199 'method' => 'POST',
17200 'body' => $params,
17201 'timeout' => 60,
17202 );
17203
17204 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
17205 $response = self::safe_remote_post( $url, $request );
17206
17207 if ( is_wp_error( $response ) ) {
17208 /**
17209 * @var WP_Error $response
17210 */
17211 $result = new stdClass();
17212
17213 $error_code = $response->get_error_code();
17214 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
17215
17216 $result->error = (object) array(
17217 'type' => $error_type,
17218 'message' => $response->get_error_message(),
17219 'code' => $error_code,
17220 'http' => 402
17221 );
17222
17223 $this->maybe_modify_api_curl_error_message( $result );
17224
17225 if ( FS_Api::is_blocked( $result ) ) {
17226 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
17227 }
17228
17229 $is_connected = null;
17230
17231 if ( empty( $license_key ) && $this->is_enable_anonymous() ) {
17232 $this->skip_connection( fs_is_network_admin() );
17233
17234 $is_connected = ( ! FS_Api::is_blocked( $result ) );
17235 }
17236
17237 $this->update_connectivity_info( $is_connected );
17238
17239 return $result;
17240 }
17241
17242 $this->update_connectivity_info( true );
17243
17244 // Module is being uninstalled, don't handle the returned data.
17245 if ( $is_uninstall ) {
17246 return true;
17247 }
17248
17249 /**
17250 * 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.
17251 *
17252 * @author Vova Feldman (@svovaf)
17253 * @since 1.2.3
17254 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
17255 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
17256 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
17257 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
17258 */
17259 $decoded = is_string( $response['body'] ) ?
17260 json_decode( $response['body'] ) :
17261 null;
17262
17263 if ( empty( $decoded ) ) {
17264 return false;
17265 }
17266
17267 if ( ! $this->is_api_result_object( $decoded ) ) {
17268 if ( ! empty( $params['license_key'] ) ) {
17269 // Pass the fully entered license key to the failure handler.
17270 $params['license_key'] = $license_key;
17271 }
17272
17273 return $is_uninstall ?
17274 $decoded :
17275 $this->apply_filters( 'after_install_failure', $decoded, $params );
17276 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
17277 if ( $is_network ) {
17278 $site_ids = array();
17279 foreach ( $sites as $site ) {
17280 $site_ids[] = $site['blog_id'];
17281 }
17282
17283 /**
17284 * Store the sites so that they can be installed once the user has clicked on the activation link
17285 * in the email.
17286 *
17287 * @author Leo Fajardo (@leorw)
17288 */
17289 $this->_storage->pending_sites_info = array(
17290 'blog_ids' => $site_ids,
17291 'license_key' => $license_key,
17292 'trial_plan_id' => $trial_plan_id
17293 );
17294 }
17295
17296 // Pending activation, add message.
17297 return $this->set_pending_confirmation(
17298 ( isset( $decoded->email ) ?
17299 $decoded->email :
17300 true ),
17301 false,
17302 $filtered_license_key,
17303 ! empty( $params['trial_plan_id'] ),
17304 isset( $decoded->is_suspicious_email ) && $decoded->is_suspicious_email
17305 );
17306 } else if ( isset( $decoded->install_secret_key ) ) {
17307 return $this->install_with_new_user(
17308 $decoded->user_id,
17309 $decoded->user_public_key,
17310 $decoded->user_secret_key,
17311 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17312 $decoded->is_marketing_allowed :
17313 null ),
17314 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17315 $decoded->is_extensions_tracking_allowed :
17316 null ),
17317 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17318 $decoded->is_diagnostic_tracking_allowed :
17319 null ),
17320 $decoded->install_id,
17321 $decoded->install_public_key,
17322 $decoded->install_secret_key,
17323 false
17324 );
17325 } else if ( is_array( $decoded->installs ) ) {
17326 return $this->install_many_with_new_user(
17327 $decoded->user_id,
17328 $decoded->user_public_key,
17329 $decoded->user_secret_key,
17330 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17331 $decoded->is_marketing_allowed :
17332 null ),
17333 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17334 $decoded->is_extensions_tracking_allowed :
17335 null ),
17336 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17337 $decoded->is_diagnostic_tracking_allowed :
17338 null ),
17339 $decoded->installs,
17340 false
17341 );
17342 }
17343
17344 return $decoded;
17345 }
17346
17347 /**
17348 * Set user and site identities.
17349 *
17350 * @author Vova Feldman (@svovaf)
17351 * @since 1.0.9
17352 *
17353 * @param FS_User $user
17354 * @param FS_Site $site
17355 * @param bool $redirect
17356 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
17357 * redirect (or return a URL) to the account page with a special parameter to
17358 * trigger the auto installation processes.
17359 *
17360 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17361 */
17362 function setup_account(
17363 FS_User $user,
17364 FS_Site $site,
17365 $redirect = true,
17366 $auto_install = false
17367 ) {
17368 return $this->setup_network_account(
17369 $user,
17370 array( $site ),
17371 $redirect,
17372 $auto_install,
17373 false
17374 );
17375 }
17376
17377 /**
17378 * Set user and site identities.
17379 *
17380 * @author Vova Feldman (@svovaf)
17381 * @since 2.0.0
17382 *
17383 * @param FS_User $user
17384 * @param FS_Site[] $installs
17385 * @param bool $redirect
17386 * @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.
17387 * @param bool $is_network_level_opt_in
17388 *
17389 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17390 */
17391 function setup_network_account(
17392 FS_User $user,
17393 array $installs,
17394 $redirect = true,
17395 $auto_install = false,
17396 $is_network_level_opt_in = true
17397 ) {
17398 $first_install = $installs[0];
17399
17400 $this->_user = $user;
17401 $this->_site = $first_install;
17402
17403 $this->_sync_plans();
17404
17405 if ( $this->_storage->handle_gdpr_admin_notice &&
17406 $this->should_handle_gdpr_admin_notice() &&
17407 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
17408 ) {
17409 /**
17410 * Clear user lock after an opt-in.
17411 */
17412 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
17413 FS_User_Lock::instance()->unlock();
17414 }
17415
17416 if ( 1 < count( $installs ) || fs_is_network_admin() ) {
17417 // Only network level opt-in can have more than one install.
17418 $is_network_level_opt_in = true;
17419 }
17420
17421 $this->update_connectivity_info( true );
17422
17423 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
17424 // If Freemius was OFF before, turn it on.
17425 $this->turn_on();
17426
17427 $this->handle_account_connection(
17428 $installs,
17429 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
17430 );
17431
17432 if ( is_numeric( $first_install->license_id ) ) {
17433 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
17434 }
17435
17436 $this->_admin_notices->remove_sticky( 'connect_account' );
17437
17438 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
17439 $this->clear_pending_activation_mode();
17440
17441 if ( ! $this->is_paying_or_trial() ) {
17442 $this->_admin_notices->add_sticky(
17443 sprintf( $this->get_text_inline( '%s opt-in was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
17444 'activation_complete'
17445 );
17446 }
17447 }
17448
17449 if ( $this->is_paying_or_trial() ) {
17450 if ( ! $this->is_premium() ||
17451 ! $this->has_premium_version() ||
17452 ! $this->has_settings_menu()
17453 ) {
17454 if ( $this->is_paying() ) {
17455 $this->add_complete_upgrade_instructions_notice(
17456 sprintf(
17457 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
17458 $this->get_plan_title()
17459 ),
17460 'plan_upgraded'
17461 );
17462 } else {
17463 $trial_plan = $this->get_trial_plan();
17464
17465 $this->add_complete_upgrade_instructions_notice(
17466 sprintf(
17467 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
17468 '<i>' . $this->get_plugin_name() . '</i>'
17469 ),
17470 'trial_started',
17471 $trial_plan->title
17472 );
17473 }
17474 }
17475
17476 $this->_admin_notices->remove_sticky( array(
17477 'trial_promotion',
17478 ) );
17479 }
17480
17481 $plugin_id = fs_request_get( 'plugin_id', false );
17482
17483 // Store activation time ONLY for plugins & themes (not add-ons).
17484 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
17485 if ( empty( $this->_storage->activation_timestamp ) ) {
17486 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
17487 }
17488 }
17489
17490 $next_page = '';
17491
17492 $extra = array();
17493 if ( $auto_install ) {
17494 $extra['auto_install'] = 'true';
17495 }
17496
17497 if ( is_numeric( $plugin_id ) ) {
17498 /**
17499 * @author Leo Fajardo (@leorw)
17500 * @since 1.2.1.6
17501 *
17502 * Also sync the license after an anonymous user subscribes.
17503 */
17504 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
17505 // Add-on was installed - sync license right after install.
17506 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
17507 }
17508 } else {
17509 /**
17510 * @author Vova Feldman (@svovaf)
17511 * @since 1.1.9 If site installed with a valid license, sync license.
17512 */
17513 if ( $this->is_paying() ) {
17514 $this->_sync_plugin_license(
17515 true,
17516 // Installs data is already synced in the beginning of this method directly or via _set_account().
17517 false
17518 );
17519 }
17520
17521 // Reload the page with the keys.
17522 $next_page = $this->is_anonymous() ?
17523 // If user previously skipped, redirect to account page.
17524 $this->get_account_url( false, $extra ) :
17525 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
17526 }
17527
17528 if ( ! empty( $next_page ) && $redirect ) {
17529 fs_redirect( $next_page );
17530 }
17531
17532 return $next_page;
17533 }
17534
17535 /**
17536 * Install plugin with new user information after approval.
17537 *
17538 * @author Vova Feldman (@svovaf)
17539 * @since 1.0.7
17540 */
17541 function _install_with_new_user() {
17542 $this->_logger->entrance();
17543
17544 if ( $this->is_registered() ) {
17545 return;
17546 }
17547
17548 // Only site administrators who can activate plugins may opt-in.
17549 if ( ! $this->is_user_admin() ) {
17550 return;
17551 }
17552
17553 $has_pending_activation_confirmation_param = fs_request_has( 'pending_activation' );
17554
17555 $this->update_license_required_permissions_if_anonymous();
17556
17557 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17558 // @todo This logic should be improved because it's executed on every load of a theme.
17559 $this->is_theme()
17560 ) {
17561 // check_admin_referer( $this->_slug . '_activate_new' );
17562
17563 if ( fs_request_has( 'user_secret_key' ) ) {
17564 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17565 $pending_sites_info = $this->_storage->pending_sites_info;
17566
17567 $this->install_many_pending_with_user(
17568 fs_request_get( 'user_id' ),
17569 fs_request_get_raw( 'user_public_key' ),
17570 fs_request_get_raw( 'user_secret_key' ),
17571 fs_request_get_bool( 'is_marketing_allowed', null ),
17572 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17573 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17574 $pending_sites_info['blog_ids'],
17575 $pending_sites_info['license_key'],
17576 $pending_sites_info['trial_plan_id']
17577 );
17578 } else {
17579 $this->install_with_new_user(
17580 fs_request_get( 'user_id' ),
17581 fs_request_get_raw( 'user_public_key' ),
17582 fs_request_get_raw( 'user_secret_key' ),
17583 fs_request_get_bool( 'is_marketing_allowed', null ),
17584 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17585 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17586 fs_request_get( 'install_id' ),
17587 fs_request_get_raw( 'install_public_key' ),
17588 fs_request_get_raw( 'install_secret_key' ),
17589 true,
17590 fs_request_get_bool( 'auto_install' )
17591 );
17592 }
17593 } else if ( $has_pending_activation_confirmation_param ) {
17594 $this->set_pending_confirmation(
17595 fs_request_get( 'user_email' ),
17596 true,
17597 false,
17598 false,
17599 fs_request_get_bool( 'is_suspicious_email' ),
17600 fs_request_get_bool( 'has_upgrade_context' ),
17601 fs_request_get( 'support_email_address' )
17602 );
17603 }
17604 }
17605 }
17606
17607 /**
17608 * @author Vova Feldman (@svovaf)
17609 * @since 2.0.0
17610 *
17611 * @param number $id
17612 * @param string $public_key
17613 * @param string $secret_key
17614 *
17615 * @return \FS_User
17616 */
17617 private function setup_user( $id, $public_key, $secret_key ) {
17618 $user = self::_get_user_by_id( $id );
17619
17620 if ( is_object( $user ) ) {
17621 $this->_user = $user;
17622 } else {
17623 $user = new FS_User();
17624 $user->id = $id;
17625 $user->public_key = $public_key;
17626 $user->secret_key = $secret_key;
17627
17628 $this->_user = $user;
17629 $user_result = $this->get_api_user_scope()->get();
17630 $user = new FS_User( $user_result );
17631
17632 $this->_user = $user;
17633 $this->_store_user();
17634 }
17635
17636 return $user;
17637 }
17638
17639 /**
17640 * Install plugin with new user.
17641 *
17642 * You can use this method to sync activation with the Freemius WP SDK where the activation happened outside of the regular opt-in flow, for example if you're using an external licensing server with our api:
17643 *
17644 * https://docs.freemius.com/api/licenses/activate
17645 *
17646 * In that case you can call this method like following:
17647 *
17648 * ```
17649 *
17650 * my_fs()->install_with_new_user(
17651 * $result['user_id'],
17652 * $result['user_public_key'],
17653 * $result['user_secret_key'],
17654 * $result['is_marketing_allowed'],
17655 * null,
17656 * true,
17657 * $result['install_id'],
17658 * $result['install_public_key'],
17659 * $result['install_secret_key'],
17660 * false
17661 * );
17662 *
17663 * ```
17664 *
17665 * Here `$result` represents the object returned by the API endpoint.
17666 *
17667 * @author Vova Feldman (@svovaf)
17668 * @since 1.1.7.4
17669 *
17670 * @param number $user_id
17671 * @param string $user_public_key
17672 * @param string $user_secret_key
17673 * @param bool|null $is_marketing_allowed
17674 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17675 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17676 * @param number $install_id
17677 * @param string $install_public_key
17678 * @param string $install_secret_key
17679 * @param bool $redirect
17680 * @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.
17681 *
17682 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17683 */
17684 public function install_with_new_user(
17685 $user_id,
17686 $user_public_key,
17687 $user_secret_key,
17688 $is_marketing_allowed,
17689 $is_extensions_tracking_allowed,
17690 $is_diagnostic_tracking_allowed,
17691 $install_id,
17692 $install_public_key,
17693 $install_secret_key,
17694 $redirect = true,
17695 $auto_install = false
17696 ) {
17697 /**
17698 * This method is also executed after opting in with a license key since the
17699 * license can be potentially associated with a different owner.
17700 *
17701 * @since 2.0.0
17702 */
17703 $user = self::_get_user_by_id( $user_id );
17704
17705 if ( ! is_object( $user ) ) {
17706 $user = new FS_User();
17707 $user->id = $user_id;
17708 $user->public_key = $user_public_key;
17709 $user->secret_key = $user_secret_key;
17710
17711 $this->_user = $user;
17712 $user_result = $this->get_api_user_scope()->get();
17713 $user = new FS_User( $user_result );
17714 }
17715
17716 $this->_user = $user;
17717
17718 $site = new FS_Site();
17719 $site->id = $install_id;
17720 $site->public_key = $install_public_key;
17721 $site->secret_key = $install_secret_key;
17722
17723 $this->_site = $site;
17724 $site_result = $this->get_api_site_scope( true )->get();
17725 $site = new FS_Site( $site_result );
17726 $this->_site = $site;
17727
17728 if ( ! is_null( $is_marketing_allowed ) ) {
17729 $this->disable_opt_in_notice_and_lock_user();
17730 }
17731
17732 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17733 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17734 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17735 ) );
17736
17737 return $this->setup_account(
17738 $this->_user,
17739 $this->_site,
17740 $redirect,
17741 $auto_install
17742 );
17743 }
17744
17745 /**
17746 * Install plugin with user.
17747 *
17748 * @author Leo Fajardo (@leorw)
17749 * @since 2.0.0
17750 *
17751 * @param number $user_id
17752 * @param string $user_public_key
17753 * @param string $user_secret_key
17754 * @param bool|null $is_marketing_allowed
17755 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17756 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17757 * @param array $site_ids
17758 * @param bool $license_key
17759 * @param bool $trial_plan_id
17760 * @param bool $redirect
17761 *
17762 * @return void
17763 */
17764 private function install_many_pending_with_user(
17765 $user_id,
17766 $user_public_key,
17767 $user_secret_key,
17768 $is_marketing_allowed,
17769 $is_extensions_tracking_allowed,
17770 $is_diagnostic_tracking_allowed,
17771 $site_ids,
17772 $license_key = false,
17773 $trial_plan_id = false,
17774 $redirect = true
17775 ) {
17776 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17777
17778 if ( ! is_null( $is_marketing_allowed ) ) {
17779 $this->disable_opt_in_notice_and_lock_user();
17780 }
17781
17782 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17783 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17784 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17785 ) );
17786
17787 $sites = array();
17788 foreach ( $site_ids as $site_id ) {
17789 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
17790 }
17791
17792 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17793 }
17794
17795 /**
17796 * Multi-site install with a new user.
17797 *
17798 * @author Vova Feldman (@svovaf)
17799 * @since 2.0.0
17800 *
17801 * @param number $user_id
17802 * @param string $user_public_key
17803 * @param string $user_secret_key
17804 * @param bool|null $is_marketing_allowed
17805 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17806 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17807 * @param object[] $installs
17808 * @param bool $redirect
17809 * @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.
17810 *
17811 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17812 */
17813 private function install_many_with_new_user(
17814 $user_id,
17815 $user_public_key,
17816 $user_secret_key,
17817 $is_marketing_allowed,
17818 $is_extensions_tracking_allowed,
17819 $is_diagnostic_tracking_allowed,
17820 array $installs,
17821 $redirect = true,
17822 $auto_install = false
17823 ) {
17824 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17825
17826 if ( ! is_null( $is_marketing_allowed ) ) {
17827 $this->disable_opt_in_notice_and_lock_user();
17828 }
17829
17830 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17831 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17832 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17833 ) );
17834
17835 $install_ids = array();
17836
17837 foreach ( $installs as $install ) {
17838 $install_ids[] = $install->id;
17839 }
17840
17841 $items_per_request = 25;
17842 $left = count( $install_ids );
17843 $offset = 0;
17844
17845 $installs = array();
17846 while ( $left > 0 ) {
17847 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, $items_per_request ) ) );
17848
17849 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
17850 // @todo Handle API error.
17851 }
17852
17853 $installs = array_merge( $installs, $result->installs );
17854
17855 $left -= $items_per_request;
17856 $offset += $items_per_request;
17857 }
17858
17859 foreach ( $installs as &$install ) {
17860 $install = new FS_Site( $install );
17861 }
17862
17863 return $this->setup_network_account(
17864 $this->_user,
17865 $installs,
17866 $redirect,
17867 $auto_install
17868 );
17869 }
17870
17871 /**
17872 * @author Vova Feldman (@svovaf)
17873 * @since 1.1.7.4
17874 *
17875 * @param string|bool $email
17876 * @param bool $redirect
17877 * @param string|bool $license_key Since 1.2.1.5
17878 * @param bool $is_pending_trial Since 1.2.1.5
17879 * @param bool $is_suspicious_email Since 2.5.0
17880 * @param bool $has_upgrade_context Since 2.5.3
17881 * @param bool|string $support_email_address Since 2.5.3
17882 *
17883 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
17884 */
17885 private function set_pending_confirmation(
17886 $email = false,
17887 $redirect = true,
17888 $license_key = false,
17889 $is_pending_trial = false,
17890 $is_suspicious_email = false,
17891 $has_upgrade_context = false,
17892 $support_email_address = false
17893 ) {
17894 $is_network_admin = fs_is_network_admin();
17895
17896 if ( $this->_ignore_pending_mode && ! $has_upgrade_context ) {
17897 /**
17898 * If explicitly asked to ignore pending mode, set to anonymous mode
17899 * 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).
17900 *
17901 * @author Vova Feldman
17902 * @since 1.2.1.6
17903 */
17904 $this->skip_connection( $is_network_admin );
17905 } else {
17906 // Install must be activated via email since
17907 // user with the same email already exist.
17908 $this->_storage->is_pending_activation = true;
17909 $this->_add_pending_activation_notice(
17910 $email,
17911 $is_pending_trial,
17912 $is_suspicious_email,
17913 $has_upgrade_context,
17914 $support_email_address
17915 );
17916 }
17917
17918 if ( ! empty( $license_key ) ) {
17919 $this->_storage->pending_license_key = $license_key;
17920 }
17921
17922 // Remove the opt-in sticky notice.
17923 $this->_admin_notices->remove_sticky( array(
17924 'connect_account',
17925 'trial_promotion',
17926 ) );
17927
17928 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
17929
17930 if ( $redirect ) {
17931 // Reload the page with a pending activation message.
17932 fs_redirect( $next_page );
17933 }
17934
17935 return $next_page;
17936 }
17937
17938 /**
17939 * Install plugin with current logged WP user info.
17940 *
17941 * @author Vova Feldman (@svovaf)
17942 * @since 1.0.7
17943 */
17944 function _install_with_current_user() {
17945 $this->_logger->entrance();
17946
17947 if ( $this->is_registered() ) {
17948 return;
17949 }
17950
17951 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
17952 check_admin_referer( $this->get_unique_affix() . '_activate_existing' );
17953
17954 /**
17955 * @author Vova Feldman (@svovaf)
17956 * @since 1.1.9 Add license key if given.
17957 */
17958 $license_key = fs_request_get_raw( 'license_secret_key' );
17959
17960 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17961 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17962 FS_Permission_Manager::PERMISSION_EXTENSIONS => fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17963 ) );
17964
17965 $this->install_with_current_user( $license_key );
17966 }
17967 }
17968
17969
17970 /**
17971 * @author Vova Feldman (@svovaf)
17972 * @since 1.1.7.4
17973 *
17974 * @param string|bool $license_key
17975 * @param number|bool $trial_plan_id
17976 * @param array $sites Since 2.0.0
17977 * @param bool $redirect
17978 *
17979 * @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.
17980 */
17981 function install_with_current_user(
17982 $license_key = false,
17983 $trial_plan_id = false,
17984 $sites = array(),
17985 $redirect = true
17986 ) {
17987 // Get current logged WP user.
17988 $current_user = self::_get_current_wp_user();
17989
17990 // Find the relevant FS user by the email.
17991 $user = self::_get_user_by_email( $current_user->user_email );
17992
17993 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17994 }
17995
17996 /**
17997 * @author Vova Feldman (@svovaf)
17998 * @since 2.0.0
17999 *
18000 * @param \FS_User $user
18001 * @param string|bool $license_key
18002 * @param number|bool $trial_plan_id
18003 * @param bool $redirect
18004 * @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.
18005 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
18006 *
18007 * @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.
18008 */
18009 function install_with_user(
18010 FS_User $user,
18011 $license_key = false,
18012 $trial_plan_id = false,
18013 $redirect = true,
18014 $setup_account = true,
18015 $sites = array()
18016 ) {
18017 // We have to set the user before getting user scope API handler.
18018 $this->_user = $user;
18019
18020 // Install the plugin.
18021 $result = $this->create_installs_with_user(
18022 $user,
18023 $license_key,
18024 $trial_plan_id,
18025 $sites,
18026 $redirect
18027 );
18028
18029 if ( ! $this->is_api_result_entity( $result ) &&
18030 ! $this->is_api_result_object( $result, 'installs' )
18031 ) {
18032 // @todo Handler potential API error of the $result
18033 }
18034
18035 if ( empty( $sites ) ) {
18036 $site = new FS_Site( $result );
18037 $this->_site = $site;
18038
18039 if ( ! $setup_account ) {
18040 $this->_store_site();
18041
18042 $this->sync_plan_if_not_exist( $site->plan_id );
18043
18044 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
18045 $this->sync_license_if_not_exist( $site->license_id, $license_key );
18046 }
18047
18048 $this->_admin_notices->remove_sticky( 'connect_account', false );
18049
18050 return $site;
18051 }
18052
18053 return $this->setup_account( $this->_user, $this->_site, $redirect );
18054 } else {
18055 $installs = array();
18056 foreach ( $result->installs as $install ) {
18057 $installs[] = new FS_Site( $install );
18058 }
18059
18060 return $this->setup_network_account(
18061 $user,
18062 $installs,
18063 $redirect
18064 );
18065 }
18066 }
18067
18068 /**
18069 * Initiate an API request to create a collection of installs.
18070 *
18071 * @author Vova Feldman (@svovaf)
18072 * @since 2.0.0
18073 *
18074 * @param \FS_User $user
18075 * @param bool $license_key
18076 * @param bool $trial_plan_id
18077 * @param array $sites
18078 * @param bool $redirect
18079 * @param bool $silent
18080 *
18081 * @return object|mixed
18082 */
18083 private function create_installs_with_user(
18084 FS_User $user,
18085 $license_key = false,
18086 $trial_plan_id = false,
18087 $sites = array(),
18088 $redirect = false,
18089 $silent = false
18090 ) {
18091 $extra_install_params = array(
18092 'uid' => $this->get_anonymous_id(),
18093 'is_disconnected' => false,
18094 );
18095
18096 if ( ! empty( $license_key ) ) {
18097 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
18098
18099 if ( $silent ) {
18100 $extra_install_params['ignore_license_owner'] = true;
18101 }
18102 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
18103 $extra_install_params['trial_plan_id'] = $trial_plan_id;
18104 }
18105
18106 if ( ! empty( $sites ) ) {
18107 $extra_install_params['sites'] = $sites;
18108 }
18109
18110 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
18111
18112 // Install the plugin.
18113 $result = $this->get_api_user_scope_by_user( $user )->call(
18114 "/plugins/{$this->get_id()}/installs.json",
18115 'post',
18116 $args
18117 );
18118
18119 if ( ! $this->is_api_result_entity( $result ) &&
18120 ! $this->is_api_result_object( $result, 'installs' )
18121 ) {
18122 if ( ! empty( $args['license_key'] ) ) {
18123 // Pass the fully entered license key to the failure handler.
18124 $args['license_key'] = $license_key;
18125 }
18126
18127 $result = $this->apply_filters( 'after_install_failure', $result, $args );
18128
18129 if ( ! $silent ) {
18130 $this->_admin_notices->add(
18131 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18132 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . esc_html( $result->error->message ) . '</b>',
18133 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18134 'error'
18135 );
18136 }
18137
18138 if ( $redirect ) {
18139 /**
18140 * We set the user before getting the user scope API handler, so the user became temporarily
18141 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
18142 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
18143 * activation page based on the return value of `is_registered()`. In addition, in case the
18144 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
18145 * misleading plugin activation errors will be shown on the `Plugins` page.
18146 *
18147 * @author Leo Fajardo (@leorw)
18148 */
18149 $user = $this->_user;
18150
18151 $this->_user = null;
18152
18153 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
18154
18155 /**
18156 * Restore the user after the redirect, this is relevant when there are cases where the redirect will choose not to do anything.
18157 */
18158 $this->_user = $user;
18159 }
18160 }
18161
18162 return $result;
18163 }
18164
18165 /**
18166 * Tries to activate add-on account based on parent plugin info.
18167 *
18168 * @author Vova Feldman (@svovaf)
18169 * @since 1.0.6
18170 *
18171 * @param Freemius $parent_fs
18172 * @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.
18173 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
18174 */
18175 private function _activate_addon_account(
18176 Freemius $parent_fs,
18177 $network_level_or_blog_id = null,
18178 $bundle_license = null
18179 ) {
18180 if ( $this->is_registered() ) {
18181 // Already activated.
18182 return;
18183 }
18184
18185 $permission_ids = FS_Permission_Manager::get_all_permission_ids();
18186 $permissions = array();
18187 foreach ( $permission_ids as $permission_id ) {
18188 $permissions[ $permission_id ] = FS_Permission_Manager::instance( $parent_fs )->is_permission( $permission_id, true );
18189 }
18190
18191 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( $permissions );
18192
18193 /**
18194 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
18195 * already returns the data for the current blog.
18196 *
18197 * @author Leo Fajardo (@leorw)
18198 * @since 2.3.0
18199 */
18200 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
18201 array() :
18202 array( 'uid' => $this->get_anonymous_id() );
18203
18204 $params = $this->get_install_data_for_api(
18205 $uid_param_to_override,
18206 false,
18207 false,
18208 /**
18209 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
18210 * already includes the data for it.
18211 *
18212 * @author Leo Fajardo (@leorw)
18213 * @since 2.3.0
18214 */
18215 ( true !== $network_level_or_blog_id )
18216 );
18217
18218 if ( true === $network_level_or_blog_id ) {
18219 $params['sites'] = $this->get_sites_for_network_level_optin();
18220
18221 if ( empty( $params['sites'] ) ) {
18222 return;
18223 }
18224 }
18225
18226 if ( is_object( $bundle_license ) ) {
18227 $params['license_key'] = $bundle_license->secret_key;
18228 }
18229
18230 // Activate add-on with parent plugin credentials.
18231 $result = $parent_fs->get_api_site_scope()->call(
18232 "/addons/{$this->_plugin->id}/installs.json",
18233 'post',
18234 $params
18235 );
18236
18237 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18238 if ( is_object( $bundle_license ) ) {
18239 /**
18240 * 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.
18241 *
18242 * @author Leo Fajardo (@leorw)
18243 * @since 2.4.0
18244 */
18245 } else {
18246 $error_message = FS_Api::is_api_error_object( $result ) ?
18247 $result->error->message :
18248 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
18249
18250 $this->_admin_notices->add(
18251 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18252 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . esc_html( $error_message ) . '</b>',
18253 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18254 'error'
18255 );
18256 }
18257
18258 return;
18259 }
18260
18261 $addon_installs = $result->installs;
18262 foreach ( $addon_installs as $key => $addon_install ) {
18263 $addon_installs[ $key ] = new FS_Site( $addon_install );
18264 }
18265
18266 $first_install = $addon_installs[0];
18267
18268 // Get user information based on parent's plugin.
18269 $user = $parent_fs->get_user();
18270
18271 // First of all, set site and user info - otherwise we won't
18272 // be able to invoke API calls.
18273 $this->_site = $first_install;
18274 $this->_user = $user;
18275
18276 // Sync add-on plans.
18277 $this->_sync_plans();
18278
18279 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
18280
18281 // Get site's current plan.
18282 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
18283
18284 // Sync licenses.
18285 $this->_sync_licenses();
18286
18287 if ( ! fs_is_network_admin() ) {
18288 // Try to activate premium license.
18289 $this->_activate_license( true, $bundle_license );
18290
18291 if ( is_object( $bundle_license ) ) {
18292 $this->maybe_activate_bundle_license( $bundle_license );
18293 }
18294 } else {
18295 if ( is_object( $bundle_license ) ) {
18296 $premium_license = $bundle_license;
18297 } else {
18298 $license_id = fs_request_get( 'license_id' );
18299
18300 if ( is_object( $this->_site ) &&
18301 FS_Plugin_License::is_valid_id( $license_id ) &&
18302 $license_id == $this->_site->license_id
18303 ) {
18304 // License is already activated.
18305 return;
18306 }
18307
18308 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
18309 $this->_get_license_by_id( $license_id ) :
18310 $this->_get_available_premium_license();
18311 }
18312
18313 if ( is_object( $premium_license ) ) {
18314 $this->maybe_network_activate_addon_license( $premium_license );
18315 }
18316 }
18317 }
18318
18319 /**
18320 * @author Leo Fajardo (@leorw)
18321 * @since 2.3.0
18322 *
18323 * @param FS_Site[] $installs
18324 * @param bool $is_site_level
18325 */
18326 private function handle_account_connection( $installs, $is_site_level ) {
18327 $first_install = $installs[0];
18328
18329 if ( $is_site_level ) {
18330 $this->_set_account( $this->_user, $first_install );
18331
18332 $this->do_action( 'after_account_connection', $this->_user, $first_install );
18333 } else {
18334 $this->_store_user();
18335
18336 // Map site addresses to their blog IDs.
18337 $address_to_blog_map = $this->get_address_to_blog_map();
18338
18339 $first_blog_id = null;
18340 $blog_2_install_map = array();
18341 foreach ( $installs as $install ) {
18342 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
18343 $blog_id = $address_to_blog_map[ $address ];
18344
18345 $this->_store_site( true, $blog_id, $install );
18346
18347 if ( is_null( $first_blog_id ) ) {
18348 $first_blog_id = $blog_id;
18349 }
18350
18351 $blog_2_install_map[ $blog_id ] = $install;
18352 }
18353
18354 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
18355 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
18356 ) {
18357 // Store network user.
18358 $this->_storage->network_user_id = $this->_user->id;
18359 }
18360
18361 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
18362 $this->_storage->network_install_blog_id = $first_blog_id;
18363 }
18364
18365 if ( count( $installs ) === count( $address_to_blog_map ) ) {
18366 // Super admin opted in for all sites in the network.
18367 $this->_storage->is_network_connected = true;
18368 }
18369
18370 $this->_store_licenses( false );
18371
18372 self::$_accounts->store();
18373
18374 // Don't sync the installs data on network upgrade
18375 if ( ! $this->network_upgrade_mode_completed() ) {
18376 $this->send_installs_update();
18377 }
18378
18379 $current_blog = get_current_blog_id();
18380
18381 foreach ( $blog_2_install_map as $blog_id => $install ) {
18382 $this->switch_to_blog( $blog_id );
18383
18384 $this->do_action( 'after_account_connection', $this->_user, $install );
18385 }
18386
18387 // Switch install context back to the first install.
18388 $this->switch_to_blog(
18389 $current_blog,
18390 $first_install,
18391 ( $this->_site->id != $first_install->id )
18392 );
18393
18394 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
18395 }
18396 }
18397
18398 /**
18399 * Tries to activate parent account based on add-on's info.
18400 *
18401 * @author Vova Feldman (@svovaf)
18402 * @since 1.2.2.7
18403 *
18404 * @param Freemius $parent_fs
18405 */
18406 private function activate_parent_account( Freemius $parent_fs ) {
18407 if ( ! $this->is_addon() ) {
18408 // This is not an add-on.
18409 return;
18410 }
18411
18412 if ( $parent_fs->is_registered() ) {
18413 // Already activated.
18414 return;
18415 }
18416
18417 // Activate parent with add-on's user credentials.
18418 $parent_install = $this->get_api_user_scope()->call(
18419 "/plugins/{$parent_fs->_plugin->id}/installs.json",
18420 'post',
18421 $parent_fs->get_install_data_for_api( array(
18422 'uid' => $parent_fs->get_anonymous_id(),
18423 ), false, false )
18424 );
18425
18426 if ( isset( $parent_install->error ) ) {
18427 $this->_admin_notices->add(
18428 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18429 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . esc_html( $parent_install->error->message ) . '</b>',
18430 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18431 'error'
18432 );
18433
18434 return;
18435 }
18436
18437 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
18438
18439 if ( $parent_fs->is_pending_activation() ) {
18440 $parent_fs->clear_pending_activation_mode();
18441 }
18442
18443 // Get user information based on parent's plugin.
18444 $user = $this->get_user();
18445
18446 // First of all, set site info - otherwise we won't
18447 // be able to invoke API calls.
18448 $parent_fs->_site = new FS_Site( $parent_install );
18449 $parent_fs->_user = $user;
18450
18451 // Sync add-on plans.
18452 $parent_fs->_sync_plans();
18453
18454 $parent_fs->update_license_required_permissions_if_anonymous();
18455
18456 $parent_fs->_set_account( $user, $parent_fs->_site );
18457 }
18458
18459 #endregion
18460
18461 #----------------------------------------------------------------------------------
18462 #region Admin Menu Items
18463 #----------------------------------------------------------------------------------
18464
18465 private $_menu_items = array();
18466
18467 /**
18468 * @author Vova Feldman (@svovaf)
18469 * @since 1.2.1.8
18470 *
18471 * @return array
18472 */
18473 function get_menu_items() {
18474 return $this->_menu_items;
18475 }
18476
18477 /**
18478 * @author Vova Feldman (@svovaf)
18479 * @since 1.0.7
18480 *
18481 * @return string
18482 */
18483 function get_menu_slug() {
18484 return $this->_menu->get_slug();
18485 }
18486
18487 /**
18488 * @author Vova Feldman (@svovaf)
18489 * @since 1.0.9
18490 */
18491 function _prepare_admin_menu() {
18492 // if ( ! $this->is_on() ) {
18493 // return;
18494 // }
18495
18496 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
18497 return;
18498 }
18499
18500 /**
18501 * When running from a site admin with a network activated module and the connection
18502 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
18503 * site level settings.
18504 *
18505 * @author Vova Feldman (@svovaf)
18506 * @since 2.0.0
18507 */
18508 $should_hide_site_admin_settings = (
18509 $this->_is_network_active &&
18510 ! fs_is_network_admin() &&
18511 ! $this->is_delegated_connection() &&
18512 ! $this->is_anonymous() &&
18513 ! $this->is_registered()
18514 );
18515
18516 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
18517
18518 if ( ( false === $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
18519 $should_hide_site_admin_settings
18520 ) {
18521 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
18522 } else {
18523 $this->do_action( fs_is_network_admin() ?
18524 'before_network_admin_menu_init' :
18525 'before_admin_menu_init'
18526 );
18527
18528 $this->add_menu_action();
18529
18530 $this->add_network_menu_when_missing();
18531
18532 $this->add_submenu_items();
18533 }
18534 }
18535
18536 /**
18537 * Admin dashboard menu items modifications.
18538 *
18539 * NOTE: admin_menu action executed before admin_init.
18540 *
18541 * @author Vova Feldman (@svovaf)
18542 * @since 1.0.7
18543 *
18544 */
18545 private function add_menu_action() {
18546 if ( $this->is_activation_mode() ) {
18547 if ( $this->show_opt_in_on_setting_page() ) {
18548 $this->override_plugin_menu_with_activation();
18549 } else {
18550 /**
18551 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
18552 */
18553 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18554 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
18555 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
18556 fs_request_get_bool( 'pending_activation' )
18557 ) {
18558 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
18559 }
18560 }
18561 } else {
18562 if ( ! $this->is_registered() ) {
18563 // If not registered try to install user.
18564 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18565 $this->_install_with_new_user();
18566 }
18567 } else if (
18568 fs_request_is_action( 'sync_user' ) &&
18569 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
18570 ) {
18571 $this->_handle_account_user_sync();
18572 }
18573 }
18574 }
18575
18576 /**
18577 * @author Vova Feldman (@svovaf)
18578 * @since 1.0.1
18579 */
18580 function _redirect_on_clicked_menu_link() {
18581 $this->_logger->entrance();
18582
18583 $page = fs_request_get('page');
18584 $page = is_string($page) ? strtolower($page) : '';
18585
18586 $this->_logger->log( 'page = ' . $page );
18587
18588 foreach ( $this->_menu_items as $priority => $items ) {
18589 foreach ( $items as $item ) {
18590 if ( isset( $item['url'] ) ) {
18591 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
18592 $this->_logger->log( 'Redirecting to ' . $item['url'] );
18593
18594 fs_redirect( $item['url'] );
18595 }
18596 }
18597 }
18598 }
18599 }
18600
18601 /**
18602 * Remove plugin's all admin menu items & pages, and replace with activation page.
18603 *
18604 * @author Vova Feldman (@svovaf)
18605 * @since 1.0.1
18606 */
18607 private function override_plugin_menu_with_activation() {
18608 $this->_logger->entrance();
18609
18610 $hook = false;
18611
18612 if ( ! $this->has_settings_menu() ) {
18613 // Add the opt-in page without a menu item.
18614 $hook = FS_Admin_Menu_Manager::add_subpage(
18615 '',
18616 $this->get_plugin_name(),
18617 $this->get_plugin_name(),
18618 'manage_options',
18619 $this->_slug,
18620 array( &$this, '_connect_page_render' )
18621 );
18622 } else if ( $this->_menu->is_top_level() ) {
18623 if ( $this->_menu->is_override_exact() ) {
18624 // Make sure the current page is matching the activation page.
18625 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18626 return;
18627 }
18628 }
18629
18630 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18631
18632 if ( false === $hook ) {
18633 // Create new menu item just for the opt-in.
18634 $hook = FS_Admin_Menu_Manager::add_page(
18635 $this->get_plugin_name(),
18636 $this->get_plugin_name(),
18637 'manage_options',
18638 $this->_menu->get_slug(),
18639 array( &$this, '_connect_page_render' )
18640 );
18641 }
18642 } else {
18643 $menus = array( $this->_menu->get_parent_slug() );
18644
18645 if ( $this->_menu->is_override_exact() ) {
18646 // Make sure the current page is matching the activation page.
18647 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18648 return;
18649 }
18650 }
18651
18652 foreach ( $menus as $parent_slug ) {
18653 $hook = $this->_menu->override_submenu_action(
18654 $parent_slug,
18655 $this->_menu->get_raw_slug(),
18656 array( &$this, '_connect_page_render' )
18657 );
18658
18659 if ( false !== $hook ) {
18660 // Found plugin's submenu item.
18661 break;
18662 }
18663 }
18664 }
18665
18666 if ( $this->is_activation_page() ) {
18667 // Clean admin page from distracting content.
18668 self::_clean_admin_content_section();
18669 }
18670
18671 if ( false !== $hook ) {
18672 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18673 $this->_install_with_current_user();
18674 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18675 $this->_install_with_new_user();
18676 }
18677 }
18678 }
18679
18680 /**
18681 * If a plugin was network activated and connected but don't have a network
18682 * level settings, then add an artificial menu item for the Account and other
18683 * Freemius settings.
18684 *
18685 * @author Vova Feldman (@svovaf)
18686 * @since 2.0.0
18687 */
18688 private function add_network_menu_when_missing() {
18689 $this->_logger->entrance();
18690
18691 if ( ! $this->_is_network_active ) {
18692 // Plugin wasn't activated on the network level.
18693 return;
18694 }
18695
18696 if ( ! fs_is_network_admin() ) {
18697 // The context is not the network admin.
18698 return;
18699 }
18700
18701 if ( $this->_menu->has_network_menu() ) {
18702 // Plugin already has a network level menu.
18703 return;
18704 }
18705
18706 if ( $this->is_network_activation_mode() ) {
18707 /**
18708 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
18709 * screen is being shown.
18710 *
18711 * @author Leo Fajardo (@leorw)
18712 */
18713 return;
18714 }
18715
18716 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
18717 if ( $this->is_network_delegated_connection() ) {
18718 // Super-admin delegated the connection to the site admins.
18719 return;
18720 }
18721 }
18722
18723 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
18724
18725 if ( $this->_menu->has_menu() ||
18726 ! $this->is_addon() ||
18727 $this->is_activation_mode()
18728 ) {
18729 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
18730 $this->get_plugin_name(),
18731 $this->get_plugin_name(),
18732 'manage_options',
18733 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
18734 );
18735 }
18736 } else {
18737 $this->_menu->add_subpage_and_update(
18738 $this->_menu->get_parent_slug(),
18739 $this->get_plugin_name(),
18740 $this->get_plugin_name(),
18741 'manage_options',
18742 $this->_menu->get_slug()
18743 );
18744 }
18745 }
18746
18747 /**
18748 * @author Leo Fajardo (@leorw)
18749 * @since 1.2.1
18750 *
18751 * return string
18752 */
18753 function get_top_level_menu_capability() {
18754 global $menu;
18755
18756 $top_level_menu_slug = $this->get_top_level_menu_slug();
18757
18758 foreach ( $menu as $menu_info ) {
18759 /**
18760 * The second element in the menu info array is the capability/role that has access to the menu and the
18761 * third element is the menu slug.
18762 */
18763 if ( $menu_info[2] === $top_level_menu_slug ) {
18764 return $menu_info[1];
18765 }
18766 }
18767
18768 return 'read';
18769 }
18770
18771 /**
18772 * @author Vova Feldman (@svovaf)
18773 * @since 1.0.0
18774 *
18775 * @return string
18776 */
18777 private function get_top_level_menu_slug() {
18778 return ( $this->is_addon() ?
18779 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
18780 $this->_menu->get_top_level_menu_slug() );
18781 }
18782
18783 /**
18784 * @author Vova Feldman (@svovaf)
18785 * @since 1.2.2.7
18786 *
18787 * @return string
18788 */
18789 function get_pricing_cta_label() {
18790 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
18791
18792 if ( $this->is_in_trial_promotion() &&
18793 ! $this->is_paying_or_trial()
18794 ) {
18795 // If running a trial promotion, modify the pricing to load the trial.
18796 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
18797 } else if ( $this->is_paying() ) {
18798 $label = $this->get_text_inline( 'Pricing', 'pricing' );
18799 }
18800
18801 return $label;
18802 }
18803
18804 /**
18805 * @author Vova Feldman (@svovaf)
18806 * @since 1.2.2.7
18807 *
18808 * @return bool
18809 */
18810 function is_pricing_page_visible() {
18811 $visible = (
18812 // Has at least one paid plan.
18813 $this->has_paid_plan() &&
18814 // Didn't ask to hide the pricing page.
18815 $this->is_page_visible( 'pricing' ) &&
18816 // Don't have a valid active license or has more than one plan.
18817 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
18818 );
18819
18820 return $this->apply_filters( 'is_pricing_page_visible', $visible );
18821 }
18822
18823 /**
18824 * @author Leo Fajardo (@leorw)
18825 * @since 2.3.0
18826 *
18827 * @param bool $is_activation_mode
18828 *
18829 * @return bool
18830 */
18831 private function should_add_submenu_or_action_links( $is_activation_mode ) {
18832 if ( $this->is_addon() ) {
18833 // No submenu items or action links for add-ons.
18834 return false;
18835 }
18836
18837 if ( $this->show_opt_in_on_themes_page() ) {
18838 if ( ! fs_is_network_admin() ) {
18839 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
18840 return true;
18841 }
18842 } else if ( $is_activation_mode ) {
18843 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
18844 return false;
18845 }
18846
18847 if ( fs_is_network_admin() ) {
18848 /**
18849 * Add submenu items or action links to network level when plugin was network activated and the super
18850 * admin did NOT delegate the connection of all sites to site admins.
18851 */
18852 return (
18853 $this->_is_network_active &&
18854 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
18855 ! $this->is_network_delegated_connection() )
18856 );
18857 }
18858
18859 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
18860 }
18861
18862 /**
18863 * Add default Freemius menu items.
18864 *
18865 * @author Vova Feldman (@svovaf)
18866 * @since 1.0.0
18867 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
18868 */
18869 private function add_submenu_items() {
18870 $this->_logger->entrance();
18871
18872 $is_activation_mode = $this->is_activation_mode();
18873
18874 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
18875
18876 if ( $add_submenu_items ) {
18877 if ( $this->has_affiliate_program() ) {
18878 // Add affiliation page.
18879 $this->add_submenu_item(
18880 $this->get_text_inline( 'Affiliation', 'affiliation' ),
18881 array( &$this, '_affiliation_page_render' ),
18882 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
18883 'manage_options',
18884 'affiliation',
18885 'Freemius::_clean_admin_content_section',
18886 WP_FS__DEFAULT_PRIORITY,
18887 $this->is_submenu_item_visible( 'affiliation' )
18888 );
18889 }
18890 }
18891
18892 if ( $add_submenu_items ||
18893 ( $is_activation_mode &&
18894 $this->is_only_premium() &&
18895 $this->is_admin_page( 'account' ) &&
18896 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
18897 )
18898 ) {
18899 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
18900 $show_account = (
18901 $this->is_submenu_item_visible( 'account' ) &&
18902 /**
18903 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
18904 */
18905 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
18906 );
18907
18908 // Add user account page.
18909 $this->add_submenu_item(
18910 $this->get_text_inline( 'Account', 'account' ),
18911 array( &$this, '_account_page_render' ),
18912 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
18913 'manage_options',
18914 'account',
18915 array( &$this, '_account_page_load' ),
18916 WP_FS__DEFAULT_PRIORITY,
18917 ( $add_submenu_items && $show_account )
18918 );
18919 }
18920 }
18921
18922 if ( $add_submenu_items ) {
18923 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18924 // Add contact page.
18925 if ( $this->is_premium() ) {
18926 $this->add_submenu_item(
18927 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18928 array( &$this, '_contact_page_render' ),
18929 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
18930 'manage_options',
18931 'contact',
18932 'Freemius::_clean_admin_content_section',
18933 WP_FS__DEFAULT_PRIORITY,
18934 $this->is_submenu_item_visible( 'contact' )
18935 );
18936 } else {
18937 $this->add_submenu_link_item(
18938 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18939 FS_Contact_Form_Manager::instance()->get_standalone_link( $this ),
18940 'contact',
18941 'manage_options',
18942 WP_FS__DEFAULT_PRIORITY,
18943 $this->is_submenu_item_visible( 'contact' ),
18944 'fs_external_contact',
18945 true
18946 );
18947 }
18948 }
18949
18950 if ( $this->has_addons() ) {
18951 $this->add_submenu_item(
18952 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18953 array( &$this, '_addons_page_render' ),
18954 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18955 'manage_options',
18956 'addons',
18957 array( &$this, '_addons_page_load' ),
18958 WP_FS__LOWEST_PRIORITY - 1,
18959 $this->is_submenu_item_visible( 'addons' )
18960 );
18961 }
18962 }
18963
18964 if ( $add_submenu_items ||
18965 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
18966 ) {
18967 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18968 $show_pricing = (
18969 $this->is_submenu_item_visible( 'pricing' ) &&
18970 $this->is_pricing_page_visible()
18971 );
18972
18973 $pricing_cta_text = $this->get_pricing_cta_label();
18974 $pricing_class = 'upgrade-mode';
18975 if ( $show_pricing ) {
18976 if ( $this->is_in_trial_promotion() &&
18977 ! $this->is_paying_or_trial()
18978 ) {
18979 // If running a trial promotion, modify the pricing to load the trial.
18980 $pricing_class = 'trial-mode';
18981 } else if ( $this->is_paying() ) {
18982 $pricing_class = '';
18983 }
18984 }
18985
18986 $custom_pricing_url = $this->get_pricing_url_with_filter( null );
18987 $pricing_menu_title = $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' ) );
18988 $show_pricing_submenu_item = ( $add_submenu_items && $show_pricing );
18989
18990 // Add upgrade/pricing submenu item.
18991 if ( ! is_null( $custom_pricing_url ) ) {
18992 $this->add_submenu_link_item(
18993 $pricing_menu_title,
18994 $custom_pricing_url,
18995 'pricing',
18996 'manage_options',
18997 WP_FS__LOWEST_PRIORITY,
18998 $show_pricing_submenu_item,
18999 $pricing_class
19000 );
19001 } else {
19002 $this->add_submenu_item(
19003 $pricing_menu_title,
19004 array( &$this, '_pricing_page_render' ),
19005 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
19006 'manage_options',
19007 'pricing',
19008 'Freemius::_clean_admin_content_section',
19009 WP_FS__LOWEST_PRIORITY,
19010 $show_pricing_submenu_item,
19011 $pricing_class
19012 );
19013 }
19014 }
19015 }
19016
19017 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
19018 /**
19019 * Add the other menu items if there are any when not in activation mode or license activation is not
19020 * required (license activation is required for registered or anonymous users after activating the
19021 * premium version when the site is not in trial mode or there's no active valid license).
19022 *
19023 * @author Leo Fajardo (@leorw)
19024 * @since 2.2.1
19025 */
19026 if ( 0 < count( $this->_menu_items ) ) {
19027 if ( ! $this->_menu->is_top_level() ) {
19028 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
19029
19030 // Append submenu items right after the plugin's submenu item.
19031 $this->order_sub_submenu_items();
19032 } else {
19033 // Append submenu items.
19034 $this->embed_submenu_items();
19035 }
19036 }
19037 }
19038 }
19039
19040 /**
19041 * Moved the actual submenu item additions to a separated function,
19042 * in order to support sub-submenu items when the plugin's settings
19043 * only have a submenu and not top-level menu item.
19044 *
19045 * @author Vova Feldman (@svovaf)
19046 * @since 1.1.4
19047 */
19048 private function embed_submenu_items() {
19049 $item_classes = $this->_menu->is_top_level() ? 'fs-submenu-item' : 'fs-submenu-item fs-sub';
19050
19051 $item_template = '<span class="' . $item_classes . ' %1$s %2$s %3$s" data-fs-external-url="%5$s" data-fs-new-tab="%6$s">%4$s</span>';
19052
19053 $top_level_menu_capability = $this->get_top_level_menu_capability();
19054
19055 ksort( $this->_menu_items );
19056
19057 $is_first_submenu_item = true;
19058
19059 foreach ( $this->_menu_items as $priority => $items ) {
19060 foreach ( $items as $item ) {
19061 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
19062
19063 $menu_item = sprintf(
19064 $item_template,
19065 $this->get_unique_affix(),
19066 $item['menu_slug'],
19067 ! empty( $item['class'] ) ? $item['class'] : '',
19068 $item['menu_title'],
19069 esc_attr( isset( $item['url'] ) ? $item['url'] : '' ),
19070 esc_attr( isset( $item['new_tab'] ) ? 'true' : 'false' )
19071 );
19072
19073 $top_level_menu_slug = $this->get_top_level_menu_slug();
19074 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
19075
19076 if ( ! isset( $item['url'] ) ) {
19077 $hook = FS_Admin_Menu_Manager::add_subpage(
19078 $item['show_submenu'] ?
19079 $top_level_menu_slug :
19080 '',
19081 $item['page_title'],
19082 $menu_item,
19083 $capability,
19084 $menu_slug,
19085 $item['render_function']
19086 );
19087
19088 if ( false !== $item['before_render_function'] ) {
19089 add_action( "load-$hook", $item['before_render_function'] );
19090 }
19091 } else {
19092 FS_Admin_Menu_Manager::add_subpage(
19093 $item['show_submenu'] ?
19094 $top_level_menu_slug :
19095 '',
19096 $item['page_title'],
19097 $menu_item,
19098 $capability,
19099 $menu_slug,
19100 array( $this, '' )
19101 );
19102 }
19103
19104 if ( $item['show_submenu'] && $is_first_submenu_item ) {
19105 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
19106 /**
19107 * If the top-level menu has been dynamically created, remove the first submenu item that
19108 * WordPress automatically creates when there's no submenu item whose slug matches the
19109 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
19110 *
19111 * Awesome Plugin
19112 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
19113 *
19114 * @author Leo Fajardo (@leorw)
19115 */
19116 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
19117 }
19118
19119 $is_first_submenu_item = false;
19120 }
19121 }
19122 }
19123 }
19124
19125 /**
19126 * Re-order the submenu items so all Freemius added new submenu items
19127 * are added right after the plugin's settings submenu item.
19128 *
19129 * @author Vova Feldman (@svovaf)
19130 * @since 1.1.4
19131 */
19132 private function order_sub_submenu_items() {
19133 global $submenu;
19134
19135 $menu_slug = $this->_menu->get_top_level_menu_slug();
19136
19137 /**
19138 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
19139 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
19140 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
19141 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
19142 *
19143 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
19144 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
19145 * capability needed to access the parent menu as the capability for the submenus that we will add.
19146 */
19147 if ( empty( $submenu[ $menu_slug ] ) ) {
19148 return;
19149 }
19150
19151 $top_level_menu = &$submenu[ $menu_slug ];
19152
19153 $all_submenu_items_after = array();
19154
19155 $found_submenu_item = false;
19156
19157 foreach ( $top_level_menu as $submenu_id => $meta ) {
19158 if ( $found_submenu_item ) {
19159 // Remove all submenu items after the plugin's submenu item.
19160 $all_submenu_items_after[] = $meta;
19161 unset( $top_level_menu[ $submenu_id ] );
19162 }
19163
19164 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
19165 // Found the submenu item, put all below.
19166 $found_submenu_item = true;
19167 continue;
19168 }
19169 }
19170
19171 // Embed all plugin's new submenu items.
19172 $this->embed_submenu_items();
19173
19174 // Start with specially high number to make sure it's appended.
19175 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
19176 foreach ( $all_submenu_items_after as $meta ) {
19177 $top_level_menu[ $i ] = $meta;
19178 $i ++;
19179 }
19180
19181 // Sort submenu items.
19182 ksort( $top_level_menu );
19183 }
19184
19185 /**
19186 * Helper method to return the module's support forum URL.
19187 *
19188 * @author Vova Feldman (@svovaf)
19189 * @since 1.2.2.7
19190 *
19191 * @return string
19192 */
19193 function get_support_forum_url() {
19194 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
19195 }
19196
19197 /**
19198 * Displays the Support Forum link when enabled.
19199 *
19200 * Can be filtered like so:
19201 *
19202 * function _fs_show_support_menu( $is_visible, $menu_id ) {
19203 * if ( 'support' === $menu_id ) {
19204 * return _fs->is_registered();
19205 * }
19206 * return $is_visible;
19207 * }
19208 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
19209 *
19210 */
19211 function _add_default_submenu_items() {
19212 if ( ! $this->is_on() ) {
19213 return;
19214 }
19215
19216 if ( ! $this->is_activation_mode() &&
19217 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
19218 ( ! $this->_is_network_active && is_admin() ) )
19219 ) {
19220 $this->add_submenu_link_item(
19221 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
19222 $this->get_support_forum_url(),
19223 'wp-support-forum',
19224 null,
19225 50,
19226 $this->is_submenu_item_visible( 'support' ),
19227 '',
19228 true
19229 );
19230 }
19231 }
19232
19233 /**
19234 * @author Vova Feldman (@svovaf)
19235 * @since 1.0.1
19236 *
19237 * @param string $menu_title
19238 * @param callable $render_function
19239 * @param bool|string $page_title
19240 * @param string $capability
19241 * @param bool|string $menu_slug
19242 * @param bool|callable $before_render_function
19243 * @param int $priority
19244 * @param bool $show_submenu
19245 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
19246 */
19247 function add_submenu_item(
19248 $menu_title,
19249 $render_function,
19250 $page_title = false,
19251 $capability = 'manage_options',
19252 $menu_slug = false,
19253 $before_render_function = false,
19254 $priority = WP_FS__DEFAULT_PRIORITY,
19255 $show_submenu = true,
19256 $class = ''
19257 ) {
19258 $this->_logger->entrance( 'Title = ' . $menu_title );
19259
19260 if ( $this->is_addon() ) {
19261 $parent_fs = $this->get_parent_instance();
19262
19263 if ( is_object( $parent_fs ) ) {
19264 $parent_fs->add_submenu_item(
19265 $menu_title,
19266 $render_function,
19267 $page_title,
19268 $capability,
19269 $menu_slug,
19270 $before_render_function,
19271 $priority,
19272 $show_submenu,
19273 $class
19274 );
19275
19276 return;
19277 }
19278 }
19279
19280 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19281 $this->_menu_items[ $priority ] = array();
19282 }
19283
19284 $this->_menu_items[ $priority ][] = array(
19285 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
19286 'menu_title' => $menu_title,
19287 'capability' => $capability,
19288 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19289 'render_function' => $render_function,
19290 'before_render_function' => $before_render_function,
19291 'show_submenu' => $show_submenu,
19292 'class' => $class,
19293 );
19294 }
19295
19296 /**
19297 * @author Vova Feldman (@svovaf)
19298 * @since 1.0.1
19299 *
19300 * @param string $menu_title
19301 * @param string $url
19302 * @param bool $menu_slug
19303 * @param string $capability
19304 * @param int $priority
19305 * @param bool $show_submenu
19306 * @param string $class
19307 * @param bool $new_tab
19308 */
19309 function add_submenu_link_item(
19310 $menu_title,
19311 $url,
19312 $menu_slug = false,
19313 $capability = 'read',
19314 $priority = WP_FS__DEFAULT_PRIORITY,
19315 $show_submenu = true,
19316 $class = '',
19317 $new_tab = false
19318 ) {
19319 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
19320
19321 if ( $this->is_addon() ) {
19322 $parent_fs = $this->get_parent_instance();
19323
19324 if ( is_object( $parent_fs ) ) {
19325 $parent_fs->add_submenu_link_item(
19326 $menu_title,
19327 $url,
19328 $menu_slug,
19329 $capability,
19330 $priority,
19331 $show_submenu,
19332 $class,
19333 $new_tab
19334 );
19335
19336 return;
19337 }
19338 }
19339
19340 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19341 $this->_menu_items[ $priority ] = array();
19342 }
19343
19344 $this->_menu_items[ $priority ][] = array(
19345 'menu_title' => $menu_title,
19346 'capability' => $capability,
19347 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19348 'url' => $url,
19349 'page_title' => $menu_title,
19350 'render_function' => 'fs_dummy',
19351 'before_render_function' => '',
19352 'show_submenu' => $show_submenu,
19353 'class' => $class,
19354 'new_tab' => $new_tab,
19355 );
19356 }
19357
19358 #endregion ------------------------------------------------------------------
19359
19360 #--------------------------------------------------------------------------------
19361 #region Admin Notices
19362 #--------------------------------------------------------------------------------
19363
19364 /**
19365 * @author Vova Feldman (@svovaf)
19366 * @since 2.3.1
19367 *
19368 * @param string|string[] $ids
19369 * @param int|null $network_level_or_blog_id
19370 *
19371 * @uses FS_Admin_Notices::remove_sticky()
19372 */
19373 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
19374 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
19375 }
19376
19377 #endregion
19378
19379 #--------------------------------------------------------------------------------
19380 #region Actions / Hooks / Filters
19381 #--------------------------------------------------------------------------------
19382
19383 /**
19384 * @author Vova Feldman (@svovaf)
19385 * @since 1.1.7
19386 *
19387 * @param string $tag
19388 *
19389 * @return string
19390 */
19391 public function get_action_tag( $tag ) {
19392 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
19393 }
19394
19395 /**
19396 * @author Vova Feldman (@svovaf)
19397 * @since 1.2.1.6
19398 *
19399 * @param string $tag
19400 * @param string $slug
19401 * @param bool $is_plugin
19402 *
19403 * @return string
19404 */
19405 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
19406 $action = "fs_{$tag}";
19407
19408 if ( ! empty( $slug ) ) {
19409 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
19410 }
19411
19412 return $action;
19413 }
19414
19415 /**
19416 * Returns a string that can be used to generate a unique action name,
19417 * option name, HTML element ID, or HTML element class.
19418 *
19419 * @author Leo Fajardo (@leorw)
19420 * @since 1.2.2
19421 *
19422 * @return string
19423 */
19424 public function get_unique_affix() {
19425 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
19426 }
19427
19428 /**
19429 * Returns a string that can be used to generate a unique action name,
19430 * option name, HTML element ID, or HTML element class.
19431 *
19432 * @author Vova Feldman (@svovaf)
19433 * @since 1.2.2.5
19434 *
19435 * @param string $slug
19436 * @param bool $is_plugin
19437 *
19438 * @return string
19439 */
19440 static function get_module_unique_affix( $slug, $is_plugin = true ) {
19441 $affix = $slug;
19442
19443 if ( ! $is_plugin ) {
19444 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
19445 }
19446
19447 return $affix;
19448 }
19449
19450 /**
19451 * @author Vova Feldman (@svovaf)
19452 * @since 1.2.1
19453 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19454 * based on the slug for backward compatibility.
19455 *
19456 * @param string $tag
19457 *
19458 * @return string
19459 */
19460 function get_ajax_action( $tag ) {
19461 return self::get_ajax_action_static( $tag, $this->_module_id );
19462 }
19463
19464 /**
19465 * @author Vova Feldman (@svovaf)
19466 * @since 1.2.1.7
19467 *
19468 * @param string $tag
19469 *
19470 * @return string
19471 */
19472 function get_ajax_security( $tag ) {
19473 return wp_create_nonce( $this->get_ajax_action( $tag ) );
19474 }
19475
19476 /**
19477 * @author Vova Feldman (@svovaf)
19478 * @since 1.2.1.7
19479 *
19480 * @param string $tag
19481 */
19482 function check_ajax_referer( $tag ) {
19483 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
19484 }
19485
19486 /**
19487 * @author Vova Feldman (@svovaf)
19488 * @since 1.2.1.6
19489 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19490 * based on the slug for backward compatibility.
19491 *
19492 * @param string $tag
19493 * @param number|null $module_id
19494 *
19495 * @return string
19496 */
19497 static function get_ajax_action_static( $tag, $module_id = null ) {
19498 $action = "fs_{$tag}";
19499
19500 if ( ! empty( $module_id ) ) {
19501 $action .= "_{$module_id}";
19502 }
19503
19504 return $action;
19505 }
19506
19507 /**
19508 * Do action, specific for the current context plugin.
19509 *
19510 * @author Vova Feldman (@svovaf)
19511 * @since 1.0.1
19512 *
19513 * @param string $tag The name of the action to be executed.
19514 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
19515 * functions hooked to the action. Default empty.
19516 *
19517 * @uses do_action()
19518 */
19519 function do_action( $tag, $arg = '' ) {
19520 $args = func_get_args();
19521
19522 $this->_logger->entrance( $tag );
19523
19524 call_user_func_array( 'do_action', array_merge(
19525 array( $this->get_action_tag( $tag ) ),
19526 array_slice( $args, 1 ) )
19527 );
19528 }
19529
19530 /**
19531 * Add action, specific for the current context plugin.
19532 *
19533 * @author Vova Feldman (@svovaf)
19534 * @since 1.0.1
19535 *
19536 * @param string $tag
19537 * @param callable $function_to_add
19538 * @param int $priority
19539 * @param int $accepted_args
19540 *
19541 * @uses add_action()
19542 */
19543 function add_action(
19544 $tag,
19545 $function_to_add,
19546 $priority = WP_FS__DEFAULT_PRIORITY,
19547 $accepted_args = 1
19548 ) {
19549 $this->_logger->entrance( $tag );
19550
19551 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19552 }
19553
19554 /**
19555 * Add AJAX action, specific for the current context plugin.
19556 *
19557 * @author Vova Feldman (@svovaf)
19558 * @since 1.2.1
19559 *
19560 * @param string $tag
19561 * @param callable $function_to_add
19562 * @param int $priority
19563 *
19564 * @uses add_action()
19565 *
19566 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19567 */
19568 function add_ajax_action(
19569 $tag,
19570 $function_to_add,
19571 $priority = WP_FS__DEFAULT_PRIORITY
19572 ) {
19573 $this->_logger->entrance( $tag );
19574
19575 return self::add_ajax_action_static(
19576 $tag,
19577 $function_to_add,
19578 $priority,
19579 $this->_module_id
19580 );
19581 }
19582
19583 /**
19584 * Add AJAX action.
19585 *
19586 * @author Vova Feldman (@svovaf)
19587 * @since 1.2.1.6
19588 *
19589 * @param string $tag
19590 * @param callable $function_to_add
19591 * @param int $priority
19592 * @param number|null $module_id
19593 *
19594 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19595 * @uses add_action()
19596 *
19597 */
19598 static function add_ajax_action_static(
19599 $tag,
19600 $function_to_add,
19601 $priority = WP_FS__DEFAULT_PRIORITY,
19602 $module_id = null
19603 ) {
19604 self::$_static_logger->entrance( $tag );
19605
19606 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
19607 return false;
19608 }
19609
19610 add_action(
19611 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
19612 $function_to_add,
19613 $priority,
19614 0
19615 );
19616
19617 self::$_static_logger->info( "$tag AJAX callback action added." );
19618
19619 return true;
19620 }
19621
19622 /**
19623 * Send a JSON response back to an Ajax request.
19624 *
19625 * @author Vova Feldman (@svovaf)
19626 * @since 1.2.1.5
19627 *
19628 * @param mixed $response
19629 */
19630 static function shoot_ajax_response( $response ) {
19631 wp_send_json( $response );
19632 }
19633
19634 /**
19635 * Send a JSON response back to an Ajax request, indicating success.
19636 *
19637 * @author Vova Feldman (@svovaf)
19638 * @since 1.2.1.5
19639 *
19640 * @param mixed $data Data to encode as JSON, then print and exit.
19641 */
19642 static function shoot_ajax_success( $data = null ) {
19643 wp_send_json_success( $data );
19644 }
19645
19646 /**
19647 * Send a JSON response back to an Ajax request, indicating failure.
19648 *
19649 * @author Vova Feldman (@svovaf)
19650 * @since 1.2.1.5
19651 *
19652 * @param mixed $error Optional error message.
19653 */
19654 static function shoot_ajax_failure( $error = '' ) {
19655 $result = array( 'success' => false );
19656 if ( ! empty( $error ) ) {
19657 $result['error'] = $error;
19658 }
19659
19660 wp_send_json( $result );
19661 }
19662
19663 /**
19664 * Returns an AJAX URL with a special extra param to indicate whether the request was triggered from the network admin or blog admin.
19665 *
19666 * @author Vova Feldman (@svovaf)
19667 * @since 2.5.1
19668 *
19669 * @param string $wrap_with By default, returns the AJAX URL wrapped with single quotes.
19670 *
19671 * @return string
19672 */
19673 static function ajax_url( $wrap_with = "'") {
19674 if ( fs_is_network_admin() ) {
19675 $param_name = '_fs_network_admin';
19676 } else {
19677 $param_name = '_fs_blog_admin';
19678 }
19679
19680 $url = admin_url( 'admin-ajax.php', 'relative' );
19681 $url .= ( false === strpos( $url, '?' ) ) ? '?' : '&';
19682 $url .= "{$param_name}=true";
19683
19684 return "{$wrap_with}{$url}{$wrap_with}";
19685 }
19686
19687 /**
19688 * Apply filter, specific for the current context plugin.
19689 *
19690 * @author Vova Feldman (@svovaf)
19691 * @since 1.0.9
19692 *
19693 * @param string $tag The name of the filter hook.
19694 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
19695 *
19696 * @return mixed The filtered value after all hooked functions are applied to it.
19697 *
19698 * @uses apply_filters()
19699 */
19700 function apply_filters( $tag, $value ) {
19701 $args = func_get_args();
19702
19703 $this->_logger->entrance( $tag );
19704
19705 array_unshift( $args, $this->get_unique_affix() );
19706
19707 return call_user_func_array( 'fs_apply_filter', $args );
19708 }
19709
19710 /**
19711 * Add filter, specific for the current context plugin.
19712 *
19713 * @author Vova Feldman (@svovaf)
19714 * @since 1.0.9
19715 *
19716 * @param string $tag
19717 * @param callable $function_to_add
19718 * @param int $priority
19719 * @param int $accepted_args
19720 *
19721 * @uses add_filter()
19722 */
19723 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
19724 $this->_logger->entrance( $tag );
19725
19726 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19727 }
19728
19729 /**
19730 * Check if has filter.
19731 *
19732 * @author Vova Feldman (@svovaf)
19733 * @since 1.1.4
19734 *
19735 * @param string $tag
19736 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
19737 *
19738 * @return false|int
19739 *
19740 * @uses has_filter()
19741 */
19742 function has_filter( $tag, $function_to_check = false ) {
19743 $this->_logger->entrance( $tag );
19744
19745 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
19746 }
19747
19748 #endregion
19749
19750 /**
19751 * Override default i18n text phrases.
19752 *
19753 * @author Vova Feldman (@svovaf)
19754 * @since 1.1.6
19755 *
19756 * @param string[] string $key_value
19757 *
19758 * @uses fs_override_i18n()
19759 */
19760 function override_i18n( $key_value ) {
19761 fs_override_i18n( $key_value, $this->_slug );
19762 }
19763
19764 /* Account Page
19765 ------------------------------------------------------------------------------------------------------------------*/
19766 /**
19767 * Update site information.
19768 *
19769 * @author Vova Feldman (@svovaf)
19770 * @since 1.0.1
19771 *
19772 * @param bool $store Flush to Database if true.
19773 * @param null|int $network_level_or_blog_id Since 2.0.0
19774 * @param \FS_Site $site Since 2.0.0
19775 */
19776 private function _store_site( $store = true, $network_level_or_blog_id = null, $site = null, $is_backup = false ) {
19777 $this->_logger->entrance();
19778
19779 if ( is_null( $site ) ) {
19780 $site = $this->_site;
19781 }
19782
19783 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
19784 $this->_logger->error( "Empty install ID, can't store site." );
19785
19786 return;
19787 }
19788
19789 $site_clone = clone $site;
19790
19791 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id, $is_backup );
19792
19793 if (
19794 ! $is_backup &&
19795 is_object( $this->_user ) && $this->_user->id != $site->user_id
19796 ) {
19797 $this->sync_user_by_current_install( $site->user_id );
19798
19799 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
19800
19801 if ( empty( $prev_stored_user_id ) &&
19802 is_object($this->_user) && $this->_user->id != $site->user_id
19803 ) {
19804 /**
19805 * Store the current user ID as the previous user ID so that the previous user can be used
19806 * as the install's owner while the new owner's details are not yet available.
19807 *
19808 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
19809 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
19810 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
19811 * into `replica`.
19812 *
19813 * @author Leo Fajardo (@leorw)
19814 */
19815 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
19816 }
19817 }
19818
19819 $sites[ $this->_slug ] = $site_clone;
19820
19821 $this->set_account_option(
19822 ( $is_backup ? 'prev_' : '' ) . 'sites',
19823 $sites,
19824 $store,
19825 $network_level_or_blog_id
19826 );
19827 }
19828
19829 /**
19830 * 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).
19831 *
19832 * @author Leo Fajardo (@leorw)
19833 * @since 2.5.0
19834 */
19835 private function back_up_site() {
19836 $this->_logger->entrance();
19837
19838 $site_clone = clone $this->_site;
19839
19840 $this->_store_site( true, null, $site_clone, true );
19841 }
19842
19843 /**
19844 * Update plugin's plans information.
19845 *
19846 * @author Vova Feldman (@svovaf)
19847 * @since 1.0.2
19848 *
19849 * @param bool $store Flush to Database if true.
19850 */
19851 private function _store_plans( $store = true ) {
19852 $this->_logger->entrance();
19853
19854 $plans = self::get_all_plans( $this->_module_type );
19855
19856 // Copy plans.
19857 $encrypted_plans = array();
19858 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
19859 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
19860 }
19861
19862 $plans[ $this->_slug ] = $encrypted_plans;
19863
19864 $this->set_account_option( 'plans', $plans, $store );
19865 }
19866
19867 /**
19868 * Update user's plugin licenses.
19869 *
19870 * @author Vova Feldman (@svovaf)
19871 * @since 1.0.5
19872 *
19873 * @param bool $store
19874 * @param number|bool $module_id
19875 * @param FS_Plugin_License[] $licenses
19876 */
19877 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
19878 $this->_logger->entrance();
19879
19880 $all_licenses = self::get_all_licenses();
19881
19882 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
19883 $module_id = $this->_module_id;
19884
19885 $user_licenses = is_array( $this->_licenses ) ?
19886 $this->_licenses :
19887 array();
19888
19889 if ( empty( $user_licenses ) ) {
19890 // If the context user doesn't have any license, don't update the licenses collection.
19891 return;
19892 }
19893
19894 $new_user_licenses_map = array();
19895 foreach ( $user_licenses as $user_license ) {
19896 $new_user_licenses_map[ $user_license->id ] = $user_license;
19897 }
19898
19899 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
19900
19901 // Update user licenses.
19902 $licenses_to_update_count = count( $new_user_licenses_map );
19903 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
19904 if ( 0 === $licenses_to_update_count ) {
19905 break;
19906 }
19907
19908 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
19909 // Update license.
19910 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
19911 unset( $new_user_licenses_map[ $license->id ] );
19912
19913 $licenses_to_update_count --;
19914 }
19915 }
19916
19917 if ( ! empty( $new_user_licenses_map ) ) {
19918 // Add new licenses.
19919 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
19920 }
19921
19922 $licenses = $all_licenses[ $module_id ];
19923 }
19924
19925 if ( ! isset( $all_licenses[ $module_id ] ) ) {
19926 $all_licenses[ $module_id ] = array();
19927 }
19928
19929 $all_licenses[ $module_id ] = $licenses;
19930
19931 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
19932 }
19933
19934 /**
19935 * Update user information.
19936 *
19937 * @author Vova Feldman (@svovaf)
19938 * @since 1.0.1
19939 *
19940 * @param bool $store Flush to Database if true.
19941 */
19942 private function _store_user( $store = true ) {
19943 $this->_logger->entrance();
19944
19945 if ( empty( $this->_user->id ) ) {
19946 $this->_logger->error( "Empty user ID, can't store user." );
19947
19948 return;
19949 }
19950
19951 $users = self::get_all_users();
19952 $users[ $this->_user->id ] = $this->_user;
19953 self::$_accounts->set_option( 'users', $users, $store );
19954 }
19955
19956 /**
19957 * Update new updates information.
19958 *
19959 * @author Vova Feldman (@svovaf)
19960 * @since 1.0.4
19961 *
19962 * @param FS_Plugin_Tag|null $update
19963 * @param bool $store Flush to Database if true.
19964 * @param bool|number $plugin_id
19965 */
19966 private function _store_update( $update, $store = true, $plugin_id = false ) {
19967 $this->_logger->entrance();
19968
19969 if ( $update instanceof FS_Plugin_Tag ) {
19970 $update->updated = time();
19971 }
19972
19973 if ( ! is_numeric( $plugin_id ) ) {
19974 $plugin_id = $this->_plugin->id;
19975 }
19976
19977 $updates = self::get_all_updates();
19978 $updates[ $plugin_id ] = $update;
19979 self::$_accounts->set_option( 'updates', $updates, $store );
19980 }
19981
19982 /**
19983 * Update new updates information.
19984 *
19985 * @author Vova Feldman (@svovaf)
19986 * @since 1.0.6
19987 *
19988 * @param FS_Plugin[] $plugin_addons
19989 * @param bool $store Flush to Database if true.
19990 */
19991 private function _store_addons( $plugin_addons, $store = true ) {
19992 $this->_logger->entrance();
19993
19994 $addons = self::get_all_addons();
19995 $addons[ $this->_plugin->id ] = $plugin_addons;
19996 self::$_accounts->set_option( 'addons', $addons, $store );
19997 }
19998
19999 /**
20000 * Delete plugin's associated add-ons.
20001 *
20002 * @author Vova Feldman (@svovaf)
20003 * @since 1.0.8
20004 *
20005 * @param bool $store
20006 *
20007 * @return bool
20008 */
20009 private function _delete_account_addons( $store = true ) {
20010 $all_addons = self::get_all_account_addons();
20011
20012 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
20013 return false;
20014 }
20015
20016 unset( $all_addons[ $this->_plugin->id ] );
20017
20018 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20019
20020 return true;
20021 }
20022
20023 /**
20024 * Update account add-ons list.
20025 *
20026 * @author Vova Feldman (@svovaf)
20027 * @since 1.0.6
20028 *
20029 * @param FS_Plugin[] $addons
20030 * @param bool $store Flush to Database if true.
20031 */
20032 private function _store_account_addons( $addons, $store = true ) {
20033 $this->_logger->entrance();
20034
20035 $all_addons = self::get_all_account_addons();
20036 $all_addons[ $this->_plugin->id ] = $addons;
20037 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20038 }
20039
20040 /**
20041 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
20042 * the valid user licenses will be fetched again and the account add-ons may be updated.
20043 *
20044 * @author Leo Fajardo (@leorw)
20045 * @since 2.2.4
20046 */
20047 private function purge_valid_user_licenses_cache() {
20048 if ( ! $this->is_registered() ) {
20049 return;
20050 }
20051
20052 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
20053 }
20054
20055 /**
20056 * @author Leo Fajardo (@leorw)
20057 * @since 2.3.0
20058 *
20059 * @param array $all_licenses
20060 * @param number|null $site_license_id
20061 * @param bool $include_parent_licenses
20062 *
20063 * @return array
20064 */
20065 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
20066 $foreign_licenses = array(
20067 'ids' => array(),
20068 'license_keys' => array()
20069 );
20070
20071 $parent_license_ids_map = array();
20072
20073 foreach ( $all_licenses as $license ) {
20074 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
20075 continue;
20076 }
20077
20078 $foreign_licenses['ids'][] = $license->id;
20079 $foreign_licenses['license_keys'][] = $license->secret_key;
20080
20081 if (
20082 $include_parent_licenses &&
20083 is_object( $this->_license ) &&
20084 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
20085 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
20086 ) {
20087 /**
20088 * Include the parent license's info only if it has not been included before since child licenses
20089 * can have the same parent license.
20090 */
20091 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
20092 $foreign_licenses['license_keys'][] = $license->secret_key;
20093
20094 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
20095 }
20096 }
20097
20098 if ( empty( $foreign_licenses['ids'] ) ) {
20099 $foreign_licenses = array();
20100 }
20101
20102 return $foreign_licenses;
20103 }
20104
20105 /**
20106 * @author Leo Fajardo (@leorw)
20107 * @since 2.3.0
20108 *
20109 * @return string
20110 */
20111 private function get_valid_user_licenses_endpoint() {
20112 $user_licenses_endpoint = '/licenses.json?type=active' .
20113 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
20114
20115 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
20116
20117 if ( ! empty ( $foreign_licenses ) ) {
20118 $foreign_licenses = array(
20119 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20120 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20121 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20122 );
20123
20124 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20125 }
20126
20127 return $user_licenses_endpoint;
20128 }
20129
20130 /**
20131 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
20132 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
20133 * method filters out non–add-on product IDs and stores the add-on IDs.
20134 *
20135 * @author Leo Fajardo (@leorw)
20136 * @since 2.2.4
20137 *
20138 * @return stdClass[] array
20139 */
20140 private function fetch_valid_user_licenses() {
20141 $this->_logger->entrance();
20142
20143 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
20144
20145 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
20146 ! is_array( $result->licenses )
20147 ) {
20148 return array();
20149 }
20150
20151 return $result->licenses;
20152 }
20153
20154 /**
20155 * @author Leo Fajardo (@leorw)
20156 * @since 2.2.4
20157 *
20158 * @return number[] Account add-on IDs.
20159 */
20160 function get_updated_account_addons() {
20161 $addons = $this->get_addons();
20162 if ( empty( $addons ) ) {
20163 return array();
20164 }
20165
20166 $account_addons = $this->get_account_addons();
20167 if ( ! is_array( $account_addons ) ) {
20168 $account_addons = array();
20169 }
20170
20171 $user_licenses = $this->is_registered() ?
20172 $this->fetch_valid_user_licenses() :
20173 array();
20174
20175 if ( empty( $user_licenses ) ) {
20176 return $account_addons;
20177 }
20178
20179 $addon_ids = array();
20180 foreach ( $addons as $addon ) {
20181 $addon_ids[] = $addon->id;
20182 }
20183
20184 $license_product_ids = array();
20185
20186 foreach ( $user_licenses as $license ) {
20187 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
20188 $license_product_ids = array_merge( $license_product_ids, $license->products );
20189 } else {
20190 $license_product_ids[] = $license->plugin_id;
20191 }
20192 }
20193
20194 // Filter out non–add-on IDs.
20195 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
20196 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
20197 $this->_store_account_addons( array_unique( $new_account_addons ) );
20198 }
20199
20200 return $new_account_addons;
20201 }
20202
20203 /**
20204 * Store account params in the Database.
20205 *
20206 * @author Vova Feldman (@svovaf)
20207 * @since 1.0.1
20208 *
20209 * @param null|int $blog_id Since 2.0.0
20210 */
20211 private function _store_account( $blog_id = null ) {
20212 $this->_logger->entrance();
20213
20214 $this->_store_site( false, $blog_id );
20215 $this->_store_user( false );
20216 $this->_store_plans( false );
20217 $this->_store_licenses( false );
20218
20219 self::$_accounts->store( $blog_id );
20220 }
20221
20222 /**
20223 * Sync user's information.
20224 *
20225 * @author Vova Feldman (@svovaf)
20226 * @since 1.0.3
20227 * @uses FS_Api
20228 */
20229 private function _handle_account_user_sync() {
20230 $this->_logger->entrance();
20231
20232 $api = $this->get_api_user_scope();
20233
20234 // Get user's information.
20235 $user = $api->get( '/', true );
20236
20237 if ( isset( $user->id ) ) {
20238 $this->_user->first = $user->first;
20239 $this->_user->last = $user->last;
20240 $this->_user->email = $user->email;
20241
20242 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
20243
20244 if ( $user->is_verified &&
20245 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
20246 ) {
20247 $this->_user->is_verified = true;
20248
20249 $this->do_action( 'account_email_verified', $user->email );
20250
20251 $this->_admin_notices->add(
20252 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
20253 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
20254 'success',
20255 // Make admin sticky if account menu item is invisible,
20256 // since the page will be auto redirected to the plugin's
20257 // main settings page, and the non-sticky message
20258 // will disappear.
20259 ! $is_menu_item_account_visible,
20260 'email_verified'
20261 );
20262 }
20263
20264 // Flush user details to DB.
20265 $this->_store_user();
20266
20267 $this->do_action( 'after_account_user_sync', $user );
20268
20269 /**
20270 * If account menu item is hidden, redirect to plugin's main settings page.
20271 *
20272 * @author Vova Feldman (@svovaf)
20273 * @since 1.1.6
20274 *
20275 * @link https://github.com/Freemius/wordpress-sdk/issues/6
20276 */
20277 if ( ! $is_menu_item_account_visible ) {
20278 fs_redirect( $this->_get_admin_page_url() );
20279 }
20280 }
20281 }
20282
20283 /**
20284 * @author Vova Feldman (@svovaf)
20285 * @since 1.0.9
20286 * @uses FS_Api
20287 *
20288 * @param number|bool $license_id
20289 *
20290 * @return FS_Subscription|object|bool
20291 */
20292 private function _fetch_site_license_subscription( $license_id = false ) {
20293 $this->_logger->entrance();
20294 $api = $this->get_api_site_scope();
20295
20296 if ( ! is_numeric( $license_id ) ) {
20297 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
20298 $this->_license->parent_license_id :
20299 $this->_license->id;
20300 }
20301
20302 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
20303
20304 return ! isset( $result->error ) ?
20305 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
20306 new FS_Subscription( $result->subscriptions[0] ) :
20307 false
20308 ) :
20309 $result;
20310 }
20311
20312 /**
20313 * @author Vova Feldman (@svovaf)
20314 * @since 1.0.4
20315 * @uses FS_Api
20316 *
20317 * @param number|bool $plan_id
20318 *
20319 * @return FS_Plugin_Plan|object
20320 */
20321 private function _fetch_site_plan( $plan_id = false ) {
20322 $this->_logger->entrance();
20323 $api = $this->get_api_site_scope();
20324
20325 if ( ! is_numeric( $plan_id ) ) {
20326 $plan_id = $this->_site->plan_id;
20327 }
20328
20329 $plan = $api->get( "/plans/{$plan_id}.json", true );
20330
20331 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
20332 }
20333
20334 /**
20335 * @author Vova Feldman (@svovaf)
20336 * @since 1.0.5
20337 * @uses FS_Api
20338 *
20339 * @return FS_Plugin_Plan[]|object
20340 */
20341 private function _fetch_plugin_plans() {
20342 $this->_logger->entrance();
20343 $api = $this->get_current_or_network_user_api_scope();
20344
20345 /**
20346 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
20347 */
20348 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
20349
20350 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
20351 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
20352 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
20353 }
20354
20355 $result = $result->plans;
20356 }
20357
20358 return $result;
20359 }
20360
20361 /**
20362 * @author Vova Feldman (@svovaf)
20363 * @since 2.0.0
20364 *
20365 * @param number $plan_id
20366 *
20367 * @return \FS_Plugin_Plan|object
20368 */
20369 private function fetch_plan_by_id( $plan_id ) {
20370 $this->_logger->entrance();
20371 $api = $this->get_current_or_network_user_api_scope();
20372
20373 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
20374
20375 return $this->is_api_result_entity( $result ) ?
20376 new FS_Plugin_Plan( $result ) :
20377 $result;
20378 }
20379
20380 /**
20381 * @author Vova Feldman (@svovaf)
20382 * @since 1.0.5
20383 * @uses FS_Api
20384 *
20385 * @param number|bool $plugin_id
20386 * @param number|bool $site_license_id
20387 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
20388 * @param number|null $blog_id
20389 *
20390 * @return FS_Plugin_License[]|object
20391 */
20392 private function _fetch_licenses(
20393 $plugin_id = false,
20394 $site_license_id = false,
20395 $foreign_licenses = array(),
20396 $blog_id = null
20397 ) {
20398 $this->_logger->entrance();
20399
20400 $api = $this->get_api_user_scope();
20401
20402 if ( ! is_numeric( $plugin_id ) ) {
20403 $plugin_id = $this->_plugin->id;
20404 }
20405
20406 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
20407 if ( ! empty ( $foreign_licenses ) ) {
20408 $foreign_licenses = array(
20409 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20410 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20411 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20412 );
20413
20414 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20415 }
20416
20417 $result = $api->get( $user_licenses_endpoint, true );
20418
20419 $is_site_license_synced = false;
20420
20421 $api_errors = array();
20422
20423 if ( $this->is_api_result_object( $result, 'licenses' ) &&
20424 is_array( $result->licenses )
20425 ) {
20426 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
20427 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
20428
20429 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
20430 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
20431 }
20432 }
20433
20434 $result = $result->licenses;
20435 } else {
20436 $api_errors[] = $result;
20437 $result = array();
20438 }
20439
20440 if ( ! $is_site_license_synced ) {
20441 if ( ! is_null( $blog_id ) ) {
20442 /**
20443 * If blog ID is not null, the request is for syncing of the license of a single site via the
20444 * network-level "Account" page.
20445 *
20446 * @author Leo Fajardo (@leorw)
20447 */
20448 $this->switch_to_blog( $blog_id );
20449 }
20450
20451 $api = $this->get_api_site_scope();
20452
20453 if ( is_numeric( $site_license_id ) ) {
20454 // Try to retrieve a foreign license that is linked to the install.
20455 $api_result = $api->call( '/licenses.json?is_enriched=true' );
20456
20457 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
20458 is_array( $api_result->licenses )
20459 ) {
20460 $licenses = $api_result->licenses;
20461
20462 if ( ! empty( $licenses ) ) {
20463 $result[] = new FS_Plugin_License( $licenses[0] );
20464 }
20465 } else {
20466 $api_errors[] = $api_result;
20467 }
20468 } else if (
20469 is_object( $this->_license ) &&
20470 /**
20471 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
20472 * the FS instance that does the syncing is the parent FS instance.
20473 *
20474 * @author Leo Fajardo (@leorw)
20475 * @since 2.3.0
20476 */
20477 $this->_license->plugin_id == $plugin_id
20478 ) {
20479 $is_license_in_result = false;
20480 if ( ! empty( $result ) ) {
20481 foreach ( $result as $license ) {
20482 if ( $license->id == $this->_license->id ) {
20483 $is_license_in_result = true;
20484 break;
20485 }
20486 }
20487 }
20488
20489 if ( ! $is_license_in_result ) {
20490 // Fetch foreign license by ID and license key.
20491 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
20492 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
20493
20494 if ( $this->is_api_result_entity( $license ) ) {
20495 $result[] = new FS_Plugin_License( $license );
20496 } else {
20497 $api_errors[] = $license;
20498 }
20499 }
20500 }
20501
20502 if ( ! is_null( $blog_id ) ) {
20503 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20504 }
20505 }
20506
20507 if ( is_array( $result ) && 0 < count( $result ) ) {
20508 // If found at least one license, return license collection even if there are errors.
20509 return $result;
20510 }
20511
20512 if ( ! empty( $api_errors ) ) {
20513 // If found any errors and no licenses, return first error.
20514 return $api_errors[0];
20515 }
20516
20517 // Fallback to empty licenses list.
20518 return $result;
20519 }
20520
20521 /**
20522 * @author Vova Feldman (@svovaf)
20523 * @since 2.0.0
20524 *
20525 * @param number $license_id
20526 * @param string $license_key
20527 *
20528 * @return \FS_Plugin_License|object
20529 */
20530 private function fetch_license_by_key( $license_id, $license_key ) {
20531 $this->_logger->entrance();
20532
20533 $api = $this->get_current_or_network_user_api_scope();
20534
20535 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
20536
20537 return $this->is_api_result_entity( $result ) ?
20538 new FS_Plugin_License( $result ) :
20539 $result;
20540 }
20541
20542 /**
20543 * @author Vova Feldman (@svovaf)
20544 * @since 1.2.0
20545 * @uses FS_Api
20546 *
20547 * @param number|bool $plugin_id
20548 * @param bool $flush
20549 *
20550 * @return FS_Payment[]|object
20551 */
20552 function _fetch_payments( $plugin_id = false, $flush = false ) {
20553 $this->_logger->entrance();
20554
20555 $api = $this->get_api_user_scope();
20556
20557 if ( ! is_numeric( $plugin_id ) ) {
20558 $plugin_id = $this->_plugin->id;
20559 }
20560
20561 $include_bundles = (
20562 is_object( $this->_plugin ) &&
20563 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
20564 );
20565
20566 $result = $api->get(
20567 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
20568 $flush
20569 );
20570
20571 if ( ! isset( $result->error ) ) {
20572 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
20573 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
20574 }
20575 $result = $result->payments;
20576 }
20577
20578 return $result;
20579 }
20580
20581 /**
20582 * @author Vova Feldman (@svovaf)
20583 * @since 1.2.1.5
20584 * @uses FS_Api
20585 *
20586 * @param bool $flush
20587 *
20588 * @return \FS_Billing|mixed
20589 */
20590 function _fetch_billing( $flush = false ) {
20591 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
20592
20593 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
20594
20595 if ( $this->is_api_result_entity( $billing ) ) {
20596 $billing = new FS_Billing( $billing );
20597 }
20598
20599 return $billing;
20600 }
20601
20602 /**
20603 * @author Vova Feldman (@svovaf)
20604 * @since 1.0.5
20605 *
20606 * @param FS_Plugin_License[] $licenses
20607 * @param number $module_id
20608 */
20609 private function _update_licenses( $licenses, $module_id ) {
20610 $this->_logger->entrance();
20611
20612 if ( is_array( $licenses ) ) {
20613 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
20614 $licenses[ $i ]->updated = time();
20615 }
20616 }
20617
20618 $this->_store_licenses( true, $module_id, $licenses );
20619 }
20620
20621 /**
20622 * @author Vova Feldman (@svovaf)
20623 * @since 1.0.4
20624 *
20625 * @param bool|number $plugin_id
20626 * @param bool $flush Since 1.1.7.3
20627 * @param int $expiration Since 1.2.2.7
20628 * @param bool|string $newer_than Since 2.2.1
20629 * @param bool $fetch_upgrade_notice Since 2.12.1
20630 *
20631 * @return object|false New plugin tag info if exist.
20632 */
20633 private function _fetch_newer_version(
20634 $plugin_id = false,
20635 $flush = true,
20636 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
20637 $newer_than = false,
20638 $fetch_upgrade_notice = true
20639 ) {
20640 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than, false, $fetch_upgrade_notice );
20641
20642 if ( ! is_object( $latest_tag ) ) {
20643 return false;
20644 }
20645
20646 $plugin_version = $this->get_plugin_version();
20647
20648 // Check if version is actually newer.
20649 $has_new_version =
20650 // If it's an non-installed add-on then always return latest.
20651 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
20652 // Compare versions.
20653 version_compare( $plugin_version, $latest_tag->version, '<' );
20654
20655 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
20656
20657 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
20658
20659 $this->_storage->beta_data = array(
20660 'is_beta' => $is_latest_version_beta,
20661 'version' => $latest_tag->version
20662 );
20663
20664 return $has_new_version ? $latest_tag : false;
20665 }
20666
20667 /**
20668 * @author Vova Feldman (@svovaf)
20669 * @since 1.0.5
20670 *
20671 * @param bool|number $plugin_id
20672 * @param bool $flush Since 1.1.7.3
20673 *
20674 * @return bool|FS_Plugin_Tag
20675 */
20676 function get_update( $plugin_id = false, $flush = true ) {
20677 $this->_logger->entrance();
20678
20679 if ( ! is_numeric( $plugin_id ) ) {
20680 $plugin_id = $this->_plugin->id;
20681 }
20682
20683 $this->check_updates( true, $plugin_id, $flush );
20684
20685 $updates = $this->get_all_updates();
20686
20687 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
20688 }
20689
20690 /**
20691 * Check if site assigned with active license.
20692 *
20693 * @author Vova Feldman (@svovaf)
20694 * @since 1.0.6
20695 *
20696 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
20697 */
20698 function has_active_license() {
20699 return (
20700 is_object( $this->_license ) &&
20701 is_numeric( $this->_license->id ) &&
20702 ! $this->_license->is_expired()
20703 );
20704 }
20705
20706 /**
20707 * Check if site assigned with active & valid (not expired) license.
20708 *
20709 * @author Vova Feldman (@svovaf)
20710 * @since 1.2.1
20711 *
20712 * @param bool $check_expiration
20713 */
20714 function has_active_valid_license( $check_expiration = true ) {
20715 return self::is_active_valid_license( $this->_license, $check_expiration );
20716 }
20717
20718 /**
20719 * @author Leo Fajardo (@leorw)
20720 * @since 2.3.1
20721 */
20722 function is_data_debug_mode() {
20723 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
20724 return false;
20725 }
20726
20727 $fs = $this->is_addon() ?
20728 $this->get_parent_instance() :
20729 $this;
20730
20731 if ( $fs->is_network_active() && fs_is_network_admin() ) {
20732 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
20733 } else {
20734 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
20735 }
20736
20737 return ( 'true' === $is_developer_license_debug_mode );
20738 }
20739
20740 /**
20741 * @author Leo Fajardo (@leorw)
20742 * @since 2.3.1
20743 */
20744 function _set_data_debug_mode() {
20745 if ( ! $this->is_whitelabeled( true ) ) {
20746 return;
20747 }
20748
20749 $license_or_user_key = fs_request_get_raw( 'license_or_user_key' );
20750
20751 $transient_value = ( ! empty( $license_or_user_key ) ) ?
20752 'true' :
20753 'false';
20754
20755 if ( 'true' === $transient_value ) {
20756 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
20757 'last_license_key' :
20758 'last_license_user_key'
20759 );
20760
20761 if ( md5( $license_or_user_key ) !== $stored_key ) {
20762 $this->shoot_ajax_failure( sprintf(
20763 '%s... %s',
20764 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
20765 $this->get_text_inline(
20766 'seems like the key you entered doesn\'t match our records.',
20767 'developer-or-license-not-found'
20768 )
20769 ) );
20770 }
20771 }
20772
20773 if ( $this->is_network_active() && fs_is_network_admin() ) {
20774 set_site_transient(
20775 "fs_{$this->get_id()}_data_debug_mode",
20776 $transient_value,
20777 WP_FS__TIME_24_HOURS_IN_SEC / 24
20778 );
20779 } else {
20780 set_transient(
20781 "fs_{$this->get_id()}_data_debug_mode",
20782 $transient_value,
20783 WP_FS__TIME_24_HOURS_IN_SEC / 24
20784 );
20785 }
20786
20787 if ( 'true' === $transient_value ) {
20788 $this->_admin_notices->add_sticky(
20789 $this->get_text_inline(
20790 '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.',
20791 'data_debug_mode_enabled'
20792 ),
20793 'data_debug_mode_enabled'
20794 );
20795 }
20796
20797 $this->shoot_ajax_success();
20798 }
20799
20800 /**
20801 * Check if a given license is active & valid (not expired).
20802 *
20803 * @author Vova Feldman (@svovaf)
20804 * @since 2.1.3
20805 *
20806 * @param FS_Plugin_License $license
20807 * @param bool $check_expiration
20808 *
20809 * @return bool
20810 */
20811 private static function is_active_valid_license( $license, $check_expiration = true ) {
20812 return (
20813 is_object( $license ) &&
20814 FS_Plugin_License::is_valid_id( $license->id ) &&
20815 $license->is_active() &&
20816 ( ! $check_expiration || $license->is_valid() )
20817 );
20818 }
20819
20820 /**
20821 * Checks if there's any site that is associated with an active & valid license.
20822 * This logic is used to determine if the admin can download the premium code base from a network level admin.
20823 *
20824 * @author Vova Feldman (@svovaf)
20825 * @since 2.1.3
20826 *
20827 * @return bool
20828 */
20829 function has_any_active_valid_license() {
20830 if ( ! fs_is_network_admin() ) {
20831 return $this->has_active_valid_license();
20832 }
20833
20834 $installs = $this->get_blog_install_map();
20835 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
20836
20837 foreach ( $installs as $blog_id => $install ) {
20838 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20839 continue;
20840 }
20841
20842 foreach ( $all_plugin_licenses as $license ) {
20843 if ( $license->id == $install->license_id ) {
20844 if ( self::is_active_valid_license( $license ) ) {
20845 return true;
20846 }
20847 }
20848 }
20849 }
20850
20851 return false;
20852 }
20853
20854 /**
20855 * Check if site assigned with license with enabled features.
20856 *
20857 * @author Vova Feldman (@svovaf)
20858 * @since 1.0.6
20859 *
20860 * @return bool
20861 */
20862 function has_features_enabled_license() {
20863 return (
20864 is_object( $this->_license ) &&
20865 is_numeric( $this->_license->id ) &&
20866 $this->_license->is_features_enabled()
20867 );
20868 }
20869
20870 /**
20871 * Checks if the product is activated with a bundle license.
20872 *
20873 * @author Leo Fajardo (@leorw)
20874 * @since 2.4.0
20875 *
20876 * @return bool
20877 */
20878 function is_activated_with_bundle_license() {
20879 if ( ! $this->has_features_enabled_license() ) {
20880 return false;
20881 }
20882
20883 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
20884 }
20885
20886 /**
20887 * Check if user is a trial or have feature enabled license.
20888 *
20889 * @author Vova Feldman (@svovaf)
20890 * @since 1.1.7
20891 *
20892 * @return bool
20893 */
20894 function can_use_premium_code() {
20895 return $this->is_trial() || $this->has_features_enabled_license();
20896 }
20897
20898 /**
20899 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
20900 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
20901 * the context user is authenticated.
20902 *
20903 * @author Leo Fajardo (@leorw)
20904 * @since 1.2.2
20905 *
20906 * @return bool
20907 */
20908 function is_user_admin() {
20909 /**
20910 * Require a super-admin when network activated, running from the network level OR if
20911 * running from the site level but not delegated the opt-in.
20912 *
20913 * @author Vova Feldman (@svovaf)
20914 * @since 2.0.0
20915 */
20916 if ( $this->_is_network_active &&
20917 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
20918 ) {
20919 return is_super_admin();
20920 }
20921
20922 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
20923 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
20924 }
20925
20926 /**
20927 * Sync site's plan.
20928 *
20929 * @author Vova Feldman (@svovaf)
20930 * @since 1.0.3
20931 *
20932 * @uses FS_Api
20933 *
20934 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
20935 * the admin.
20936 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
20937 * network-level "Account" page.
20938 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
20939 * `_sync_plugin_license` method in order to switch to the previous blog when sending
20940 * updates for a single site in case `execute_cron` has switched to a different blog.
20941 */
20942 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
20943 $this->_logger->entrance();
20944
20945 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
20946
20947 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
20948
20949 if ( $is_addon_sync ) {
20950 $this->_sync_addon_license( $plugin_id, $background );
20951 } else {
20952 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
20953 }
20954
20955 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
20956 }
20957
20958 /**
20959 * Sync plugin's add-on license.
20960 *
20961 * @author Vova Feldman (@svovaf)
20962 * @since 1.0.6
20963 * @uses FS_Api
20964 *
20965 * @param number $addon_id
20966 * @param bool $background
20967 */
20968 private function _sync_addon_license( $addon_id, $background ) {
20969 $this->_logger->entrance();
20970
20971 if ( $this->is_addon_activated( $addon_id ) ) {
20972 // If already installed, use add-on sync.
20973 $fs_addon = self::get_instance_by_id( $addon_id );
20974
20975 if (
20976 // Add-on is network activated and network integrated.
20977 $fs_addon->is_network_active() ||
20978 // Background sync cron.
20979 self::is_cron() ||
20980 // Add-on is not network activated or not network integrated.
20981 ! fs_is_network_admin()
20982 ) {
20983 $fs_addon->_sync_license( $background );
20984
20985 return;
20986 }
20987 }
20988
20989 // Validate add-on exists.
20990 $addon = $this->get_addon( $addon_id );
20991
20992 if ( ! is_object( $addon ) ) {
20993 return;
20994 }
20995
20996 // Add add-on into account add-ons.
20997 $account_addons = $this->get_account_addons();
20998 if ( ! is_array( $account_addons ) ) {
20999 $account_addons = array();
21000 }
21001 $account_addons[] = $addon->id;
21002 $account_addons = array_unique( $account_addons );
21003 $this->_store_account_addons( $account_addons );
21004
21005 // Load add-on licenses.
21006 $licenses = $this->_fetch_licenses( $addon->id );
21007
21008 // Sync add-on licenses.
21009 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
21010 $this->_update_licenses( $licenses, $addon->id );
21011
21012 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
21013 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
21014
21015 if ( ! isset( $plans_result->error ) ) {
21016 $plans = array();
21017 foreach ( $plans_result->plans as $plan ) {
21018 $plans[] = new FS_Plugin_Plan( $plan );
21019 }
21020
21021 $this->_admin_notices->add_sticky(
21022 sprintf(
21023 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
21024 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
21025 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
21026 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
21027 $addon->title
21028 ) . ' ' . $this->get_latest_download_link(
21029 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
21030 $addon_id
21031 ),
21032 'addon_plan_upgraded_' . $addon->slug,
21033 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
21034 );
21035 }
21036 }
21037 }
21038 }
21039
21040 /**
21041 * Sync site's plugin plan.
21042 *
21043 * @author Vova Feldman (@svovaf)
21044 * @since 1.0.6
21045 * @uses FS_Api
21046 *
21047 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
21048 * @param bool $send_installs_update Since 2.0.0
21049 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
21050 * syncing its license from the network-level "Account" page (e.g.: after
21051 * activating a license only for the single install).
21052 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
21053 * can be used here to switch to the previous blog in case `execute_cron`
21054 * has switched to a different blog.
21055 */
21056 private function _sync_plugin_license(
21057 $background = false,
21058 $send_installs_update = true,
21059 $is_context_single_site = false,
21060 $current_blog_id = null
21061 ) {
21062 $this->_logger->entrance();
21063
21064 $plan_change = 'none';
21065
21066 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
21067
21068 if ( ! $send_installs_update ) {
21069 $site = $this->_site;
21070 } else {
21071 /**
21072 * Sync site info.
21073 *
21074 * @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.
21075 */
21076 if ( $is_site_level_sync ) {
21077 /**
21078 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
21079 *
21080 * @author Leo Fajardo (@leorw)
21081 * @since 2.2.3
21082 */
21083 if ( is_numeric( $current_blog_id ) ) {
21084 $this->switch_to_blog( $current_blog_id );
21085 }
21086
21087 $result = $this->send_install_update( array(), true, true );
21088 $is_valid = $this->is_api_result_entity( $result );
21089 } else {
21090 $result = $this->send_installs_update( array(), true, true );
21091 $is_valid = $this->is_api_result_object( $result, 'installs' );
21092 }
21093
21094 if ( ! $is_valid ) {
21095 if ( $is_context_single_site ) {
21096 // Switch back to the main blog so that the following logic will have the right entities.
21097 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21098 }
21099
21100 // Show API message only if not background sync or if paying customer.
21101 if ( ! $background || $this->is_paying() ) {
21102 // Try to ping API to see if not blocked.
21103 if ( FS_Api::is_blocked( $result ) ) {
21104 /**
21105 * @author Vova Feldman (@svovaf)
21106 * @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.
21107 */
21108 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
21109 // Add notice immediately if not a background sync.
21110 $add_notice = ( ! $background );
21111
21112 if ( ! $add_notice ) {
21113 $counter = (int) get_transient( '_fs_api_connection_retry_counter' );
21114
21115 // We only want to add the notice after 3 consecutive failures.
21116 $add_notice = ( 3 <= $counter );
21117
21118 if ( ! $add_notice ) {
21119 /**
21120 * 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.
21121 *
21122 * 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.
21123 */
21124 set_transient( '_fs_api_connection_retry_counter', $counter + 1, WP_FS__TIME_WEEK_IN_SEC );
21125 }
21126 }
21127
21128 // Add notice instantly for not-background sync and only after 3 failed attempts for background sync.
21129 if ( $add_notice ) {
21130 self::$_global_admin_notices->add(
21131 $this->generate_api_blocked_notice_message_from_result( $result ),
21132 '',
21133 'error',
21134 $background,
21135 'api_blocked'
21136 );
21137
21138 add_action( 'admin_footer', array( 'Freemius', '_add_api_connectivity_notice_handler_js' ) );
21139
21140 // Notice was just shown, reset connectivity counter.
21141 delete_transient( '_fs_api_connection_retry_counter' );
21142 }
21143 }
21144 } else if ( is_object( $result ) ) {
21145 // Authentication params are broken.
21146 $this->_admin_notices->add(
21147 $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' ) . esc_html( var_export( $result->error, true ) ),
21148 '',
21149 'error'
21150 );
21151 }
21152 }
21153
21154 // No reason to continue with license sync while there are API issues.
21155 return;
21156 }
21157
21158 // API is working now. Delete the transient and start afresh.
21159 delete_transient('_fs_api_connection_retry_counter');
21160
21161 if ( $is_site_level_sync ) {
21162 $site = new FS_Site( $result );
21163 } else {
21164 // Map site addresses to their blog IDs.
21165 $address_to_blog_map = $this->get_address_to_blog_map();
21166
21167 // Find the current context install.
21168 $site = null;
21169 foreach ( $result->installs as $install ) {
21170 if ( $install->id == $this->_site->id ) {
21171 $site = new FS_Site( $install );
21172 } else {
21173 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
21174 $blog_id = $address_to_blog_map[ $address ];
21175
21176 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
21177 }
21178 }
21179 }
21180
21181 // Sync plans.
21182 $this->_sync_plans();
21183 }
21184
21185 // Remove sticky API connectivity message.
21186 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
21187
21188 if ( ! $this->has_paid_plan() ) {
21189 $this->_site = $site;
21190 $this->_store_site(
21191 true,
21192 $is_site_level_sync ?
21193 null :
21194 $this->get_network_install_blog_id()
21195 );
21196 } else {
21197 $context_blog_id = 0;
21198
21199 if ( $is_context_single_site ) {
21200 $context_blog_id = get_current_blog_id();
21201
21202 // Switch back to the main blog in order to properly sync the license.
21203 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21204 }
21205
21206 /**
21207 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
21208 * associated with that ID is not included in the user's licenses collection.
21209 * Save previous value to manage remote license renewals.
21210 */
21211 $was_license_expired_before_sync = is_object( $this->_license ) && $this->_license->is_expired();
21212 $this->_sync_licenses(
21213 $site->license_id,
21214 ( $is_context_single_site ?
21215 $context_blog_id :
21216 null
21217 )
21218 );
21219
21220 if ( $is_context_single_site ) {
21221 $this->switch_to_blog( $context_blog_id );
21222 }
21223
21224 // Check if plan / license changed.
21225 if ( $site->plan_id != $this->_site->plan_id ||
21226 // Check if trial started.
21227 $site->trial_plan_id != $this->_site->trial_plan_id ||
21228 $site->trial_ends != $this->_site->trial_ends ||
21229 // Check if license changed.
21230 $site->license_id != $this->_site->license_id
21231 ) {
21232 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
21233 // New trial started.
21234 $this->_site = $site;
21235 $plan_change = 'trial_started';
21236
21237 // For trial with subscription use-case.
21238 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
21239
21240 if ( is_object( $new_license ) && $new_license->is_valid() ) {
21241 $this->_site = $site;
21242 $this->_update_site_license( $new_license );
21243 $this->_store_licenses();
21244
21245 $this->_sync_site_subscription( $this->_license );
21246 }
21247 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
21248 // Was in trial, but now trial expired and no license ID.
21249 // New trial started.
21250 $this->_site = $site;
21251 $plan_change = 'trial_expired';
21252 } else {
21253 $is_free = $this->is_free_plan();
21254
21255 // Make sure license exist and not expired.
21256 $new_license = is_null( $site->license_id ) ?
21257 null :
21258 $this->_get_license_by_id( $site->license_id );
21259
21260 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
21261 // License cancelled.
21262 $this->_site = $site;
21263 $this->_update_site_license( $new_license );
21264 $this->_store_licenses();
21265
21266 $plan_change = 'cancelled';
21267 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
21268 // The license is expired, so ignore upgrade method.
21269 $this->_site = $site;
21270 } else {
21271 // License changed.
21272 $this->_site = $site;
21273
21274 /**
21275 * IMPORTANT:
21276 * 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.
21277 *
21278 * @author Vova Feldman (@svovaf)
21279 * @since 2.0.0
21280 */
21281 $this->_update_site_license( $new_license );
21282
21283 if ( ! $is_context_single_site &&
21284 fs_is_network_admin() &&
21285 $this->_is_network_active &&
21286 $new_license->quota > 1 &&
21287 get_blog_count() > 1
21288 ) {
21289 // See if license can activated on all sites.
21290 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
21291 if ( ! fs_request_get_bool( 'auto_install' ) ) {
21292 // Open the license activation dialog box on the account page.
21293 add_action( 'admin_footer', array(
21294 &$this,
21295 '_open_license_activation_dialog_box'
21296 ) );
21297 }
21298 }
21299 }
21300
21301 $this->_store_licenses();
21302
21303 $plan_change = $is_free ?
21304 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
21305 ( is_object( $new_license ) ?
21306 'changed' :
21307 'downgraded' );
21308 }
21309 }
21310
21311 // Store updated site info.
21312 $this->_store_site(
21313 true,
21314 $is_site_level_sync ?
21315 null :
21316 $this->get_network_install_blog_id()
21317 );
21318 } else {
21319 if ( ! is_object( $this->_license ) ) {
21320 $this->maybe_update_whitelabel_flag(
21321 FS_Plugin_License::is_valid_id( $site->license_id ) ?
21322 $this->get_license_by_id( $site->license_id ) :
21323 null
21324 );
21325 } else {
21326 $this->maybe_update_whitelabel_flag( $this->_license );
21327
21328 if ( $this->_license->is_expired() ) {
21329 if ( ! $this->has_features_enabled_license() ) {
21330 $this->_deactivate_license();
21331 $plan_change = 'downgraded';
21332 } else {
21333 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
21334
21335 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
21336 /**
21337 * Show the expired license notice every 14 days.
21338 *
21339 * @author Leo Fajardo (@leorw)
21340 * @since 2.3.1
21341 */
21342 $plan_change = 'expired';
21343 }
21344 }
21345 } else if ( $was_license_expired_before_sync ) {
21346 /**
21347 * If license was expired but it is not anymore.
21348 *
21349 *
21350 * @author Daniele Alessandra (@danielealessandra)
21351 */
21352 $plan_change = 'extended';
21353 }
21354 }
21355
21356 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
21357 $this->_sync_site_subscription( $this->_license );
21358 }
21359 }
21360
21361 if ( ! $this->is_addon() &&
21362 $this->_site->is_beta() !== $site->is_beta()
21363 ) {
21364 // Beta flag updated.
21365 $this->_site = $site;
21366
21367 $this->_store_site(
21368 true,
21369 $is_site_level_sync ?
21370 null :
21371 $this->get_network_install_blog_id()
21372 );
21373 }
21374
21375 if ( $this->is_addon() || $this->has_addons() ) {
21376 /**
21377 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21378 * an updated valid user licenses collection will be fetched from the server which is used to also
21379 * update the account add-ons (add-ons the user has licenses for).
21380 *
21381 * @author Leo Fajardo (@leorw)
21382 * @since 2.2.4
21383 */
21384 $this->purge_valid_user_licenses_cache();
21385 }
21386 }
21387
21388 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21389
21390 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
21391 switch ( $plan_change ) {
21392 case 'none':
21393 if ( ! $background && is_admin() ) {
21394 $plan = $this->is_trial() ?
21395 $this->get_trial_plan() :
21396 $this->get_plan();
21397
21398 if ( $plan->is_free() ) {
21399 $this->_admin_notices->add(
21400 sprintf(
21401 $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' ),
21402 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
21403 ) . ' ' . sprintf(
21404 '<a href="%s">%s</a>',
21405 $this->contact_url(
21406 'bug',
21407 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' ),
21408 strtoupper( $plan->name )
21409 )
21410 ),
21411 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21412 ),
21413 $hmm_text
21414 );
21415 }
21416 }
21417 break;
21418 case 'upgraded':
21419 case 'activated':
21420 $this->add_after_plan_activation_or_upgrade_instructions_notice( 'upgraded' === $plan_change );
21421
21422 $this->_admin_notices->remove_sticky( array(
21423 'trial_started',
21424 'trial_promotion',
21425 'trial_expired',
21426 'activation_complete',
21427 'license_expired',
21428 ) );
21429 break;
21430 case 'extended':
21431 $this->_admin_notices->remove_sticky( array(
21432 'trial_expired',
21433 'license_expired',
21434 ) );
21435 break;
21436 case 'changed':
21437 $this->_admin_notices->add_sticky(
21438 sprintf(
21439 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
21440 $this->get_plan_title()
21441 ),
21442 'plan_changed'
21443 );
21444
21445 $this->_admin_notices->remove_sticky( array(
21446 'trial_started',
21447 'trial_promotion',
21448 'trial_expired',
21449 'activation_complete',
21450 ) );
21451 break;
21452 case 'downgraded':
21453 $this->_admin_notices->add_sticky(
21454 ($this->has_free_plan() ?
21455 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 ) :
21456 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21457 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) ) ),
21458 'license_expired',
21459 $hmm_text
21460 );
21461 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21462 break;
21463 case 'cancelled':
21464 $this->_admin_notices->add(
21465 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
21466 sprintf(
21467 '<a href="%s">%s</a>',
21468 esc_url( $this->contact_url( 'bug' ) ),
21469 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21470 ),
21471 $hmm_text,
21472 'error'
21473 );
21474 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21475 break;
21476 case 'expired':
21477 $this->_admin_notices->add_sticky(
21478 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 ),
21479 'license_expired',
21480 $hmm_text
21481 );
21482
21483 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
21484
21485 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21486 break;
21487 case 'trial_started':
21488 $this->add_complete_upgrade_instructions_notice(
21489 sprintf(
21490 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
21491 '<i>' . $this->get_plugin_name() . '</i>'
21492 ),
21493 'trial_started',
21494 $this->get_trial_plan()->title
21495 );
21496
21497 $this->_admin_notices->remove_sticky( array(
21498 'trial_promotion',
21499 ) );
21500 break;
21501 case 'trial_expired':
21502 $this->_admin_notices->add_sticky(
21503 ($this->has_free_plan() ?
21504 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
21505 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21506 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))),
21507 'trial_expired',
21508 $hmm_text
21509 );
21510 $this->_admin_notices->remove_sticky( array(
21511 'trial_started',
21512 'trial_promotion',
21513 'plan_upgraded',
21514 ) );
21515 break;
21516 }
21517 }
21518
21519 if ( 'none' !== $plan_change ) {
21520 if (
21521 ! is_object( $this->_license ) ||
21522 ! $this->_license->is_whitelabeled
21523 ) {
21524 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
21525 }
21526
21527 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
21528 }
21529 }
21530
21531 /**
21532 * @author Leo Fajardo (@leorw)
21533 * @since 2.5.4
21534 *
21535 * @param mixed $result
21536 *
21537 * @return string
21538 */
21539 private function generate_api_blocked_notice_message_from_result( $result ) {
21540 $api_domains = $this->apply_filters( 'api_domains', array(
21541 'api.freemius.com',
21542 'wp.freemius.com',
21543 ) );
21544
21545 $api_domains_list_items = '';
21546
21547 foreach( $api_domains as $api_domain ) {
21548 $api_domains_list_items .= "<li>{$api_domain}</li>";
21549 }
21550
21551 $error_message = sprintf(
21552 $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' ),
21553 $this->get_plugin_name(),
21554 "<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>"
21555 );
21556
21557 $error_message =
21558 "<div>{$error_message}</div>" .
21559 '<div class="fs-api-request-error-details" style="display: none">' .
21560 '<strong>' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . '</strong><br>' .
21561 esc_html( $result->error->message ) .
21562 '</div>';
21563
21564 return $error_message;
21565 }
21566
21567 /**
21568 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
21569 *
21570 * @author Vova Feldman (@svovaf)
21571 * @since 2.0.0
21572 */
21573 public function _open_license_activation_dialog_box() {
21574 $vars = array( 'license_id' => $this->_site->license_id );
21575 fs_require_once_template( 'js/open-license-activation.php', $vars );
21576 }
21577
21578 /**
21579 * @author Vova Feldman (@svovaf)
21580 * @since 1.0.5
21581 *
21582 * @param bool $background
21583 * @param FS_Plugin_License|null $premium_license
21584 */
21585 protected function _activate_license( $background = false, $premium_license = null ) {
21586 $this->_logger->entrance();
21587
21588 if ( is_null( $premium_license ) ) {
21589 $license_id = fs_request_get( 'license_id' );
21590
21591 if ( is_object( $this->_site ) &&
21592 FS_Plugin_License::is_valid_id( $license_id ) &&
21593 $license_id == $this->_site->license_id
21594 ) {
21595 // License is already activated.
21596 return;
21597 }
21598
21599 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
21600 $this->_get_license_by_id( $license_id ) :
21601 $this->_get_available_premium_license();
21602 }
21603
21604 if ( ! is_object( $premium_license ) ) {
21605 return;
21606 }
21607
21608 if ( ! is_object( $this->_site ) ) {
21609 // Not yet opted-in.
21610 $user = $this->get_current_or_network_user();
21611 if ( ! is_object( $user ) ) {
21612 $user = self::_get_user_by_id( $premium_license->user_id );
21613 }
21614
21615 if ( is_object( $user ) ) {
21616 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
21617 } else {
21618 $this->opt_in(
21619 false,
21620 false,
21621 false,
21622 $premium_license->secret_key,
21623 false,
21624 false,
21625 false,
21626 null,
21627 array(),
21628 true,
21629 $premium_license->user_id
21630 );
21631
21632 return;
21633 }
21634 }
21635
21636
21637 /**
21638 * If the premium license is already associated with the install, just
21639 * update the license reference (activation is not required).
21640 *
21641 * @since 1.1.9
21642 */
21643 if ( $premium_license->id == $this->_site->license_id ) {
21644 // License is already activated.
21645 $this->_update_site_license( $premium_license );
21646 $this->_store_account();
21647
21648 return;
21649 }
21650
21651 if ( $this->_site->user_id != $premium_license->user_id ) {
21652 $api_request_params = array( 'license_key' => $premium_license->secret_key );
21653 } else {
21654 $api_request_params = array();
21655 }
21656
21657 $api = $this->get_api_site_scope();
21658 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
21659
21660 if ( ! $this->is_api_result_entity( $license ) ) {
21661 if ( ! $background ) {
21662 $this->_admin_notices->add( sprintf(
21663 '%s %s',
21664 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
21665 ( is_object( $license ) && isset( $license->error ) ?
21666 esc_html( $license->error->message ) :
21667 sprintf( '%s<br><code>%s</code>',
21668 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
21669 esc_html( var_export( $license, true ) )
21670 )
21671 )
21672 ),
21673 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
21674 'error'
21675 );
21676 }
21677
21678 return;
21679 }
21680
21681 $this->do_action( 'after_license_activation' );
21682
21683 $premium_license = new FS_Plugin_License( $license );
21684
21685 // Updated site plan.
21686 $site = $this->get_api_site_scope()->get( '/', true );
21687 if ( $this->is_api_result_entity( $site ) ) {
21688 $this->_site = new FS_Site( $site );
21689 }
21690 $this->_update_site_license( $premium_license );
21691
21692 $this->_store_account();
21693
21694 if ( $this->is_addon() || $this->has_addons() ) {
21695 /**
21696 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21697 * an updated valid user licenses collection will be fetched from the server which is used to also
21698 * update the account add-ons (add-ons the user has licenses for).
21699 *
21700 * @author Leo Fajardo (@leorw)
21701 * @since 2.2.4
21702 */
21703 $this->purge_valid_user_licenses_cache();
21704 }
21705
21706 if ( ! $background ) {
21707 $this->add_complete_upgrade_instructions_notice(
21708 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ),
21709 'license_activated'
21710 );
21711 }
21712
21713 $this->_admin_notices->remove_sticky( array(
21714 'trial_promotion',
21715 'license_expired',
21716 ) );
21717 }
21718
21719 /**
21720 * @author Vova Feldman (@svovaf)
21721 * @since 1.0.5
21722 *
21723 * @param bool $show_notice
21724 */
21725 protected function _deactivate_license( $show_notice = true ) {
21726 $this->_logger->entrance();
21727
21728 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21729
21730 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
21731 $this->_admin_notices->add(
21732 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() ),
21733 $hmm_text
21734 );
21735
21736 return;
21737 }
21738
21739 $api = $this->get_api_site_scope();
21740 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
21741
21742 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
21743 }
21744
21745 /**
21746 * @author Leo Fajardo (@leorw)
21747 * @since 2.2.1
21748 *
21749 * @param FS_Plugin_License $license
21750 * @param bool|string $hmm_text
21751 * @param bool $show_notice
21752 */
21753 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
21754 if ( isset( $license->error ) ) {
21755 $this->_admin_notices->add(
21756 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
21757 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . esc_html( var_export( $license->error, true ) ),
21758 $hmm_text,
21759 'error'
21760 );
21761
21762 $this->do_action( 'after_license_deactivation', $license );
21763
21764 return;
21765 }
21766
21767 // Update license cache.
21768 if ( is_array( $this->_licenses ) ) {
21769 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
21770 if ( $license->id == $this->_licenses[ $i ]->id ) {
21771 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
21772 }
21773 }
21774 }
21775
21776 // Update site plan to default.
21777 $this->_sync_plans();
21778 $this->_site->plan_id = $this->_plans[0]->id;
21779 // Unlink license from site.
21780 $this->_update_site_license( null );
21781
21782 $this->_store_account();
21783
21784 $this->do_action( 'after_license_deactivation', $license );
21785
21786 if ( $show_notice ) {
21787 $this->_admin_notices->add(
21788 sprintf( $this->is_only_premium() ?
21789 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
21790 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
21791 $this->get_plan_title()
21792 ),
21793 $this->get_text_inline( 'O.K', 'ok' )
21794 );
21795 }
21796
21797 $this->_admin_notices->remove_sticky( array(
21798 'plan_upgraded',
21799 'license_activated',
21800 ) );
21801 }
21802
21803 /**
21804 * Site plan downgrade.
21805 *
21806 * @author Vova Feldman (@svovaf)
21807 * @since 1.0.4
21808 *
21809 * @return object
21810 *
21811 * @uses FS_Api
21812 */
21813 private function _downgrade_site() {
21814 $this->_logger->entrance();
21815
21816 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
21817
21818 $api = $this->get_api_site_scope();
21819 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
21820
21821 $plan_downgraded = false;
21822 $plan = false;
21823 if ( $this->is_api_result_entity( $site ) ) {
21824 $prev_plan_id = $this->_site->plan_id;
21825
21826 // Update new site plan id.
21827 $this->_site->plan_id = $site->plan_id;
21828
21829 $plan = $this->get_plan();
21830 $subscription = $this->_sync_site_subscription( $this->_license );
21831
21832 // Plan downgraded if plan was changed or subscription was cancelled.
21833 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
21834 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
21835 } else {
21836 // handle different error cases.
21837 $this->handle_license_deactivation_result(
21838 $site,
21839 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
21840 );
21841 }
21842
21843 if ( ! $plan_downgraded ) {
21844 return (object) array(
21845 'error' => (object) array(
21846 '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' )
21847 )
21848 );
21849 }
21850
21851 // Remove previous sticky message about upgrade (if exist).
21852 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21853
21854 $this->_admin_notices->add(
21855 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
21856 $plan->title,
21857 human_time_diff( time(), strtotime( $this->_license->expiration ) )
21858 )
21859 );
21860
21861 // Store site updates.
21862 $this->_store_site();
21863
21864 if ( $deactivate_license &&
21865 ! FS_Plugin_License::is_valid_id( $site->license_id )
21866 ) {
21867 if ( $this->_site->is_localhost() ) {
21868 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
21869 } else {
21870 $this->_license->activated = max( 0, $this->_license->activated - 1 );
21871 }
21872
21873 // Handle successful license deactivation result.
21874 $this->handle_license_deactivation_result( $this->_license );
21875 }
21876
21877 return $site;
21878 }
21879
21880 /**
21881 * @author Vova Feldman (@svovaf)
21882 * @since 1.1.8.1
21883 *
21884 * @param bool|string $plan_name
21885 * @param bool $add_sticky_notice
21886 *
21887 * @return bool If trial was successfully started.
21888 */
21889 function start_trial( $plan_name = false, $add_sticky_notice = false ) {
21890 $this->_logger->entrance();
21891
21892 // Alias.
21893 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
21894
21895 if ( $this->is_trial() ) {
21896 // Already in trial mode.
21897 $this->_admin_notices->add(
21898 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
21899 $oops_text,
21900 'error',
21901 $add_sticky_notice
21902 );
21903
21904 return false;
21905 }
21906
21907 if ( $this->_site->is_trial_utilized() && ! $this->is_payments_sandbox() ) {
21908 // Trial was already utilized.
21909 $this->_admin_notices->add(
21910 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
21911 $oops_text,
21912 'error',
21913 $add_sticky_notice
21914 );
21915
21916 return false;
21917 }
21918
21919 if ( false !== $plan_name ) {
21920 $plan = $this->get_plan_by_name( $plan_name );
21921
21922 if ( false === $plan ) {
21923 // Plan doesn't exist.
21924 $this->_admin_notices->add(
21925 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
21926 $oops_text,
21927 'error',
21928 $add_sticky_notice
21929 );
21930
21931 return false;
21932 }
21933
21934 if ( ! $plan->has_trial() ) {
21935 // Plan doesn't exist.
21936 $this->_admin_notices->add(
21937 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
21938 $oops_text,
21939 'error',
21940 $add_sticky_notice
21941 );
21942
21943 return false;
21944 }
21945 } else {
21946 if ( ! $this->has_trial_plan() ) {
21947 // None of the plans have a trial.
21948 $this->_admin_notices->add(
21949 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
21950 $oops_text,
21951 'error',
21952 $add_sticky_notice
21953 );
21954
21955 return false;
21956 }
21957
21958 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
21959
21960 $plan = $plans_with_trial[0];
21961 }
21962
21963 $trial_params = array();
21964
21965 if ( $this->is_payments_sandbox() ) {
21966 $trial_params['trial_timestamp'] = time();
21967 $trial_params['trial_token'] = FS_Security::instance()->get_trial_token(
21968 $this->get_plugin(),
21969 $plan,
21970 $trial_params['trial_timestamp']
21971 );
21972 }
21973
21974 $api = $this->get_api_site_scope();
21975 $trial = $api->call( "plans/{$plan->id}/trials.json", 'post', $trial_params );
21976
21977 if ( ! $this->is_api_result_entity( $trial ) ) {
21978 // Some API error while trying to start the trial.
21979 $this->_admin_notices->add(
21980 $this->get_api_error_message( $trial ),
21981 $oops_text,
21982 'error',
21983 $add_sticky_notice
21984 );
21985
21986 return false;
21987 }
21988
21989 // Sync license.
21990 $this->_sync_license();
21991
21992 return $this->is_trial();
21993 }
21994
21995 /**
21996 * Cancel site trial.
21997 *
21998 * @author Vova Feldman (@svovaf)
21999 * @since 1.0.9
22000 *
22001 * @return object
22002 *
22003 * @uses FS_Api
22004 */
22005 private function _cancel_trial() {
22006 $this->_logger->entrance();
22007
22008 if ( ! $this->is_trial() ) {
22009 return (object) array(
22010 'error' => (object) array(
22011 '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' )
22012 )
22013 );
22014 }
22015
22016 $trial_plan = $this->get_trial_plan();
22017
22018 $api = $this->get_api_site_scope();
22019 $site = $api->call( 'trials.json', 'delete' );
22020
22021 $trial_cancelled = false;
22022
22023 if ( $this->is_api_result_entity( $site ) ) {
22024 $prev_trial_ends = $this->_site->trial_ends;
22025
22026 if ( $this->is_paid_trial() ) {
22027 $this->_license->expiration = $site->trial_ends;
22028 $this->_license->is_cancelled = true;
22029 $this->_update_site_license( $this->_license );
22030 $this->_store_licenses();
22031
22032 // Clear subscription reference.
22033 $this->_sync_site_subscription( null );
22034 }
22035
22036 // Update site info.
22037 $this->_site = new FS_Site( $site );
22038
22039 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
22040 } else {
22041 // @todo handle different error cases.
22042 }
22043
22044 if ( ! $trial_cancelled ) {
22045 return (object) array(
22046 'error' => (object) array(
22047 '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' )
22048 )
22049 );
22050 }
22051
22052 // Remove previous sticky messages about upgrade or trial (if exist).
22053 $this->_admin_notices->remove_sticky( array(
22054 'trial_started',
22055 'trial_promotion',
22056 'plan_upgraded',
22057 ) );
22058
22059 // Store site updates.
22060 $this->_store_site();
22061
22062 if ( ! $this->is_addon() ||
22063 ! $this->deactivate_premium_only_addon_without_license( true )
22064 ) {
22065 $this->_admin_notices->add(
22066 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
22067 );
22068 }
22069
22070 return $site;
22071 }
22072
22073 /**
22074 * @author Vova Feldman (@svovaf)
22075 * @since 1.0.6
22076 *
22077 * @param bool|number $plugin_id
22078 *
22079 * @return bool
22080 */
22081 private function _is_addon_id( $plugin_id ) {
22082 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
22083 }
22084
22085 /**
22086 * Check if user eligible to download premium version updates.
22087 *
22088 * @author Vova Feldman (@svovaf)
22089 * @since 1.0.6
22090 *
22091 * @return bool
22092 */
22093 private function _can_download_premium() {
22094 return $this->has_any_active_valid_license() ||
22095 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
22096 }
22097
22098 /**
22099 *
22100 * @author Vova Feldman (@svovaf)
22101 * @since 1.0.6
22102 *
22103 * @param bool|number $addon_id
22104 * @param string $type "json" or "zip"
22105 *
22106 * @return string
22107 */
22108 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
22109
22110 $is_addon = $this->_is_addon_id( $addon_id );
22111
22112 $is_premium = null;
22113 if ( ! $is_addon ) {
22114 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
22115 } else if ( $this->is_addon_activated( $addon_id ) ) {
22116 $fs_addon = self::get_instance_by_id( $addon_id );
22117 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
22118 }
22119
22120 // If add-on, then append add-on ID.
22121 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
22122 '/updates/latest.' . $type;
22123
22124 // If add-on and not yet activated, try to fetch based on server licensing.
22125 if ( is_bool( $is_premium ) ) {
22126 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
22127 }
22128
22129 if ( $this->has_secret_key() ) {
22130 $endpoint = add_query_arg( 'type', 'all', $endpoint );
22131 } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) {
22132 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
22133 }
22134
22135 return $endpoint;
22136 }
22137
22138 /**
22139 * @author Vova Feldman (@svovaf)
22140 * @since 1.0.4
22141 *
22142 * @param bool|number $addon_id
22143 * @param bool $flush Since 1.1.7.3
22144 * @param int $expiration Since 1.2.2.7
22145 * @param bool|string $newer_than Since 2.2.1
22146 * @param bool|string $fetch_readme Since 2.2.1
22147 * @param bool $fetch_upgrade_notice Since 2.12.1
22148 *
22149 * @return object|false Plugin latest tag info.
22150 */
22151 function _fetch_latest_version(
22152 $addon_id = false,
22153 $flush = true,
22154 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22155 $newer_than = false,
22156 $fetch_readme = true,
22157 $fetch_upgrade_notice = false
22158 ) {
22159 $this->_logger->entrance();
22160
22161 if ( $this->is_unresolved_clone( true ) ) {
22162 return false;
22163 }
22164
22165 $switch_to_blog_id = null;
22166
22167 /**
22168 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
22169 * @since 1.1.7.4 Also check updates for add-ons.
22170 */
22171 if (
22172 ( ! $this->is_registered() || ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed() ) &&
22173 ! $this->_is_addon_id( $addon_id )
22174 ) {
22175 if ( ! is_multisite() ) {
22176 return false;
22177 }
22178
22179 $installs_map = $this->get_blog_install_map();
22180
22181 foreach ( $installs_map as $blog_id => $install ) {
22182 if ( ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed( $blog_id ) ) {
22183 continue;
22184 }
22185
22186 /**
22187 * @var FS_Site $install
22188 */
22189 if ( $install->is_trial() ) {
22190 $switch_to_blog_id = $blog_id;
22191 break;
22192 }
22193
22194 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
22195 $license = $this->get_license_by_id( $install->license_id );
22196
22197 if ( is_object( $license ) && $license->is_features_enabled() ) {
22198 $switch_to_blog_id = $blog_id;
22199 break;
22200 }
22201 }
22202 }
22203
22204 if ( is_null( $switch_to_blog_id ) ) {
22205 return false;
22206 }
22207 }
22208
22209 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
22210 get_current_blog_id() :
22211 0;
22212
22213 if ( is_numeric( $switch_to_blog_id ) ) {
22214 $this->switch_to_blog( $switch_to_blog_id );
22215 }
22216
22217 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
22218
22219 if ( ! empty( $newer_than ) ) {
22220 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
22221 }
22222
22223 if ( true === $fetch_readme ) {
22224 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
22225
22226 // Don't cache the API response when fetching readme information.
22227 $expiration = null;
22228 }
22229
22230 if ( true === $fetch_upgrade_notice ) {
22231 $latest_version_endpoint = add_query_arg( 'include_upgrade_notice', 'true', $latest_version_endpoint );
22232 }
22233
22234 $tag = $this->get_api_site_or_plugin_scope()->get(
22235 $latest_version_endpoint,
22236 $flush,
22237 $expiration
22238 );
22239
22240 if ( is_numeric( $switch_to_blog_id ) ) {
22241 $this->switch_to_blog( $current_blog_id );
22242 }
22243
22244 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
22245
22246 $this->_logger->departure( 'Latest version ' . $latest_version );
22247
22248 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
22249 }
22250
22251 #----------------------------------------------------------------------------------
22252 #region Download Plugin
22253 #----------------------------------------------------------------------------------
22254
22255 /**
22256 * Download latest plugin version, based on plan.
22257 *
22258 * Not like _download_latest(), this will redirect the page
22259 * to secure download url to prevent dual download (from FS to WP server,
22260 * and then from WP server to the client / browser).
22261 *
22262 * @author Vova Feldman (@svovaf)
22263 * @since 1.0.9
22264 *
22265 * @param bool|number $plugin_id
22266 *
22267 * @uses FS_Api
22268 * @uses wp_redirect()
22269 */
22270 private function download_latest_directly( $plugin_id = false ) {
22271 $this->_logger->entrance();
22272
22273 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
22274 }
22275
22276 /**
22277 * Get latest plugin FS API download URL.
22278 *
22279 * @author Vova Feldman (@svovaf)
22280 * @since 1.0.9
22281 *
22282 * @param bool|number $plugin_id
22283 *
22284 * @return string
22285 */
22286 private function get_latest_download_api_url( $plugin_id = false ) {
22287 $this->_logger->entrance();
22288
22289 $download_api_url = $this->get_api_site_scope()->get_signed_url(
22290 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
22291 );
22292
22293 return str_replace( 'http:', 'https:', $download_api_url );
22294 }
22295
22296 /**
22297 * Get payment invoice URL.
22298 *
22299 * @author Vova Feldman (@svovaf)
22300 * @since 1.2.0
22301 *
22302 * @param bool|number $payment_id
22303 *
22304 * @return string
22305 */
22306 function _get_invoice_api_url( $payment_id = false ) {
22307 $this->_logger->entrance();
22308
22309 $url = $this->get_api_user_scope()->get_signed_url(
22310 "/payments/{$payment_id}/invoice.pdf"
22311 );
22312
22313 if ( ! fs_starts_with( $url, 'https://' ) ) {
22314 // Always use HTTPS for invoices.
22315 $url = 'https' . substr( $url, 4 );
22316 }
22317
22318 return $url;
22319 }
22320
22321 /**
22322 * Get latest plugin download link.
22323 *
22324 * @author Vova Feldman (@svovaf)
22325 * @since 1.0.9
22326 *
22327 * @param string $label
22328 * @param bool|number $plugin_id
22329 *
22330 * @return string
22331 */
22332 private function get_latest_download_link( $label, $plugin_id = false ) {
22333 return sprintf(
22334 '<a target="_blank" rel="noopener" href="%s">%s</a>',
22335 $this->_get_latest_download_local_url( $plugin_id ),
22336 $label
22337 );
22338 }
22339
22340 /**
22341 * Get latest plugin download local URL.
22342 *
22343 * @author Vova Feldman (@svovaf)
22344 * @since 1.0.9
22345 *
22346 * @param bool|number $plugin_id
22347 *
22348 * @return string
22349 */
22350 function _get_latest_download_local_url( $plugin_id = false ) {
22351 // Add timestamp to protect from caching.
22352 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
22353
22354 if ( ! empty( $plugin_id ) ) {
22355 $params['plugin_id'] = $plugin_id;
22356 } else if ( $this->is_addon() ) {
22357 $params['plugin_id'] = $this->get_id();
22358 }
22359
22360 $fs = $this->is_addon() ?
22361 $this->get_parent_instance() :
22362 $this;
22363
22364 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
22365 }
22366
22367 #endregion Download Plugin ------------------------------------------------------------------
22368
22369 /**
22370 * @author Vova Feldman (@svovaf)
22371 * @since 1.0.4
22372 *
22373 * @uses FS_Api
22374 *
22375 * @param bool $background Hints the method if it's a background updates check. If false, it means that
22376 * was initiated by the admin.
22377 * @param bool|number $plugin_id
22378 * @param bool $flush Since 1.1.7.3
22379 */
22380 private function check_updates( $background = false, $plugin_id = false, $flush = true ) {
22381 $this->_logger->entrance();
22382
22383 $newer_than = ( $this->is_premium() ? $this->get_plugin_version() : false );
22384
22385 // Check if there's a newer version for download.
22386 $new_version = $this->_fetch_newer_version(
22387 $plugin_id,
22388 $flush,
22389 FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION,
22390 $newer_than,
22391 ( false !== $newer_than )
22392 );
22393
22394 $update = null;
22395 if ( is_object( $new_version ) ) {
22396 $update = new FS_Plugin_Tag( $new_version );
22397
22398 if ( ! $background ) {
22399 $this->_admin_notices->add(
22400 sprintf(
22401 /* translators: %s: Numeric version number (e.g. '2.1.9' */
22402 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
22403 $update->version,
22404 sprintf(
22405 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22406 esc_url( $this->get_account_url( 'download_latest' ) ),
22407 sprintf(
22408 /* translators: %s: plan name (e.g. latest "Professional" version) */
22409 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
22410 $this->get_plan_title()
22411 )
22412 )
22413 ),
22414 $this->get_text_inline( 'New', 'new' ) . '!'
22415 );
22416 }
22417 } else if ( false === $new_version && ! $background ) {
22418 $this->_admin_notices->add(
22419 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
22420 $this->get_text_inline( 'You are all good!', 'you-are-good' )
22421 );
22422 }
22423
22424 $this->_store_update( $update, true, $plugin_id );
22425 }
22426
22427 /**
22428 * @author Vova Feldman (@svovaf)
22429 * @since 1.0.4
22430 *
22431 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
22432 *
22433 * @return FS_Plugin[]
22434 *
22435 * @uses FS_Api
22436 */
22437 private function sync_addons( $flush = false ) {
22438 $this->_logger->entrance();
22439
22440 $api = $this->get_api_site_or_plugin_scope();
22441
22442 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
22443
22444 /**
22445 * @since 1.2.1
22446 *
22447 * If there's a cached version of the add-ons and not asking
22448 * for a flush, just use the currently stored add-ons.
22449 */
22450 if ( ! $flush && $api->is_cached( $path ) ) {
22451 $addons = self::get_all_addons();
22452
22453 return isset( $addons[ $this->_plugin->id ] ) ?
22454 $addons[ $this->_plugin->id ] :
22455 array();
22456 }
22457
22458 $result = $api->get( $path, $flush );
22459
22460 $addons = array();
22461 if ( $this->is_api_result_object( $result, 'plugins' ) &&
22462 is_array( $result->plugins )
22463 ) {
22464 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
22465 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
22466 }
22467
22468 $this->_store_addons( $addons, true );
22469 }
22470
22471 return $addons;
22472 }
22473
22474 /**
22475 * Handle user email update.
22476 *
22477 * @author Vova Feldman (@svovaf)
22478 * @since 1.0.3
22479 * @uses FS_Api
22480 *
22481 * @param string $new_email
22482 *
22483 * @return object
22484 */
22485 private function update_email( $new_email ) {
22486 $this->_logger->entrance();
22487
22488 $api = $this->get_api_user_scope();
22489 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
22490 'email' => $new_email,
22491 'after_email_confirm_url' => $this->_get_admin_page_url(
22492 'account',
22493 array( 'fs_action' => 'sync_user' )
22494 ),
22495 ) );
22496
22497 if ( ! isset( $user->error ) ) {
22498 $this->_user->email = $user->email;
22499 $this->_user->is_verified = $user->is_verified;
22500 $this->_store_user();
22501 } else {
22502 // handle different error cases.
22503 }
22504
22505 return $user;
22506 }
22507
22508 #----------------------------------------------------------------------------------
22509 #region API Error Handling
22510 #----------------------------------------------------------------------------------
22511
22512 /**
22513 * @author Vova Feldman (@svovaf)
22514 * @since 1.1.1
22515 *
22516 * @param mixed $result
22517 *
22518 * @return bool Is API result contains an error.
22519 */
22520 private function is_api_error( $result ) {
22521 return FS_Api::is_api_error( $result );
22522 }
22523
22524 /**
22525 * Checks if given API result is a non-empty and not an error object.
22526 *
22527 * @author Vova Feldman (@svovaf)
22528 * @since 1.2.1.5
22529 *
22530 * @param mixed $result
22531 * @param string|null $required_property Optional property we want to verify that is set.
22532 *
22533 * @return bool
22534 */
22535 function is_api_result_object( $result, $required_property = null ) {
22536 return FS_Api::is_api_result_object( $result, $required_property );
22537 }
22538
22539 /**
22540 * Checks if given API result is a non-empty entity object with non-empty ID.
22541 *
22542 * @author Vova Feldman (@svovaf)
22543 * @since 1.2.1.5
22544 *
22545 * @param mixed $result
22546 *
22547 * @return bool
22548 */
22549 private function is_api_result_entity( $result ) {
22550 return FS_Api::is_api_result_entity( $result );
22551 }
22552
22553 #endregion
22554
22555 /**
22556 * Make sure a given argument is an array of a specific type.
22557 *
22558 * @author Vova Feldman (@svovaf)
22559 * @since 1.2.1.5
22560 *
22561 * @param mixed $array
22562 * @param string $class
22563 *
22564 * @return bool
22565 */
22566 private function is_array_instanceof( $array, $class ) {
22567 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
22568 }
22569
22570 /**
22571 * Start install ownership change.
22572 *
22573 * @author Vova Feldman (@svovaf)
22574 * @since 1.1.1
22575 * @uses FS_Api
22576 *
22577 * @param string $candidate_email
22578 * @param string $transfer_type
22579 *
22580 * @return bool Is ownership change successfully initiated.
22581 */
22582 private function init_change_owner( $candidate_email, $transfer_type ) {
22583 $this->_logger->entrance();
22584
22585 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
22586 $install_ids = array();
22587
22588 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
22589 $install = $install_info['install'];
22590
22591 if ( $this->_user->id != $install->user_id ) {
22592 // Skip add-on installs that are not owned by the parent product's install's owner.
22593 continue;
22594 }
22595
22596 $install_ids[ $slug ] = $install->id;
22597 }
22598
22599 $api = $this->get_api_site_scope();
22600 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
22601 'email' => $candidate_email,
22602 'transfer_type' => $transfer_type,
22603 'install_ids' => implode( ',', array_values( $install_ids ) ),
22604 'after_confirm_url' => $this->_get_admin_page_url(
22605 'account',
22606 array( 'fs_action' => 'change_owner' )
22607 ),
22608 ) );
22609
22610 return ! $this->is_api_error( $result );
22611 }
22612
22613 /**
22614 * Handle install ownership change.
22615 *
22616 * @author Vova Feldman (@svovaf)
22617 * @since 1.1.1
22618 * @uses FS_Api
22619 *
22620 * @return bool Was ownership change successfully complete.
22621 */
22622 private function complete_change_owner() {
22623 $this->_logger->entrance();
22624
22625 $install_ids = fs_request_get( 'install_ids' );
22626
22627 if ( ! empty( $install_ids ) ) {
22628 $install_ids = explode( ',', $install_ids );
22629
22630 foreach ( $install_ids as $key => $install_id ) {
22631 if ( ! FS_Site::is_valid_id( $install_id ) ) {
22632 unset( $install_ids[ $key ] );
22633 }
22634 }
22635 }
22636
22637 if ( ! is_array( $install_ids ) ) {
22638 $install_ids = array();
22639 }
22640
22641 $user = new FS_User();
22642 $user->id = fs_request_get( 'user_id' );
22643 $user->public_key = fs_request_get_raw( 'user_public_key' );
22644 $user->secret_key = fs_request_get_raw( 'user_secret_key' );
22645
22646 $prev_user = $this->_user;
22647 $this->_user = $user;
22648
22649 $result = $this->get_api_user_scope( true )->get(
22650 "/installs.json?install_ids=" . implode( ',', $install_ids )
22651 );
22652
22653 $current_blog_sites = self::get_all_sites( $this->get_module_type() );
22654
22655 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22656 $site_id_slug_map = array();
22657
22658 foreach ( $current_blog_sites as $slug => $site ) {
22659 $site_id_slug_map[ $site->id ] = $slug;
22660 }
22661
22662 foreach ( $result->installs as $install ) {
22663 $site = new FS_Site( $install );
22664
22665 if ( ! isset( $site_id_slug_map[ $install->id ] ) ) {
22666 continue;
22667 }
22668
22669 $current_blog_sites[ $site_id_slug_map[ $install->id ] ] = clone $site;
22670
22671 if ( $this->_site->id == $site->id ) {
22672 $this->_site = $site;
22673 }
22674 }
22675 }
22676
22677 // Validate install's user and given user.
22678 if ( $user->id != $this->_site->user_id ) {
22679 $this->_user = $prev_user;
22680
22681 return false;
22682 }
22683
22684 $this->set_account_option( 'sites', $current_blog_sites, true );
22685
22686 // Fetch new user information.
22687 $user_result = $this->get_api_user_scope( true )->get();
22688 $user = new FS_User( $user_result );
22689 $this->_user = $user;
22690
22691 $this->_set_account( $user, $this->_site );
22692
22693 $remove_user = true;
22694 $all_modules_sites = FS_DebugManager::get_all_modules_sites();
22695
22696 foreach ( $all_modules_sites as $sites_by_module_type ) {
22697 foreach ( $sites_by_module_type as $sites_by_slug ) {
22698 foreach ( $sites_by_slug as $site ) {
22699 if ( $prev_user->id == $site->user_id ) {
22700 $remove_user = false;
22701 break;
22702 }
22703 }
22704
22705 if ( ! $remove_user ) {
22706 break;
22707 }
22708 }
22709
22710 if ( ! $remove_user ) {
22711 break;
22712 }
22713 }
22714
22715 if ( $remove_user ) {
22716 $users = self::get_all_users();
22717
22718 if ( isset( $users[ $prev_user->id ] ) ) {
22719 unset( $users[ $prev_user->id ] );
22720 } else {
22721 // If the prev user wasn't found by the key, iterate over the users collection.
22722 foreach ( $users as $key => $user ) {
22723 if ( $user->id == $prev_user->id ) {
22724 unset( $users[ $key ] );
22725 break;
22726 }
22727 }
22728 }
22729
22730 $this->set_account_option( 'users', $users, true );
22731 }
22732
22733 return true;
22734 }
22735
22736 /**
22737 * Completes ownership change by license.
22738 *
22739 * @author Leo Fajardo (@leorw)
22740 * @since 2.3.2
22741 *
22742 * @param number $user_id
22743 * @param array[string]number $install_ids_by_slug_map
22744 *
22745 */
22746 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
22747 $this->_logger->entrance();
22748
22749 $this->sync_user_by_current_install( $user_id );
22750
22751 $result = $this->get_api_user_scope( true )->get(
22752 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
22753 );
22754
22755 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22756 $sites = self::get_all_sites( $this->get_module_type() );
22757 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
22758
22759 foreach ( $result->installs as $install ) {
22760 $site = new FS_Site( $install );
22761
22762 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
22763 }
22764
22765 $this->set_account_option( 'sites', $sites, true );
22766 }
22767 }
22768
22769 /**
22770 * Handle user name update.
22771 *
22772 * @author Vova Feldman (@svovaf)
22773 * @since 1.0.9
22774 * @uses FS_Api
22775 *
22776 * @return object
22777 */
22778 private function update_user_name() {
22779 $this->_logger->entrance();
22780 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
22781
22782 $api = $this->get_api_user_scope();
22783 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
22784 'name' => $name,
22785 ) );
22786
22787 if ( ! isset( $user->error ) ) {
22788 $this->_user->first = $user->first;
22789 $this->_user->last = $user->last;
22790 $this->_store_user();
22791 } else {
22792 // handle different error cases.
22793
22794 }
22795
22796 return $user;
22797 }
22798
22799 /**
22800 * Verify user email.
22801 *
22802 * @author Vova Feldman (@svovaf)
22803 * @since 1.0.3
22804 * @uses FS_Api
22805 */
22806 private function verify_email() {
22807 $this->_handle_account_user_sync();
22808
22809 if ( $this->_user->is_verified() ) {
22810 return;
22811 }
22812
22813 $api = $this->get_api_site_scope();
22814 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
22815 'after_email_confirm_url' => $this->_get_admin_page_url(
22816 'account',
22817 array( 'fs_action' => 'sync_user' )
22818 )
22819 ) );
22820
22821 if ( ! isset( $result->error ) ) {
22822 $this->_admin_notices->add( sprintf(
22823 $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' ),
22824 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), esc_html( $this->_user->email ) )
22825 ) );
22826 } else {
22827 // handle different error cases.
22828
22829 }
22830 }
22831
22832 /**
22833 * @author Vova Feldman (@svovaf)
22834 * @since 1.1.2
22835 *
22836 * @param array $params
22837 * @param bool|null $network
22838 *
22839 * @return string
22840 */
22841 function get_activation_url( $params = array(), $network = null ) {
22842 if ( $this->is_addon() && $this->has_free_plan() ) {
22843 /**
22844 * @author Vova Feldman (@svovaf)
22845 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
22846 */
22847 return $this->get_parent_instance()->get_activation_url( $params );
22848 }
22849
22850 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
22851 }
22852
22853 /**
22854 * @author Vova Feldman (@svovaf)
22855 * @since 1.2.1.5
22856 *
22857 * @param array $params
22858 *
22859 * @return string
22860 */
22861 function get_reconnect_url( $params = array() ) {
22862 $params['fs_action'] = 'reset_anonymous_mode';
22863 $params['fs_unique_affix'] = $this->get_unique_affix();
22864
22865 return $this->get_activation_url( $params );
22866 }
22867
22868 /**
22869 * Get the URL of the page that should be loaded after the user connect
22870 * or skip in the opt-in screen.
22871 *
22872 * @author Vova Feldman (@svovaf)
22873 * @since 1.1.3
22874 *
22875 * @param string $filter Filter name.
22876 * @param array $params Since 1.2.2.7
22877 * @param bool|null $network
22878 *
22879 * @return string
22880 */
22881 function get_after_activation_url( $filter, $params = array(), $network = null ) {
22882 if ( $this->show_opt_in_on_themes_page() &&
22883 ( fs_request_has( 'pending_activation' ) ||
22884 // For cases when the first time path is set, even though it's a WP.org theme.
22885 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
22886 ) {
22887 $first_time_path = '';
22888 } else {
22889 $first_time_path = $this->_menu->get_first_time_path(
22890 fs_is_network_admin() && $this->_is_network_active
22891 );
22892 }
22893
22894 if ( $this->_is_network_active &&
22895 fs_is_network_admin() &&
22896 ! $this->_menu->has_network_menu() &&
22897 $this->is_network_registered()
22898 ) {
22899 $target_url = $this->get_account_url();
22900 } else {
22901 // Default plugin's page.
22902 $target_url = $this->_get_admin_page_url( '', array(), $network );
22903 }
22904
22905 return add_query_arg( $params, $this->apply_filters(
22906 $filter,
22907 empty( $first_time_path ) ?
22908 $target_url :
22909 $first_time_path
22910 ) );
22911 }
22912
22913 /**
22914 * Handle account page updates / edits / actions.
22915 *
22916 * @author Vova Feldman (@svovaf)
22917 * @since 1.0.2
22918 *
22919 */
22920 private function _handle_account_edits() {
22921 if ( ! $this->is_user_admin() ) {
22922 return;
22923 }
22924
22925 $action = fs_get_action();
22926
22927 if ( empty( $action ) ) {
22928 return;
22929 }
22930
22931 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
22932 $install_id = fs_request_get( 'install_id', '' );
22933
22934 // Alias.
22935 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
22936
22937 $is_network_action = $this->is_network_level_action();
22938 $blog_id = $this->is_network_level_site_specific_action();
22939 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
22940
22941 if ( is_numeric( $blog_id ) ) {
22942 $this->switch_to_blog( $blog_id );
22943 } else {
22944 $blog_id = '';
22945 }
22946
22947 switch ( $action ) {
22948 case 'opt_in':
22949 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22950
22951 if ( $is_parent_plugin_action ) {
22952 if ( $is_network_action && ! empty( $blog_id ) ) {
22953 if ( ! $this->is_registered() ) {
22954 $this->install_with_user(
22955 $this->get_network_user(),
22956 false,
22957 false,
22958 false,
22959 false
22960 );
22961
22962 $this->_admin_notices->add(
22963 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
22964 $this->get_text_inline( 'Awesome', 'awesome' )
22965 );
22966 }
22967 }
22968 }
22969 break;
22970
22971 case 'toggle_tracking':
22972 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22973
22974 if ( $is_parent_plugin_action ) {
22975 if ( $is_network_action && ! empty( $blog_id ) ) {
22976 if ( $this->is_registered( true ) ) {
22977 if ( $this->is_tracking_prohibited( $blog_id ) ) {
22978 if ( $this->toggle_site_tracking( true, $blog_id ) ) {
22979 $this->_admin_notices->add(
22980 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>" ),
22981 $this->get_text_inline( 'Thank you!', 'thank-you' )
22982 );
22983 }
22984 } else {
22985 if ( $this->toggle_site_tracking( false, $blog_id ) ) {
22986 $install = $this->get_install_by_blog_id( $blog_id );
22987
22988 $this->_admin_notices->add(
22989 sprintf(
22990 $this->get_text_inline( 'Diagnostic data will no longer be sent from %s to %s.', 'opted-out-successfully' ),
22991 self::get_unfiltered_site_url( $blog_id, true ),
22992 "<b>{$this->get_plugin_title()}</b>"
22993 )
22994 );
22995 }
22996 }
22997 }
22998 }
22999 }
23000
23001 break;
23002
23003 case 'delete_account':
23004 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23005
23006 $is_network_deletion = $is_network_action && empty( $blog_id );
23007
23008 if ( $is_parent_plugin_action ) {
23009 // Delete add-on installs if have any.
23010 $installed_addons = $this->get_installed_addons();
23011 foreach ( $installed_addons as $fs_addon ) {
23012 if ( $is_network_deletion ) {
23013 $fs_addon->delete_network_account_event();
23014 } else {
23015 $fs_addon->delete_account_event();
23016 }
23017 }
23018
23019 if ( $is_network_deletion ) {
23020 $this->delete_network_account_event();
23021 } else {
23022 $this->delete_account_event();
23023 }
23024
23025 // Clear user and site.
23026 $this->_site = null;
23027 $this->_user = null;
23028
23029 $this->maybe_set_slug_and_network_menu_exists_flag();
23030
23031 fs_redirect( $this->get_activation_url() );
23032 } else {
23033 if ( $this->is_addon_activated( $plugin_id ) ) {
23034 $fs_addon = self::get_instance_by_id( $plugin_id );
23035
23036 if ( $is_network_deletion ) {
23037 $fs_addon->delete_network_account_event();
23038 } else {
23039 $fs_addon->delete_account_event();
23040 }
23041
23042 fs_redirect( $this->_get_admin_page_url( 'account' ) );
23043 }
23044 }
23045
23046 return;
23047
23048 case 'downgrade_account':
23049 if ( is_numeric( $blog_id ) ) {
23050 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23051 } else {
23052 check_admin_referer( $action );
23053 }
23054
23055 $switch_to_network_install_blog_after_cancellation = (
23056 is_numeric( $blog_id ) &&
23057 $plugin_id == $this->get_id() &&
23058 ! $this->is_trial()
23059 );
23060
23061 $result = $this->cancel_subscription_or_trial( $plugin_id );
23062 if ( $this->is_api_error( $result ) ) {
23063 $this->_admin_notices->add(
23064 $result->error->message,
23065 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23066 'error'
23067 );
23068 }
23069
23070 if ( $switch_to_network_install_blog_after_cancellation ) {
23071 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23072 }
23073
23074 return;
23075
23076 case 'activate_license':
23077 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23078
23079 $fs = $this;
23080 if ( $plugin_id != $this->get_id() ) {
23081 $fs = $this->is_addon_activated( $plugin_id ) ?
23082 self::get_instance_by_id( $plugin_id ) :
23083 null;
23084 }
23085
23086 if ( is_object( $fs ) ) {
23087 $fs->_activate_license();
23088
23089 /**
23090 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
23091 *
23092 * @author Leo Fajardo (@leorw)
23093 * @since 2.4.0
23094 */
23095 unset( $_REQUEST['plugin_id'] );
23096
23097 if ( $this->is_bundle_license_auto_activation_enabled() ) {
23098 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
23099 }
23100 }
23101
23102 return;
23103
23104 case 'deactivate_license':
23105 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23106
23107 if ( $plugin_id == $this->get_id() ) {
23108 $this->_deactivate_license();
23109
23110 if ( $this->is_only_premium() ) {
23111 // Clear user and site.
23112 $this->_site = null;
23113 $this->_user = null;
23114
23115 if ( ! $is_network_action ) {
23116 fs_redirect( $this->get_activation_url() );
23117 } else if ( is_numeric( $blog_id ) ) {
23118 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23119 }
23120 }
23121 } else {
23122 if ( $this->is_addon_activated( $plugin_id ) ) {
23123 $fs_addon = self::get_instance_by_id( $plugin_id );
23124 $fs_addon->_deactivate_license();
23125 }
23126 }
23127
23128 return;
23129
23130 case 'check_updates':
23131 check_admin_referer( $action );
23132 $this->check_updates();
23133
23134 return;
23135
23136 case 'change_owner':
23137 $state = fs_request_get( 'state', 'init' );
23138 switch ( $state ) {
23139 case 'init':
23140 // The nonce is injected by the error handler in `_email_address_update_ajax_handler` function.
23141 check_admin_referer( 'change_owner' );
23142
23143 $candidate_email = fs_request_get( 'candidate_email' );
23144 $transfer_type = fs_request_get( 'transfer_type' );
23145
23146 if ( $this->init_change_owner( $candidate_email, $transfer_type ) ) {
23147 if ( 'transfer' === $transfer_type ) {
23148 $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>' . esc_html( $this->_user->email ) . '</b>' ) );
23149 } else {
23150 $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>' . esc_html( $this->_user->email ) . '</b>' ) );
23151 }
23152 }
23153 break;
23154 case 'owner_confirmed':
23155 // We cannot (or need not to) check the nonce and referer here, because the link comes from the email sent by our API.
23156 $candidate_email = fs_request_get( 'candidate_email', '' );
23157
23158 if ( ! is_email($candidate_email ) ) {
23159 return;
23160 }
23161
23162 $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>' . esc_html( $candidate_email ) . '</b>' ) );
23163 break;
23164 case 'candidate_confirmed':
23165 // We do not need to validate the authenticity of this request here, because the `complete_change_owner` does that for us through API calls.
23166 if ( $this->complete_change_owner() ) {
23167 $this->_admin_notices->add_sticky(
23168 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . esc_html( $this->_user->email ) . '</b>' ),
23169 'ownership_changed',
23170 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
23171 );
23172 } else {
23173 // @todo Handle failed ownership change message.
23174 }
23175 break;
23176 }
23177
23178 return;
23179
23180 case 'update_user_name':
23181 check_admin_referer( 'update_user_name' );
23182
23183 $result = $this->update_user_name();
23184
23185 if ( isset( $result->error ) ) {
23186 $this->_admin_notices->add(
23187 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
23188 $oops_text,
23189 'error'
23190 );
23191 } else {
23192 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
23193 }
23194
23195 return;
23196
23197 #region Actions that might be called from external links (e.g. email)
23198
23199 /**
23200 * !!IMPORTANT!!: We cannot check for a valid nonce in this region, because the links could be coming from emails.
23201 */
23202
23203 case 'cancel_trial':
23204 $result = $this->cancel_subscription_or_trial( $plugin_id );
23205 if ( $this->is_api_error( $result ) ) {
23206 $this->_admin_notices->add(
23207 $result->error->message,
23208 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23209 'error'
23210 );
23211 }
23212
23213 return;
23214
23215 case 'verify_email':
23216 $this->verify_email();
23217
23218 return;
23219
23220 case 'sync_user':
23221 $this->_handle_account_user_sync();
23222
23223 return;
23224
23225 case $this->get_unique_affix() . '_sync_license':
23226 $this->_sync_license();
23227
23228 return;
23229
23230 case 'download_latest':
23231 $this->download_latest_directly( $plugin_id );
23232
23233 return;
23234
23235 #endregion
23236 }
23237
23238 if ( WP_FS__IS_POST_REQUEST ) {
23239 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
23240 foreach ( $properties as $p ) {
23241 if ( 'update_' . $p === $action ) {
23242 check_admin_referer( $action );
23243
23244 $this->_logger->log( $action );
23245
23246 $site_property = substr( $p, strlen( 'site_' ) );
23247 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
23248 $this->get_site()->{$site_property} = $site_property_value;
23249
23250 // Store account after modification.
23251 $this->_store_site();
23252
23253 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
23254
23255 $this->_admin_notices->add( sprintf(
23256 /* translators: %s: User's account property (e.g. email address, name) */
23257 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
23258 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
23259 ) );
23260
23261 return;
23262 }
23263 }
23264 }
23265 }
23266
23267 /**
23268 * Adds CSS classes for the body tag in the admin.
23269 *
23270 * @param string $classes Space-separated string of class names.
23271 *
23272 * @return string $classes FS Admin body tag class names.
23273 */
23274 public function fs_addons_body_class( $classes ) {
23275 $classes .= ' plugins-php';
23276 return $classes;
23277 }
23278
23279 /**
23280 * Account page resources load.
23281 *
23282 * @author Vova Feldman (@svovaf)
23283 * @since 1.0.6
23284 */
23285 function _account_page_load() {
23286 $this->_logger->entrance();
23287
23288 $this->_logger->info( var_export( $_REQUEST, true ) );
23289
23290 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
23291
23292 if ( $this->has_addons() ) {
23293 wp_enqueue_script( 'plugin-install' );
23294 add_thickbox();
23295 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23296 }
23297
23298 if ( $this->has_paid_plan() &&
23299 ! $this->has_any_license() &&
23300 ! $this->is_sync_executed() &&
23301 $this->is_tracking_allowed()
23302 ) {
23303 /**
23304 * If no licenses found and no sync job was executed during the last 24 hours,
23305 * just execute the sync job right away (blocking execution).
23306 *
23307 * @since 1.1.7.3
23308 */
23309 $this->run_manual_sync();
23310 }
23311
23312 $this->_handle_account_edits();
23313
23314 if (
23315 is_object( $this->_license ) &&
23316 $this->_license->user_id == $this->_user->id &&
23317 ! $this->is_whitelabeled( true )
23318 ) {
23319 $this->_admin_notices->add(
23320 sprintf(
23321 $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' ),
23322 sprintf(
23323 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
23324 $this->get_text_inline( 'Click here', 'click-here' )
23325 )
23326 ),
23327 '',
23328 'success',
23329 false,
23330 'license_not_whitelabeled'
23331 );
23332 }
23333
23334 $this->do_action( 'account_page_load_before_departure' );
23335 }
23336
23337 /**
23338 * Renders the "Affiliation" page.
23339 *
23340 * @author Leo Fajardo (@leorw)
23341 * @since 1.2.3
23342 */
23343 function _affiliation_page_render() {
23344 $this->_logger->entrance();
23345
23346 $this->fetch_affiliate_and_terms();
23347
23348 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
23349
23350 $is_bundle_context = $this->has_bundle_context();
23351
23352 $plugin_title = $this->get_plugin_title();
23353
23354 if ( $is_bundle_context ) {
23355 $plugin_title = $this->plugin_affiliate_terms->plugin_title;
23356
23357 // Add the suffix "Bundle" only if the word is not present in the title itself.
23358 if ( false === mb_stripos( $plugin_title, fs_text_inline( 'Bundle', 'bundle' ) ) ) {
23359 $plugin_title = $this->apply_filters(
23360 'formatted_bundle_title',
23361 $plugin_title . ' ' . fs_text_inline( 'Bundle', 'bundle' )
23362 );
23363 }
23364 }
23365
23366 $vars = array(
23367 'id' => $this->_module_id,
23368 'plugin_title' => $plugin_title,
23369 );
23370 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
23371 }
23372
23373
23374 /**
23375 * Render account page.
23376 *
23377 * @author Vova Feldman (@svovaf)
23378 * @since 1.0.0
23379 */
23380 function _account_page_render() {
23381 $this->_logger->entrance();
23382
23383 $template = 'account.php';
23384 $vars = array( 'id' => $this->_module_id );
23385
23386 /**
23387 * Added filter to the template to allow developers wrapping the template
23388 * in custom HTML (e.g. within a wizard/tabs).
23389 *
23390 * @author Vova Feldman (@svovaf)
23391 * @since 1.2.1.6
23392 */
23393 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
23394 }
23395
23396 /**
23397 * Render account connect page.
23398 *
23399 * @author Vova Feldman (@svovaf)
23400 * @since 1.0.7
23401 */
23402 function _connect_page_render() {
23403 $this->_logger->entrance();
23404
23405 $vars = array( 'id' => $this->_module_id );
23406
23407 /**
23408 * Added filter to the template to allow developers wrapping the template
23409 * in custom HTML (e.g. within a wizard/tabs).
23410 *
23411 * @author Vova Feldman (@svovaf)
23412 * @since 1.2.1.6
23413 */
23414 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
23415 }
23416
23417 /**
23418 * Load required resources before add-ons page render.
23419 *
23420 * @author Vova Feldman (@svovaf)
23421 * @since 1.0.6
23422 */
23423 function _addons_page_load() {
23424 $this->_logger->entrance();
23425
23426 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
23427
23428 wp_enqueue_script( 'plugin-install' );
23429 add_thickbox();
23430 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23431
23432 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
23433 $this->_admin_notices->add(
23434 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>' ),
23435 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
23436 'update-nag'
23437 );
23438 }
23439 }
23440
23441 /**
23442 * Render add-ons page.
23443 *
23444 * @author Vova Feldman (@svovaf)
23445 * @since 1.0.6
23446 */
23447 function _addons_page_render() {
23448 $this->_logger->entrance();
23449
23450 $vars = array( 'id' => $this->_module_id );
23451
23452 /**
23453 * Added filter to the template to allow developers wrapping the template
23454 * in custom HTML (e.g. within a wizard/tabs).
23455 *
23456 * @author Vova Feldman (@svovaf)
23457 * @since 1.2.1.6
23458 */
23459 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
23460 }
23461
23462 /* Pricing & Upgrade
23463 ------------------------------------------------------------------------------------------------------------------*/
23464 /**
23465 * Render pricing page.
23466 *
23467 * @author Vova Feldman (@svovaf)
23468 * @since 1.0.0
23469 */
23470 function _pricing_page_render() {
23471 $this->_logger->entrance();
23472
23473 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
23474 fs_enqueue_local_style( 'fs_checkout', '/admin/checkout.css' );
23475
23476 $vars = array( 'id' => $this->_module_id );
23477
23478 if ( 'true' === fs_request_get( 'checkout', false ) ) {
23479 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
23480 } else {
23481 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
23482 }
23483 }
23484
23485 /**
23486 * @author Leo Fajardo (@leorw)
23487 * @since 2.3.1
23488 */
23489 function _add_pricing_ajax_handler() {
23490 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
23491 }
23492
23493 /**
23494 * @author Leo Fajardo (@leorw)
23495 * @since 2.3.1
23496 */
23497 function _fs_pricing_ajax_action_handler() {
23498 $this->check_ajax_referer( 'pricing_ajax_action' );
23499
23500 $result = null;
23501 $pricing_action = fs_request_get( 'pricing_action' );
23502
23503 switch ( $pricing_action ) {
23504 case 'fetch_pricing_data':
23505 $params = array(
23506 'is_enriched' => true,
23507 'trial' => fs_request_get_bool( 'trial' ),
23508 'sandbox' => fs_request_get_raw( 'sandbox' ),
23509 's_ctx_type' => fs_request_get_raw( 's_ctx_type' ),
23510 's_ctx_id' => fs_request_get_raw( 's_ctx_id' ),
23511 's_ctx_ts' => fs_request_get_raw( 's_ctx_ts' ),
23512 's_ctx_secure' => fs_request_get_raw( 's_ctx_secure' ),
23513 );
23514
23515 $bundle_id = $this->get_bundle_id();
23516 $bundle_public_key = $this->get_bundle_public_key();
23517
23518 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
23519
23520 if ( ! $has_bundle_context ) {
23521 $api = $this->get_api_plugin_scope();
23522 } else {
23523 $api = FS_Api::instance(
23524 $bundle_id,
23525 'plugin',
23526 $bundle_id,
23527 $bundle_public_key,
23528 ! $this->is_live(),
23529 false,
23530 $this->get_sdk_version()
23531 );
23532
23533 $params['plugin_id'] = $this->get_id();
23534 $params['plugin_public_key'] = $this->get_public_key();
23535 }
23536
23537 $result = $api->get( $this->add_show_pending( 'pricing.json?' . http_build_query( $params ) ) );
23538 break;
23539 case 'start_trial':
23540 $trial_plan_id = fs_request_get( 'plan_id' );
23541
23542 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
23543 $plan = $this->_get_plan_by_id( $trial_plan_id );
23544
23545 if ( ! $plan ) {
23546 $this->shoot_ajax_failure( 'Invalid plan ID.' );
23547 return;
23548 }
23549
23550 $result = $this->start_trial( $plan->name, true );
23551 } else {
23552 // @todo - This fails for sandbox trial at the moment if the trial was already utilized.
23553 $result = $this->opt_in(
23554 false,
23555 false,
23556 false,
23557 false,
23558 false,
23559 $trial_plan_id
23560 );
23561 }
23562 }
23563
23564 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
23565 $this->_logger->api_error( $result );
23566
23567 self::shoot_ajax_failure(
23568 isset( $result->error ) ?
23569 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
23570 var_export( $result, true )
23571 );
23572 }
23573
23574 $this->shoot_ajax_success( $result );
23575 }
23576
23577 #----------------------------------------------------------------------------------
23578 #region Contact Us
23579 #----------------------------------------------------------------------------------
23580
23581 /**
23582 * Render contact-us page.
23583 *
23584 * @author Vova Feldman (@svovaf)
23585 * @since 1.0.3
23586 */
23587 function _contact_page_render() {
23588 $this->_logger->entrance();
23589
23590 $vars = array( 'id' => $this->_module_id );
23591
23592 /**
23593 * Added filter to the template to allow developers wrapping the template
23594 * in custom HTML (e.g. within a wizard/tabs).
23595 *
23596 * @author Vova Feldman (@svovaf)
23597 * @since 2.1.3
23598 */
23599 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
23600 }
23601
23602 #endregion ------------------------------------------------------------------------
23603
23604 /**
23605 * Hide all admin notices to prevent distractions.
23606 *
23607 * @author Vova Feldman (@svovaf)
23608 * @since 1.0.3
23609 *
23610 * @uses remove_all_actions()
23611 */
23612 private static function _hide_admin_notices() {
23613 remove_all_actions( 'admin_notices' );
23614 remove_all_actions( 'network_admin_notices' );
23615 remove_all_actions( 'all_admin_notices' );
23616 remove_all_actions( 'user_admin_notices' );
23617 }
23618
23619 static function _clean_admin_content_section_hook() {
23620 $hide_admin_notices = true;
23621
23622 if ( fs_request_is_action( 'allow_clone_resolution_notice' ) ) {
23623 check_admin_referer( 'fs_allow_clone_resolution_notice' );
23624
23625 $hide_admin_notices = false;
23626 }
23627
23628 if ( $hide_admin_notices ) {
23629 self::_hide_admin_notices();
23630 }
23631
23632 // Hide footer.
23633 echo '<style>#wpfooter { display: none !important; }</style>';
23634 }
23635
23636 /**
23637 * Attach to admin_head hook to hide all admin notices.
23638 *
23639 * @author Vova Feldman (@svovaf)
23640 * @since 1.0.3
23641 */
23642 static function _clean_admin_content_section() {
23643 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
23644 }
23645
23646 /* CSS & JavaScript
23647 ------------------------------------------------------------------------------------------------------------------*/
23648 /* function _enqueue_script($handle, $src) {
23649 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
23650
23651 $this->_logger->entrance( 'script = ' . $url );
23652
23653 wp_enqueue_script( $handle, $url );
23654 }*/
23655
23656 /* SDK
23657 ------------------------------------------------------------------------------------------------------------------*/
23658 private $_user_api;
23659
23660 /**
23661 *
23662 * @author Vova Feldman (@svovaf)
23663 * @since 1.0.2
23664 *
23665 * @param bool $flush
23666 *
23667 * @return FS_Api
23668 */
23669 function get_api_user_scope( $flush = false ) {
23670 if ( ! isset( $this->_user_api ) || $flush ) {
23671 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
23672 }
23673
23674 return $this->_user_api;
23675 }
23676
23677 /**
23678 * @author Vova Feldman (@svovaf)
23679 * @since 2.0.0
23680 *
23681 * @param \FS_User $user
23682 *
23683 * @return \FS_Api
23684 */
23685 private function get_api_user_scope_by_user( FS_User $user ) {
23686 return FS_Api::instance(
23687 $this->_module_id,
23688 'user',
23689 $user->id,
23690 $user->public_key,
23691 ! $this->is_live(),
23692 $user->secret_key,
23693 $this->get_sdk_version()
23694 );
23695 }
23696
23697 /**
23698 *
23699 * @author Leo Fajardo (@leorw)
23700 * @since 2.0.0
23701 *
23702 * @param bool $flush
23703 *
23704 * @return FS_Api
23705 */
23706 private function get_current_or_network_user_api_scope( $flush = false ) {
23707 if ( ! $this->_is_network_active ||
23708 ( isset( $this->_user ) && $this->_user instanceof FS_User )
23709 ) {
23710 return $this->get_api_user_scope( $flush );
23711 }
23712
23713 $user = $this->get_current_or_network_user();
23714
23715 $this->_user_api = FS_Api::instance(
23716 $this->_module_id,
23717 'user',
23718 $user->id,
23719 $user->public_key,
23720 ! $this->is_live(),
23721 $user->secret_key,
23722 $this->get_sdk_version()
23723 );
23724
23725 return $this->_user_api;
23726 }
23727
23728 private $_site_api;
23729
23730 /**
23731 *
23732 * @author Vova Feldman (@svovaf)
23733 * @since 1.0.2
23734 *
23735 * @param bool $flush
23736 *
23737 * @return FS_Api
23738 */
23739 private function get_api_site_scope( $flush = false ) {
23740 if ( ! isset( $this->_site_api ) || $flush ) {
23741 $this->_site_api = FS_Api::instance(
23742 $this->_module_id,
23743 'install',
23744 $this->_site->id,
23745 $this->_site->public_key,
23746 ! $this->is_live(),
23747 $this->_site->secret_key,
23748 $this->get_sdk_version(),
23749 self::get_unfiltered_site_url()
23750 );
23751 }
23752
23753 return $this->_site_api;
23754 }
23755
23756 /**
23757 * @author Leo Fajardo (@leorw)
23758 * @since 2.5.0
23759 *
23760 * @param string $path
23761 * @param string $method
23762 * @param array $params
23763 * @param bool $flush_instance
23764 *
23765 * @return array|mixed|string|void
23766 * @throws Freemius_Exception
23767 */
23768 private function api_site_call( $path, $method = 'GET', $params = array(), $flush_instance = false ) {
23769 $result = $this->get_api_site_scope( $flush_instance )->call( $path, $method, $params );
23770
23771 /**
23772 * 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.
23773 *
23774 * @author Leo Fajardo (@leorw)
23775 * @since 2.5.0
23776 */
23777 if (
23778 $this->is_registered() &&
23779 FS_Api::is_api_result_entity( $result ) &&
23780 isset( $result->url )
23781 ) {
23782 $stored_local_url = trailingslashit( $this->_site->url );
23783 $stored_remote_url = trailingslashit( $result->url );
23784
23785 if ( $stored_local_url !== $stored_remote_url ) {
23786 $this->_site->url = $result->url;
23787 $this->_store_site();
23788 }
23789
23790 if ( fs_strip_url_protocol( $stored_remote_url ) !== self::get_unfiltered_site_url( null, true, true ) ) {
23791 FS_Clone_Manager::instance()->maybe_run_clone_resolution();
23792 }
23793 }
23794
23795 return $result;
23796 }
23797
23798 private $_plugin_api;
23799
23800 /**
23801 * Get plugin public API scope.
23802 *
23803 * @author Vova Feldman (@svovaf)
23804 * @since 1.0.7
23805 *
23806 * @return FS_Api
23807 */
23808 function get_api_plugin_scope() {
23809 if ( ! isset( $this->_plugin_api ) ) {
23810 $this->_plugin_api = FS_Api::instance(
23811 $this->_module_id,
23812 'plugin',
23813 $this->_plugin->id,
23814 $this->_plugin->public_key,
23815 ! $this->is_live(),
23816 false,
23817 $this->get_sdk_version()
23818 );
23819 }
23820
23821 return $this->_plugin_api;
23822 }
23823
23824 /**
23825 * Get bundle public API scope.
23826 *
23827 * @author Vova Feldman (@svovaf)
23828 * @since 2.3.1
23829 *
23830 * @return FS_Api
23831 */
23832 function get_api_bundle_scope() {
23833 return FS_Api::instance(
23834 $this->get_bundle_id(),
23835 'plugin',
23836 $this->get_bundle_id(),
23837 $this->get_bundle_public_key(),
23838 ! $this->is_live(),
23839 false,
23840 $this->get_sdk_version()
23841 );
23842 }
23843
23844 /**
23845 * Get site API scope object (fallback to public plugin scope when not registered).
23846 *
23847 * @author Vova Feldman (@svovaf)
23848 * @since 1.0.7
23849 *
23850 * @return FS_Api
23851 */
23852 function get_api_site_or_plugin_scope() {
23853 return $this->is_registered() ?
23854 $this->get_api_site_scope() :
23855 $this->get_api_plugin_scope();
23856 }
23857
23858 /**
23859 * @author Leo Fajardo (@leorw)
23860 * @since 2.2.3.1
23861 *
23862 * @param object $result
23863 */
23864 private function maybe_modify_api_curl_error_message( $result ) {
23865 if (
23866 'cUrlMissing' !== $result->error->type &&
23867 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
23868 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
23869 ) {
23870 return;
23871 }
23872
23873 $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' ) .
23874 ' ' .
23875 $this->esc_html_inline(
23876 sprintf(
23877 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
23878 implode(
23879 ', ',
23880 $this->apply_filters( 'api_domains', array(
23881 'api.freemius.com',
23882 'wp.freemius.com'
23883 ) )
23884 )
23885 ),
23886 'connectivity-whitelist'
23887 ) .
23888 ' ' .
23889 sprintf(
23890 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
23891 $this->get_module_type()
23892 );
23893 }
23894
23895 /**
23896 * Show trial promotional notice (if any trial exist).
23897 *
23898 * @author Vova Feldman (@svovaf)
23899 * @since 1.0.9
23900 *
23901 * @param FS_Plugin_Plan[] $plans
23902 */
23903 function _check_for_trial_plans( $plans ) {
23904 /**
23905 * 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.
23906 *
23907 * @author Vova Feldman (@svovaf)
23908 * @since 2.1.2
23909 */
23910 if ( ! is_array( $plans ) && is_object( $plans ) ) {
23911 $plans = array( $plans );
23912 }
23913
23914 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
23915 $plans = array();
23916 }
23917
23918 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
23919 }
23920
23921 /**
23922 * During trial promotion the "upgrade" submenu item turns to
23923 * "start trial" to encourage the trial. Since we want to keep
23924 * the same menu item handler and there's no robust way to
23925 * add new arguments to the menu item link's querystring,
23926 * use JavaScript to find the menu item and update the href of
23927 * the link.
23928 *
23929 * @author Vova Feldman (@svovaf)
23930 * @since 1.2.1.5
23931 */
23932 function _fix_start_trial_menu_item_url() {
23933 $template_args = array( 'id' => $this->_module_id );
23934 fs_require_template( 'add-trial-to-pricing.php', $template_args );
23935 }
23936
23937 /**
23938 * Check if module is currently in a trial promotion mode.
23939 *
23940 * @author Vova Feldman (@svovaf)
23941 * @since 1.2.2.7
23942 *
23943 * @return bool
23944 */
23945 function is_in_trial_promotion() {
23946 return $this->_admin_notices->has_sticky( 'trial_promotion' );
23947 }
23948
23949 /**
23950 * Show trial promotional notice (if any trial exist).
23951 *
23952 * @author Vova Feldman (@svovaf)
23953 * @since 1.0.9
23954 *
23955 * @return bool If trial notice added.
23956 */
23957 function _add_trial_notice() {
23958 if ( ! $this->is_user_admin() ) {
23959 return false;
23960 }
23961
23962 if ( ! $this->is_user_in_admin() ) {
23963 return false;
23964 }
23965
23966 if ( $this->_is_network_active ) {
23967 if ( fs_is_network_admin() ) {
23968 // Network level trial is disabled at the moment.
23969 return false;
23970 }
23971
23972 if ( ! $this->is_delegated_connection() ) {
23973 // Only delegated sites should support trials.
23974 return false;
23975 }
23976 }
23977
23978 // Check if trial message is already shown.
23979 if ( $this->is_in_trial_promotion() ) {
23980 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
23981
23982 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
23983
23984 return false;
23985 }
23986
23987 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
23988 // Don't show trial if running the premium code, unless running in DEV mode.
23989 return false;
23990 }
23991
23992 if ( ! $this->has_trial_plan() ) {
23993 // No plans with trial.
23994 return false;
23995 }
23996
23997 if ( ! $this->apply_filters( 'show_trial', true ) ) {
23998 // Developer explicitly asked not to show the trial promo.
23999 return false;
24000 }
24001
24002 if ( $this->is_registered() ) {
24003 // Check if trial already utilized.
24004 if ( $this->_site->is_trial_utilized() ) {
24005 return false;
24006 }
24007
24008 if ( $this->is_paying_or_trial() ) {
24009 // Don't show trial if paying or already in trial.
24010 return false;
24011 }
24012 }
24013
24014 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24015 // If not yet opted-in/skipped, or pending activation, don't show trial.
24016 return false;
24017 }
24018
24019 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
24020 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
24021
24022 // Show promotion if never shown before and 24 hours after initial activation with FS.
24023 if ( ! $was_promotion_shown_before &&
24024 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
24025 ) {
24026 return false;
24027 }
24028
24029 // OR if promotion was shown before, try showing it every 30 days.
24030 if ( $was_promotion_shown_before &&
24031 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
24032 ) {
24033 return false;
24034 }
24035
24036 $trial_period = $this->_trial_days;
24037 $require_payment = $this->_is_trial_require_payment;
24038 $trial_url = $this->get_trial_url();
24039 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
24040
24041 if ( $this->is_registered() ) {
24042 // If opted-in, override trial with up to date data from API.
24043 $trial_plans = FS_Plan_Manager::instance()->get_visible_trial_plans( $this->_plans );
24044 $trial_plans_count = count( $trial_plans );
24045
24046 if ( 0 === $trial_plans_count ) {
24047 // If there's no plans with a trial just exit.
24048 return false;
24049 }
24050
24051 /**
24052 * @var FS_Plugin_Plan $paid_plan
24053 */
24054 $paid_plan = $trial_plans[0];
24055 $require_payment = $paid_plan->is_require_subscription;
24056 $trial_period = $paid_plan->trial_period;
24057
24058 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
24059
24060 if ( $total_paid_plans !== $trial_plans_count ) {
24061 // Not all paid plans have a trial - generate a string of those that have it.
24062 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
24063 $plans_string .= sprintf(
24064 ' <a href="%s">%s</a>',
24065 $trial_url,
24066 $trial_plans[ $i ]->title
24067 );
24068
24069 if ( $i < $trial_plans_count - 2 ) {
24070 $plans_string .= ', ';
24071 } else if ( $i == $trial_plans_count - 2 ) {
24072 $plans_string .= ' and ';
24073 }
24074 }
24075 }
24076 }
24077
24078 $message = sprintf(
24079 $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' ),
24080 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
24081 $plans_string,
24082 $trial_period
24083 );
24084
24085 // "No Credit-Card Required" or "No Commitment for N Days".
24086 $cc_string = $require_payment ?
24087 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
24088 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
24089
24090
24091 // Start trial button.
24092 $button = ' ' . sprintf(
24093 '<div><a class="button button-primary" href="%s">%s &nbsp;&#10140;</a></div>',
24094 $trial_url,
24095 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
24096 );
24097
24098 $message_text = $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string}" );
24099
24100 $this->_admin_notices->add_sticky(
24101 "<div class=\"fs-trial-message-container\"><div>{$message_text}</div> {$button}</div>",
24102 'trial_promotion',
24103 '',
24104 'promotion'
24105 );
24106
24107 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
24108
24109 return true;
24110 }
24111
24112 /**
24113 * Lets users/customers know that the product has an affiliate program.
24114 *
24115 * @author Leo Fajardo (@leorw)
24116 * @since 1.2.2.11
24117 *
24118 * @return bool Returns true if the notice has been added.
24119 */
24120 function _add_affiliate_program_notice() {
24121 if ( ! $this->is_user_admin() ) {
24122 return false;
24123 }
24124
24125 if ( ! $this->is_user_in_admin() ) {
24126 return false;
24127 }
24128
24129 // Check if the notice is already shown.
24130 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
24131 return false;
24132 }
24133
24134 if (
24135 // Product has no affiliate program.
24136 ! $this->has_affiliate_program() ||
24137 // User has applied for an affiliate account.
24138 ! empty( $this->_storage->affiliate_application_data )
24139 ) {
24140 return false;
24141 }
24142
24143 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
24144 // Developer explicitly asked not to show the notice about the affiliate program.
24145 return false;
24146 }
24147
24148 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24149 // If not yet opted in/skipped, or pending activation, don't show the notice.
24150 return false;
24151 }
24152
24153 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
24154 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
24155
24156 /**
24157 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
24158 * activation with FS.
24159 */
24160 if ( $was_notice_shown_before ||
24161 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
24162 ) {
24163 return false;
24164 }
24165
24166 if ( ! $this->is_paying() &&
24167 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
24168 ) {
24169 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
24170 return false;
24171 }
24172
24173 $message = sprintf(
24174 $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' ),
24175 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
24176 $this->get_module_label( true )
24177 );
24178
24179 // HTML code for the "Learn more..." button.
24180 $button = ' ' . sprintf(
24181 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24182 $this->_get_admin_page_url( 'affiliation' ),
24183 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
24184 );
24185
24186 $this->_admin_notices->add_sticky(
24187 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
24188 'affiliate_program',
24189 '',
24190 'promotion'
24191 );
24192
24193 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
24194
24195 return true;
24196 }
24197
24198 /**
24199 * @author Vova Feldman (@svovaf)
24200 * @since 1.2.1.5
24201 */
24202 function _enqueue_common_css() {
24203 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
24204 // Add basic CSS for admin-notices and menu-item colors.
24205 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
24206 }
24207 }
24208
24209 /**
24210 * @author Leo Fajardo (@leorw)
24211 * @since 1.2.2
24212 */
24213 function _show_theme_activation_optin_dialog() {
24214 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
24215
24216 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
24217 }
24218
24219 /**
24220 * @author Leo Fajardo (@leorw)
24221 * @since 1.2.2
24222 */
24223 function _add_fs_theme_activation_dialog() {
24224 global $pagenow;
24225
24226 if ( 'themes.php' !== $pagenow ) {
24227 return;
24228 }
24229
24230 $vars = array( 'id' => $this->_module_id );
24231 fs_require_once_template( 'connect.php', $vars );
24232 }
24233
24234 /* Action Links
24235 ------------------------------------------------------------------------------------------------------------------*/
24236 private $_action_links_hooked = false;
24237 private $_action_links = array();
24238
24239 /**
24240 * Hook to plugin action links filter.
24241 *
24242 * @author Vova Feldman (@svovaf)
24243 * @since 1.0.0
24244 */
24245 private function hook_plugin_action_links() {
24246 $this->_logger->entrance();
24247
24248 $this->_action_links_hooked = true;
24249
24250 $this->_logger->log( 'Adding action links hooks.' );
24251
24252 // Add action link to settings page.
24253 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
24254 &$this,
24255 '_modify_plugin_action_links_hook'
24256 ), WP_FS__DEFAULT_PRIORITY, 2 );
24257 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
24258 &$this,
24259 '_modify_plugin_action_links_hook'
24260 ), WP_FS__DEFAULT_PRIORITY, 2 );
24261 }
24262
24263 /**
24264 * Add plugin action link.
24265 *
24266 * @author Vova Feldman (@svovaf)
24267 * @since 1.0.0
24268 *
24269 * @param $label
24270 * @param $url
24271 * @param bool $external
24272 * @param int $priority
24273 * @param bool $key
24274 */
24275 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
24276 $this->_logger->entrance();
24277
24278 if ( ! isset( $this->_action_links[ $priority ] ) ) {
24279 $this->_action_links[ $priority ] = array();
24280 }
24281
24282 if ( false === $key ) {
24283 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
24284 }
24285
24286 $this->_action_links[ $priority ][] = array(
24287 'label' => $label,
24288 'href' => $url,
24289 'key' => $key,
24290 'external' => $external
24291 );
24292 }
24293
24294 /**
24295 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
24296 *
24297 * @author Vova Feldman (@svovaf)
24298 * @since 1.0.0
24299 */
24300 function _add_upgrade_action_link() {
24301 $this->_logger->entrance();
24302
24303 $is_activation_mode = $this->is_activation_mode();
24304
24305 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
24306
24307 /**
24308 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
24309 * and "Add-Ons" menus should be added.
24310 *
24311 * @author Leo Fajardo (@leorw)
24312 * @since 2.3.0
24313 */
24314 $add_upgrade_link = (
24315 $add_action_links ||
24316 ( $is_activation_mode && $this->is_only_premium() )
24317 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
24318
24319 $add_addons_link = ( $add_action_links && $this->has_addons() );
24320
24321 if ( ! $add_upgrade_link && ! $add_addons_link ) {
24322 return;
24323 }
24324
24325 if (
24326 $add_upgrade_link &&
24327 $this->is_pricing_page_visible() &&
24328 $this->is_submenu_item_visible( 'pricing' )
24329 ) {
24330 $this->add_plugin_action_link(
24331 $this->get_text_inline( 'Upgrade', 'upgrade' ),
24332 $this->get_upgrade_url(),
24333 false,
24334 7,
24335 'upgrade'
24336 );
24337 }
24338
24339 if (
24340 $add_addons_link &&
24341 $this->has_addons() &&
24342 $this->is_submenu_item_visible( 'addons' )
24343 ) {
24344 $this->add_plugin_action_link(
24345 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
24346 $this->_get_admin_page_url( 'addons' ),
24347 false,
24348 9,
24349 'addons'
24350 );
24351 }
24352 }
24353
24354 /**
24355 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
24356 *
24357 * @author Leo Fajardo (@leorw)
24358 * @since 1.1.9
24359 */
24360 function _add_license_action_link() {
24361 $this->_logger->entrance();
24362
24363 if ( ! self::is_ajax() ) {
24364 // Inject license activation dialog UI and client side code.
24365 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
24366 }
24367
24368 $link_text = $this->is_free_plan() ?
24369 $this->get_text_inline( 'Activate License', 'activate-license' ) :
24370 $this->get_text_inline( 'Change License', 'change-license' );
24371
24372 $this->add_plugin_action_link(
24373 $link_text,
24374 '#',
24375 false,
24376 11,
24377 ( 'activate-license ' . $this->get_unique_affix() )
24378 );
24379 }
24380
24381 /**
24382 * @author Leo Fajardo (@leorw)
24383 * @since 2.0.2
24384 */
24385 function _add_premium_version_upgrade_selection_action() {
24386 $this->_logger->entrance();
24387
24388 if ( ! self::is_ajax() ) {
24389 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
24390 }
24391 }
24392
24393 /**
24394 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
24395 *
24396 * @author Leo Fajardo (@leorw)
24397 * @since 1.2.1.5
24398 */
24399 function _add_tracking_links() {
24400 if ( ! current_user_can( 'manage_options' ) ) {
24401 return;
24402 }
24403
24404 $this->_logger->entrance();
24405
24406 if ( $this->is_only_premium() && $this->is_free_plan() ) {
24407 // 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.
24408 return;
24409 }
24410
24411 if (
24412 $this->is_addon() &&
24413 ! $this->is_only_premium()
24414 ) {
24415 $parent = $this->get_parent_instance();
24416
24417 if ( is_object( $parent ) && $parent->is_anonymous() ) {
24418 return;
24419 }
24420 }
24421
24422 if ( fs_is_network_admin() ) {
24423 if ( ! $this->_is_network_active ) {
24424 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
24425 return;
24426 } else if ( $this->is_network_delegated_connection() ) {
24427 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
24428 return;
24429 }
24430 } else {
24431 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
24432 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
24433 return;
24434 }
24435 }
24436
24437 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
24438 if ( ! $this->is_registered() && $this->is_anonymous() ) {
24439 $this->connect_again();
24440
24441 return;
24442 }
24443 }
24444
24445 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
24446 ( $this->is_theme() && ! self::is_themes_page() )
24447 ) {
24448 // Only show tracking links on the plugins and themes pages.
24449 return;
24450 }
24451
24452 if (
24453 $this->is_activation_mode() &&
24454 $this->is_premium() &&
24455 ! $this->is_registered()
24456 ) {
24457 // If not yet registered and running the premium code base, a license activation link will already be shown.
24458 return;
24459 }
24460
24461 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24462 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
24463 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
24464 return;
24465 }
24466 }
24467
24468 if ( $this->add_ajax_action( 'toggle_permission_tracking', array( &$this, '_toggle_permission_tracking_callback' ) ) ) {
24469 return;
24470 }
24471
24472 $link_text_id = '';
24473 $url = '#';
24474
24475 if ( $this->is_registered( true ) ) {
24476 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24477 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
24478 } else {
24479 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24480 }
24481 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
24482 /**
24483 * Show opt-in link only if skipped or in activation mode.
24484 */
24485 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24486
24487 $params = ! $this->is_anonymous() ?
24488 array() :
24489 array(
24490 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
24491 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
24492 );
24493
24494 $url = $this->get_activation_url( $params );
24495 }
24496
24497 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
24498
24499 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
24500 $this->add_plugin_action_link(
24501 $link_text_id,
24502 $url,
24503 false,
24504 13,
24505 "opt-in-or-opt-out {$this->_slug}"
24506 );
24507 }
24508 }
24509
24510 /**
24511 * Get the URL of the page that should be loaded right after the plugin activation.
24512 *
24513 * @author Vova Feldman (@svovaf)
24514 * @since 1.1.7.4
24515 *
24516 * @return string
24517 */
24518 function get_after_plugin_activation_redirect_url() {
24519 $url = false;
24520
24521 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
24522 $first_time_path = $this->_menu->get_first_time_path(
24523 fs_is_network_admin() && $this->_is_network_active
24524 );
24525
24526 if ( $this->is_activation_mode() ) {
24527 $url = $this->get_activation_url();
24528 } else if ( ! empty( $first_time_path ) ) {
24529 $url = $first_time_path;
24530 } else {
24531 $page = '';
24532 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
24533 if ( $this->is_network_registered() ) {
24534 $page = 'account';
24535 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
24536 $this->maybe_set_slug_and_network_menu_exists_flag();
24537 }
24538 }
24539
24540 $url = $this->_get_admin_page_url( $page );
24541 }
24542 } else {
24543 $plugin_fs = false;
24544
24545 if ( $this->is_parent_plugin_installed() ) {
24546 $plugin_fs = self::get_parent_instance();
24547 }
24548
24549 if ( is_object( $plugin_fs ) ) {
24550 if ( ! $plugin_fs->is_registered() ) {
24551 // Forward to parent plugin connect when parent not registered.
24552 $url = $plugin_fs->get_activation_url();
24553 } else {
24554 // Forward to account page.
24555 $url = $plugin_fs->_get_admin_page_url( 'account' );
24556 }
24557 }
24558 }
24559
24560 return $url;
24561 }
24562
24563 /**
24564 * Forward page to activation page.
24565 *
24566 * @author Vova Feldman (@svovaf)
24567 * @since 1.0.3
24568 */
24569 function _redirect_on_activation_hook() {
24570 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
24571 $url = $this->get_after_plugin_activation_redirect_url();
24572
24573 if ( is_string( $url ) ) {
24574 fs_redirect( $url );
24575 }
24576 }
24577 }
24578
24579 /**
24580 * Modify plugin's page action links collection.
24581 *
24582 * @author Vova Feldman (@svovaf)
24583 * @since 1.0.0
24584 *
24585 * @param array $links
24586 * @param $file
24587 *
24588 * @return array
24589 */
24590 function _modify_plugin_action_links_hook( $links, $file ) {
24591 $this->_logger->entrance();
24592
24593 $passed_deactivate = false;
24594 $deactivate_link = '';
24595 $before_deactivate = array();
24596 $after_deactivate = array();
24597 foreach ( $links as $key => $link ) {
24598 if ( 'deactivate' === $key ) {
24599 $deactivate_link = $link;
24600 $passed_deactivate = true;
24601 continue;
24602 }
24603
24604 if ( ! $passed_deactivate ) {
24605 $before_deactivate[ $key ] = $link;
24606 } else {
24607 $after_deactivate[ $key ] = $link;
24608 }
24609 }
24610
24611 ksort( $this->_action_links );
24612
24613 foreach ( $this->_action_links as $new_links ) {
24614 foreach ( $new_links as $link ) {
24615 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
24616 }
24617 }
24618
24619 if ( ! empty( $deactivate_link ) ) {
24620 /**
24621 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
24622 *
24623 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
24624 */
24625 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
24626
24627 // Append deactivation link.
24628 $before_deactivate['deactivate'] = $deactivate_link;
24629 }
24630
24631 return array_merge( $before_deactivate, $after_deactivate );
24632 }
24633
24634 /**
24635 * Adds admin message.
24636 *
24637 * @author Vova Feldman (@svovaf)
24638 * @since 1.0.4
24639 *
24640 * @param string $message
24641 * @param string $title
24642 * @param string $type
24643 */
24644 function add_admin_message( $message, $title = '', $type = 'success' ) {
24645 $this->_admin_notices->add( $message, $title, $type );
24646 }
24647
24648 /**
24649 * Adds sticky admin message.
24650 *
24651 * @author Vova Feldman (@svovaf)
24652 * @since 1.1.0
24653 *
24654 * @param string $message
24655 * @param string $id
24656 * @param string $title
24657 * @param string $type
24658 */
24659 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
24660 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
24661 }
24662
24663 /**
24664 * Check if the paid version of the module is installed.
24665 *
24666 * @author Vova Feldman (@svovaf)
24667 * @since 2.2.0
24668 *
24669 * @return bool
24670 */
24671 private function is_premium_version_installed() {
24672 $premium_plugin_basename = $this->premium_plugin_basename();
24673
24674 if ( $this->is_theme() ) {
24675 return $this->can_activate_theme( $this->get_premium_slug() );
24676 }
24677
24678 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_plugin_basename ) );
24679 }
24680
24681 /**
24682 * Helper function that returns the final steps for the upgrade completion.
24683 *
24684 * If the module is already running the premium code, returns an empty string.
24685 *
24686 * @author Vova Feldman (@svovaf)
24687 * @since 1.2.1
24688 *
24689 * @param string $plan_title
24690 *
24691 * @return string
24692 */
24693 private function get_complete_upgrade_instructions( $plan_title = '' ) {
24694 $this->_logger->entrance();
24695
24696 $activate_license_string = $this->get_license_network_activation_notice();
24697
24698 if ( ! $this->has_premium_version() || $this->is_premium() ) {
24699 return '' . $activate_license_string;
24700 }
24701
24702 if ( empty( $plan_title ) ) {
24703 $plan_title = $this->get_plan_title();
24704 }
24705
24706 if ( $this->is_premium_version_installed() ) {
24707 /**
24708 * If the premium version is already installed, instead of showing the installation instructions,
24709 * tell the current user to activate it.
24710 *
24711 * @author Leo Fajardo (@leorw)
24712 * @since 2.2.1
24713 */
24714 $premium_theme_slug_or_plugin_basename = $this->is_theme() ?
24715 $this->get_premium_slug() :
24716 $this->premium_plugin_basename();
24717
24718 if ( is_admin() ) {
24719 return sprintf(
24720 /* translators: %1$s: Product title; %2$s: Plan title */
24721 $this->get_text_inline( ' The paid version of %1$s is already installed. Please activate it to start benefiting from the %2$s features. %3$s', 'activate-premium-version' ),
24722 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24723 $plan_title,
24724 sprintf(
24725 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
24726 ( $this->is_theme() ?
24727 wp_nonce_url( 'themes.php?action=activate&amp;stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) :
24728 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ),
24729 esc_html( sprintf(
24730 /* translators: %s: Plan title */
24731 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
24732 $plan_title
24733 ) )
24734 )
24735 );
24736 } else {
24737 return sprintf(
24738 /* translators: %1$s: Product title; %3$s: Plan title */
24739 $this->get_text_inline( ' The paid version of %1$s is already installed. Please navigate to the %2$s to activate it and start benefiting from the %3$s features.', 'activate-premium-version-plugins-page' ),
24740 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24741 sprintf(
24742 '<a href="%s">%s</a>',
24743 admin_url( $this->is_theme() ? 'themes.php' : 'plugins.php' ),
24744 ( $this->is_theme() ?
24745 $this->get_text_inline( 'Themes page', 'themes-page' ) :
24746 $this->get_text_inline( 'Plugins page', 'plugins-page' ) )
24747 ),
24748 $plan_title
24749 );
24750 }
24751 } else {
24752 // @since 1.2.1.5 The free version is auto deactivated.
24753 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
24754 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
24755 '';
24756
24757 return sprintf(
24758 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
24759 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
24760 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
24761 $this->get_latest_download_link( sprintf(
24762 /* translators: %s: Plan title */
24763 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
24764 $plan_title
24765 ) ),
24766 $deactivation_step,
24767 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
24768 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/wp-' . $this->_module_type . '-upload' ),
24769 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
24770 );
24771 }
24772 }
24773
24774 /**
24775 * @author Leo Fajardo (@leorw)
24776 * @since 2.5.3
24777 *
24778 * @param string $message_before_the_instructions
24779 * @param string $message_id
24780 * @param string $plan_title
24781 */
24782 private function add_complete_upgrade_instructions_notice(
24783 $message_before_the_instructions,
24784 $message_id,
24785 $plan_title = ''
24786 ) {
24787 $this->_admin_notices->add_sticky(
24788 $message_before_the_instructions .
24789 $this->get_complete_upgrade_instructions( $plan_title ),
24790 $message_id,
24791 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
24792 );
24793 }
24794
24795 /**
24796 * @author Leo Fajardo (@leorw)
24797 * @since 2.5.3
24798 *
24799 * @param bool $is_upgrade
24800 */
24801 private function add_after_plan_activation_or_upgrade_instructions_notice( $is_upgrade = true ) {
24802 $this->add_complete_upgrade_instructions_notice(
24803 $is_upgrade ?
24804 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) :
24805 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ),
24806 'plan_upgraded'
24807 );
24808 }
24809
24810 /**
24811 * @author Leo Fajardo (@leorw)
24812 * @since 2.1.0
24813 *
24814 * @param string $url
24815 * @param array $request
24816 * @param int $success_cache_expiration
24817 * @param int $failure_cache_expiration
24818 * @param bool $maybe_enrich_request_for_debug
24819 *
24820 * @return WP_Error|array
24821 */
24822 static function safe_remote_post(
24823 &$url,
24824 $request,
24825 $success_cache_expiration = 0,
24826 $failure_cache_expiration = 0,
24827 $maybe_enrich_request_for_debug = true
24828 ) {
24829 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
24830
24831 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
24832
24833 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
24834 get_transient( $cache_key ) :
24835 false;
24836
24837 if ( false === $response ) {
24838 if ( $maybe_enrich_request_for_debug ) {
24839 FS_DebugManager::enrich_request_for_debug( $url, $request );
24840 }
24841
24842 if ( ! isset( $request['method'] ) ) {
24843 $request['method'] = 'POST';
24844 }
24845
24846 $response = FS_Api::remote_request( $url, $request );
24847
24848 if (
24849 'https://' === substr( $url, 0, 8 ) &&
24850 FS_Api::is_ssl_error_response( $response )
24851 ) {
24852 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
24853 $url = 'http://' . substr( $url, 8 );
24854
24855 $request['timeout'] = 15;
24856 $response = FS_Api::remote_request( $url, $request );
24857 }
24858
24859 if ( false !== $cache_key ) {
24860 set_transient(
24861 $cache_key,
24862 $response,
24863 ( ( $response instanceof WP_Error ) ?
24864 $failure_cache_expiration :
24865 $success_cache_expiration )
24866 );
24867 }
24868 }
24869
24870 return $response;
24871 }
24872
24873 /**
24874 * This method is used to enrich the after upgrade notice instructions when the upgraded
24875 * license cannot be activated network wide (license quota isn't large enough).
24876 *
24877 * @author Vova Feldman (@svovaf)
24878 * @since 2.0.0
24879 *
24880 * @return string
24881 */
24882 private function get_license_network_activation_notice() {
24883 if ( ! $this->_is_network_active ) {
24884 // Module isn't network level activated.
24885 return '';
24886 }
24887
24888 if ( ! fs_is_network_admin() ) {
24889 // Not network level admin.
24890 return '';
24891 }
24892
24893 if ( get_blog_count() == 1 ) {
24894 // There's only a single site in the network so if there's a context license it was already activated.
24895 return '';
24896 }
24897
24898 if ( ! is_object( $this->_license ) ) {
24899 // No context license.
24900 return '';
24901 }
24902
24903 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
24904 // 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).
24905 return '';
24906 }
24907
24908 if ( $this->can_activate_license_on_network( $this->_license ) ) {
24909 // 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).
24910 return '';
24911 }
24912
24913 return sprintf(
24914 $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' ),
24915 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
24916 '</a>'
24917 );
24918 }
24919
24920 /**
24921 * @author Vova Feldman (@svovaf)
24922 * @since 1.2.1.7
24923 *
24924 * @param string $key
24925 *
24926 * @return string
24927 */
24928 function get_text( $key ) {
24929 return fs_text( $key, $this->_slug );
24930 }
24931
24932 /**
24933 * @author Vova Feldman (@svovaf)
24934 * @since 1.2.3
24935 *
24936 * @param string $text Translatable string.
24937 * @param string $key String key for overrides.
24938 *
24939 * @return string
24940 */
24941 function get_text_inline( $text, $key = '' ) {
24942 return _fs_text_inline( $text, $key, $this->_slug );
24943 }
24944
24945 /**
24946 * @author Vova Feldman (@svovaf)
24947 * @since 1.2.3
24948 *
24949 * @param string $text Translatable string.
24950 * @param string $context Context information for the translators.
24951 * @param string $key String key for overrides.
24952 *
24953 * @return string
24954 */
24955 function get_text_x_inline( $text, $context, $key ) {
24956 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
24957 }
24958
24959 /**
24960 * @author Vova Feldman (@svovaf)
24961 * @since 1.2.3
24962 *
24963 * @param string $text Translatable string.
24964 * @param string $key String key for overrides.
24965 *
24966 * @return string
24967 */
24968 function esc_html_inline( $text, $key ) {
24969 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
24970 }
24971
24972 #----------------------------------------------------------------------------------
24973 #region Versioning
24974 #----------------------------------------------------------------------------------
24975
24976 /**
24977 * Check if Freemius in SDK upgrade mode.
24978 *
24979 * @author Vova Feldman (@svovaf)
24980 * @since 1.0.9
24981 *
24982 * @return bool
24983 */
24984 function is_sdk_upgrade_mode() {
24985 return isset( $this->_storage->sdk_upgrade_mode ) ?
24986 $this->_storage->sdk_upgrade_mode :
24987 false;
24988 }
24989
24990 /**
24991 * Turn SDK upgrade mode off.
24992 *
24993 * @author Vova Feldman (@svovaf)
24994 * @since 1.0.9
24995 */
24996 function set_sdk_upgrade_complete() {
24997 $this->_storage->sdk_upgrade_mode = false;
24998 }
24999
25000 /**
25001 * Check if plugin upgrade mode.
25002 *
25003 * @author Vova Feldman (@svovaf)
25004 * @since 1.0.9
25005 *
25006 * @return bool
25007 */
25008 function is_plugin_upgrade_mode() {
25009 return isset( $this->_storage->plugin_upgrade_mode ) ?
25010 $this->_storage->plugin_upgrade_mode :
25011 false;
25012 }
25013
25014 /**
25015 * Turn plugin upgrade mode off.
25016 *
25017 * @author Vova Feldman (@svovaf)
25018 * @since 1.0.9
25019 */
25020 function set_plugin_upgrade_complete() {
25021 $this->_storage->plugin_upgrade_mode = false;
25022
25023 $license_migration = ! empty( $this->_storage->license_migration ) ?
25024 $this->_storage->license_migration :
25025 array();
25026
25027 $license_migration['is_migrating'] = false;
25028
25029 $this->_storage->license_migration = $license_migration;
25030 }
25031
25032 #endregion
25033
25034 #----------------------------------------------------------------------------------
25035 #region Permissions
25036 #----------------------------------------------------------------------------------
25037
25038 /**
25039 * Check if specific permission requested.
25040 *
25041 * @author Vova Feldman (@svovaf)
25042 * @since 1.1.6
25043 *
25044 * @param string $permission
25045 *
25046 * @return bool
25047 */
25048 function is_permission_requested( $permission ) {
25049 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
25050 }
25051
25052 #endregion
25053
25054 #----------------------------------------------------------------------------------
25055 #region Auto Activation
25056 #----------------------------------------------------------------------------------
25057
25058 /**
25059 * Hints the SDK if running an auto-installation.
25060 *
25061 * @var bool
25062 */
25063 private $_isAutoInstall = false;
25064
25065 /**
25066 * After upgrade callback to install and auto activate a plugin.
25067 * This code will only be executed on explicit request from the user,
25068 * following the practice Jetpack are using with their theme installations.
25069 *
25070 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
25071 *
25072 * @author Vova Feldman (@svovaf)
25073 * @since 1.2.1.7
25074 */
25075 function _install_premium_version_ajax_action() {
25076 $this->_logger->entrance();
25077
25078 $this->check_ajax_referer( 'install_premium_version' );
25079
25080 if ( ! $this->is_registered() ) {
25081 // Not registered.
25082 self::shoot_ajax_failure( array(
25083 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
25084 'code' => 'premium_installed',
25085 ) );
25086 }
25087
25088 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
25089
25090 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25091 // Invalid ID.
25092 self::shoot_ajax_failure( array(
25093 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25094 'code' => 'invalid_module_id',
25095 ) );
25096 }
25097
25098 if ( $plugin_id == $this->get_id() ) {
25099 if ( $this->is_premium() ) {
25100 // Already using the premium code version.
25101 self::shoot_ajax_failure( array(
25102 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
25103 'code' => 'premium_installed',
25104 ) );
25105 }
25106 if ( ! $this->can_use_premium_code() ) {
25107 // Don't have access to the premium code.
25108 self::shoot_ajax_failure( array(
25109 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
25110 'code' => 'invalid_license',
25111 ) );
25112 }
25113 if ( ! $this->has_release_on_freemius() ) {
25114 // Plugin is a serviceware, no premium code version.
25115 self::shoot_ajax_failure( array(
25116 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
25117 'code' => 'premium_version_missing',
25118 ) );
25119 }
25120 } else {
25121 $addon = $this->get_addon( $plugin_id );
25122
25123 if ( ! is_object( $addon ) ) {
25124 // Invalid add-on ID.
25125 self::shoot_ajax_failure( array(
25126 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25127 'code' => 'invalid_module_id',
25128 ) );
25129 }
25130
25131 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25132 // Premium add-on version is already activated.
25133 self::shoot_ajax_failure( array(
25134 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
25135 'code' => 'premium_installed',
25136 ) );
25137 }
25138 }
25139
25140 $this->_isAutoInstall = true;
25141
25142 // Try to install and activate.
25143 $updater = FS_Plugin_Updater::instance( $this );
25144 $result = $updater->install_and_activate_plugin( $plugin_id );
25145
25146 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
25147 self::shoot_ajax_failure( array(
25148 'message' => $result['message'],
25149 'code' => $result['code'],
25150 ) );
25151 }
25152
25153 self::shoot_ajax_success( $result );
25154 }
25155
25156 /**
25157 * Displays module activation dialog box after a successful upgrade
25158 * where the user explicitly requested to auto download and install
25159 * the premium version.
25160 *
25161 * @author Vova Feldman (@svovaf)
25162 * @since 1.2.1.7
25163 */
25164 function _add_auto_installation_dialog_box() {
25165 $this->_logger->entrance();
25166
25167 if ( ! $this->is_registered() ) {
25168 // Not registered.
25169 return;
25170 }
25171
25172 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
25173
25174 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25175 // Invalid module ID.
25176 return;
25177 }
25178
25179 if ( $plugin_id == $this->get_id() ) {
25180 if ( $this->is_premium() ) {
25181 // Already using the premium code version.
25182 return;
25183 }
25184 if ( ! $this->can_use_premium_code() ) {
25185 // Don't have access to the premium code.
25186 return;
25187 }
25188 if ( ! $this->has_release_on_freemius() ) {
25189 // Plugin is a serviceware, no premium code version.
25190 return;
25191 }
25192 } else {
25193 $addon = $this->get_addon( $plugin_id );
25194
25195 if ( ! is_object( $addon ) ) {
25196 // Invalid add-on ID.
25197 return;
25198 }
25199
25200 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25201 // Premium add-on version is already activated.
25202 return;
25203 }
25204 }
25205
25206 $vars = array(
25207 'id' => $this->_module_id,
25208 'target_module_id' => $plugin_id,
25209 'slug' => $this->_slug,
25210 );
25211
25212 fs_require_template( 'auto-installation.php', $vars );
25213 }
25214
25215 #endregion
25216
25217 #--------------------------------------------------------------------------------
25218 #region Tabs Integration
25219 #--------------------------------------------------------------------------------
25220
25221 #region Module's Original Tabs
25222
25223 /**
25224 * Inject a JavaScript logic to capture the theme tabs HTML.
25225 *
25226 * @author Vova Feldman (@svovaf)
25227 * @since 1.2.2.7
25228 */
25229 function _tabs_capture() {
25230 $this->_logger->entrance();
25231
25232 if (
25233 ! $this->is_product_settings_page() ||
25234 ! $this->should_page_include_tabs() ||
25235 ! $this->is_matching_url( $this->main_menu_url() )
25236 ) {
25237 return;
25238 }
25239
25240 $params = array(
25241 'id' => $this->_module_id,
25242 );
25243
25244 fs_require_once_template( 'tabs-capture-js.php', $params );
25245 }
25246
25247 /**
25248 * Cache theme's tabs HTML for a week. The cache will also be set as expired
25249 * after version and type (free/premium) changes, in addition to the week period.
25250 *
25251 * @author Vova Feldman (@svovaf)
25252 * @since 1.2.2.7
25253 */
25254 function _store_tabs_ajax_action() {
25255 $this->_logger->entrance();
25256
25257 $this->check_ajax_referer( 'store_tabs' );
25258
25259 // Init filesystem if not yet initiated.
25260 WP_Filesystem();
25261
25262 // Get POST body HTML data.
25263 global $wp_filesystem;
25264 $tabs_html = $wp_filesystem->get_contents( "php://input" );
25265
25266 if ( is_string( $tabs_html ) ) {
25267 $tabs_html = trim( $tabs_html );
25268 }
25269
25270 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
25271 self::shoot_ajax_failure();
25272 }
25273
25274 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25275
25276 self::shoot_ajax_success();
25277 }
25278
25279 /**
25280 * Cache theme's settings page custom styles. The cache will also be set as expired
25281 * after version and type (free/premium) changes, in addition to the week period.
25282 *
25283 * @author Vova Feldman (@svovaf)
25284 * @since 1.2.2.7
25285 */
25286 function _store_tabs_styles() {
25287 $this->_logger->entrance();
25288
25289 if (
25290 ! $this->is_product_settings_page() ||
25291 ! $this->should_page_include_tabs() ||
25292 ! $this->is_matching_url( $this->main_menu_url() )
25293 ) {
25294 return;
25295 }
25296
25297 $wp_styles = wp_styles();
25298
25299 $theme_styles_url = get_template_directory_uri();
25300
25301 $stylesheets = array();
25302 foreach ( $wp_styles->queue as $handler ) {
25303 if ( fs_starts_with( $handler, 'fs_' ) ) {
25304 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
25305 continue;
25306 }
25307
25308 /**
25309 * @var _WP_Dependency $stylesheet
25310 */
25311 $stylesheet = $wp_styles->registered[ $handler ];
25312
25313 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
25314 $stylesheets[] = $stylesheet->src;
25315 }
25316 }
25317
25318 if ( ! empty( $stylesheets ) ) {
25319 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25320 }
25321 }
25322
25323 /**
25324 * Check if module's original settings page has any tabs.
25325 *
25326 * @author Vova Feldman (@svovaf)
25327 * @since 1.2.2.7
25328 *
25329 * @return bool
25330 */
25331 private function has_tabs() {
25332 return $this->_cache->has( 'tabs' );
25333 }
25334
25335 /**
25336 * Get module's settings page HTML content, starting
25337 * from the beginning of the <div class="wrap"> element,
25338 * until the tabs HTML (including).
25339 *
25340 * @author Vova Feldman (@svovaf)
25341 * @since 1.2.2.7
25342 *
25343 * @return string
25344 */
25345 private function get_tabs_html() {
25346 $this->_logger->entrance();
25347
25348 return $this->_cache->get( 'tabs' );
25349 }
25350
25351 /**
25352 * Check if page should include tabs.
25353 *
25354 * @author Vova Feldman (@svovaf)
25355 * @since 1.2.2.7
25356 *
25357 * @return bool
25358 */
25359 private function should_page_include_tabs() {
25360 if ( ! $this->has_settings_menu() ) {
25361 // Don't add tabs if no settings at all.
25362 return false;
25363 }
25364
25365 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
25366 // Only add tabs to themes for now.
25367 return false;
25368 }
25369
25370 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
25371 // Only add tabs to monetizing themes.
25372 return false;
25373 }
25374
25375 if ( ! $this->is_product_settings_page() ) {
25376 // Only add tabs if browsing one of the product's setting pages.
25377 return false;
25378 }
25379
25380 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
25381 // Don't include tabs in the activation page.
25382 return false;
25383 }
25384
25385 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
25386 // Don't add tabs on checkout page, we want to reduce distractions
25387 // as much as possible.
25388 return false;
25389 }
25390
25391 return true;
25392 }
25393
25394 /**
25395 * Add the tabs HTML before the setting's page content and
25396 * enqueue any required stylesheets.
25397 *
25398 * @author Vova Feldman (@svovaf)
25399 * @since 1.2.2.7
25400 *
25401 * @return bool If tabs were included.
25402 */
25403 function _add_tabs_before_content() {
25404 $this->_logger->entrance();
25405
25406 if ( ! $this->should_page_include_tabs() ) {
25407 return false;
25408 }
25409
25410 $tabs_html = $this->get_tabs_html();
25411
25412 if ( empty( $tabs_html ) ) {
25413 return false;
25414 }
25415
25416 /**
25417 * Enqueue the original stylesheets that are included in the
25418 * theme settings page. That way, if the theme settings has
25419 * some custom _styled_ content above the tabs UI, this
25420 * will make sure that the styling is preserved.
25421 */
25422 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
25423 if ( is_array( $stylesheets ) ) {
25424 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
25425 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
25426 }
25427 }
25428
25429 // Cut closing </div> tag.
25430 echo substr( trim( $tabs_html ), 0, - 6 );
25431
25432 return true;
25433 }
25434
25435 /**
25436 * Add the tabs closing HTML after the setting's page content.
25437 *
25438 * @author Vova Feldman (@svovaf)
25439 * @since 1.2.2.7
25440 *
25441 * @return bool If tabs closing HTML was included.
25442 */
25443 function _add_tabs_after_content() {
25444 $this->_logger->entrance();
25445
25446 if ( ! $this->should_page_include_tabs() ) {
25447 return false;
25448 }
25449
25450 echo '</div>';
25451
25452 return true;
25453 }
25454
25455 #endregion
25456
25457 /**
25458 * Add in-page JavaScript to inject the Freemius tabs into
25459 * the module's setting tabs section.
25460 *
25461 * @author Vova Feldman (@svovaf)
25462 * @since 1.2.2.7
25463 */
25464 function _add_freemius_tabs() {
25465 $this->_logger->entrance();
25466
25467 if ( ! $this->should_page_include_tabs() ) {
25468 return;
25469 }
25470
25471 $params = array( 'id' => $this->_module_id );
25472 fs_require_once_template( 'tabs.php', $params );
25473 }
25474
25475 #endregion
25476
25477 #--------------------------------------------------------------------------------
25478 #region Customizer Integration for Themes
25479 #--------------------------------------------------------------------------------
25480
25481 /**
25482 * @author Vova Feldman (@svovaf)
25483 * @since 1.2.2.7
25484 *
25485 * @param WP_Customize_Manager $customizer
25486 */
25487 function _customizer_register( $customizer ) {
25488 $this->_logger->entrance();
25489
25490 if ( $this->is_pricing_page_visible() ) {
25491 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
25492
25493 $customizer->add_section( 'freemius_upsell', array(
25494 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
25495 'priority' => 1,
25496 ) );
25497 $customizer->add_setting( 'freemius_upsell', array(
25498 'sanitize_callback' => 'esc_html',
25499 ) );
25500
25501 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
25502 'fs' => $this,
25503 'section' => 'freemius_upsell',
25504 'priority' => 100,
25505 ) ) );
25506 }
25507
25508 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
25509 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
25510
25511 // Main Documentation Link In Customizer Root.
25512 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
25513 'fs' => $this,
25514 'priority' => 1000,
25515 ) ) );
25516 }
25517 }
25518
25519 #endregion
25520
25521 /**
25522 * If the theme has a paid version, add some custom
25523 * styling to the theme's premium version (if exists)
25524 * to highlight that it's the premium version of the
25525 * same theme, making it easier for identification
25526 * after the user upgrades and upload it to the site.
25527 *
25528 * @author Vova Feldman (@svovaf)
25529 * @since 1.2.2.7
25530 */
25531 function _style_premium_theme() {
25532 $this->_logger->entrance();
25533
25534 if ( ! self::is_themes_page() ) {
25535 // Only include in the themes page.
25536 return;
25537 }
25538
25539 if ( ! $this->has_paid_plan() ) {
25540 // Only include if has any paid plans.
25541 return;
25542 }
25543
25544 $params = null;
25545 fs_require_once_template( '/js/jquery.content-change.php', $params );
25546
25547 $params = array(
25548 'slug' => $this->_slug,
25549 'id' => $this->_module_id,
25550 );
25551
25552 fs_require_template( '/js/style-premium-theme.php', $params );
25553 }
25554
25555 /**
25556 * This method will return the absolute URL of the module's local icon.
25557 *
25558 * When you are running your plugin or theme on a **localhost** environment, if the icon
25559 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
25560 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
25561 * If an icon is found, this method will automatically attempt to download the icon and store it
25562 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
25563 *
25564 * It's important to mention that this method is NOT phoning home since the developer will deploy
25565 * the product with the local icon in the assets folder. The download process just simplifies
25566 * the process for the developer.
25567 *
25568 * @author Vova Feldman (@svovaf)
25569 * @since 2.0.0
25570 *
25571 * @return string
25572 */
25573 function get_local_icon_url() {
25574 global $fs_active_plugins;
25575
25576 /**
25577 * @since 1.1.7.5
25578 */
25579 $local_path = $this->apply_filters( 'plugin_icon', false );
25580
25581 if ( is_string( $local_path ) ) {
25582 $icons = array( $local_path );
25583 } else {
25584 $img_dir = WP_FS__DIR_IMG;
25585
25586 // Locate the main assets folder.
25587 if ( ! empty( $fs_active_plugins->plugins ) ) {
25588 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
25589
25590 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
25591 if ( $data->plugin_path == $this->get_plugin_basename() ) {
25592 $img_dir = $plugin_or_theme_img_dir
25593 . '/'
25594 /**
25595 * The basename will be `themes` or the basename of a custom themes directory.
25596 *
25597 * @author Leo Fajardo (@leorw)
25598 * @since 2.2.3
25599 */
25600 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
25601 . '/assets/img';
25602
25603 break;
25604 }
25605 }
25606 }
25607
25608 // Try to locate the icon in the assets folder.
25609 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
25610
25611 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
25612 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
25613 $icons = array(
25614 fs_normalize_path( $img_dir . '/theme-icon.png' )
25615 );
25616 } else {
25617 $icon_found = false;
25618 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
25619
25620 if ( ! function_exists( 'get_filesystem_method' ) ) {
25621 require_once ABSPATH . 'wp-admin/includes/file.php';
25622 }
25623
25624 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
25625
25626 /**
25627 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
25628 *
25629 * This code will only be executed once during the testing
25630 * of the plugin in a local environment. The plugin icon file WILL
25631 * already exist in the assets folder when the plugin is deployed to
25632 * the repository.
25633 */
25634 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
25635 // Fetch icon from Freemius.
25636 $icon = $this->fetch_remote_icon_url();
25637
25638 // Fetch icon from WordPress.org.
25639 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
25640 if ( ! function_exists( 'plugins_api' ) ) {
25641 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
25642 }
25643
25644 $plugin_information = plugins_api( 'plugin_information', array(
25645 'slug' => $this->_slug,
25646 'fields' => array(
25647 'sections' => false,
25648 'tags' => false,
25649 'icons' => true
25650 )
25651 ) );
25652
25653 if (
25654 ! is_wp_error( $plugin_information )
25655 && isset( $plugin_information->icons )
25656 && ! empty( $plugin_information->icons )
25657 ) {
25658 /**
25659 * Get the smallest icon.
25660 *
25661 * @author Leo Fajardo (@leorw)
25662 * @since 1.2.2
25663 */
25664 $icon = end( $plugin_information->icons );
25665 }
25666 }
25667
25668 if ( ! empty( $icon ) ) {
25669 if ( 0 !== strpos( $icon, 'http' ) ) {
25670 $icon = 'http:' . $icon;
25671 }
25672
25673 /**
25674 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
25675 *
25676 * @author Leo Fajardo (@leorw)
25677 * @since 1.2.2
25678 */
25679 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
25680
25681 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
25682
25683 // Try to download the icon.
25684 $icon_found = fs_download_image( $icon, $local_path );
25685 }
25686 }
25687
25688 if ( ! $icon_found ) {
25689 // No icons found, fallback to default icon.
25690 if ( $have_write_permissions ) {
25691 // If have write permissions, copy default icon.
25692 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
25693 } else {
25694 // If doesn't have write permissions, use default icon path.
25695 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
25696 }
25697 }
25698
25699 $icons = array( $local_path );
25700 }
25701 }
25702 }
25703
25704 $icon_dir = dirname( $icons[0] );
25705
25706 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
25707 }
25708
25709 /**
25710 * Fetch module's extended info.
25711 *
25712 * @author Vova Feldman (@svovaf)
25713 * @since 2.0.0
25714 *
25715 * @return object|mixed
25716 */
25717 private function fetch_module_info() {
25718 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
25719 }
25720
25721 /**
25722 * Fetch module's remote icon URL.
25723 *
25724 * @author Vova Feldman (@svovaf)
25725 * @since 2.0.0
25726 *
25727 * @return string
25728 */
25729 function fetch_remote_icon_url() {
25730 $info = $this->fetch_module_info();
25731
25732 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
25733 $info->icon :
25734 '';
25735 }
25736
25737 #--------------------------------------------------------------------------------
25738 #region GDPR
25739 #--------------------------------------------------------------------------------
25740
25741 /**
25742 * @author Leo Fajardo (@leorw)
25743 * @since 2.1.0
25744 *
25745 * @param array $user_plugins
25746 *
25747 * @return string
25748 */
25749 private function get_gdpr_admin_notice_string( $user_plugins ) {
25750 $this->_logger->entrance();
25751
25752 $addons = self::get_all_addons();
25753
25754 foreach ( $user_plugins as $user_plugin ) {
25755 $has_addons = isset( $addons[ $user_plugin->id ] );
25756
25757 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
25758 if ( $this->_module_id == $user_plugin->id ) {
25759 $addons = $this->get_addons();
25760 $has_addons = ( ! empty( $addons ) );
25761 } else {
25762 $plugin_api = FS_Api::instance(
25763 $user_plugin->id,
25764 'plugin',
25765 $user_plugin->id,
25766 $user_plugin->public_key,
25767 ! $user_plugin->is_live,
25768 false,
25769 $this->get_sdk_version()
25770 );
25771
25772 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
25773
25774 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
25775 is_array( $addons_result->plugins ) &&
25776 ! empty( $addons_result->plugins )
25777 ) {
25778 $has_addons = true;
25779 }
25780 }
25781 }
25782
25783 $user_plugin->has_addons = $has_addons;
25784 }
25785
25786 $is_single_parent_product = ( 1 === count( $user_plugins ) );
25787
25788 $multiple_products_text = '';
25789
25790 if ( $is_single_parent_product ) {
25791 $single_parent_product = reset( $user_plugins );
25792
25793 $thank_you = sprintf(
25794 "<span data-plugin-id='%d'>%s</span>",
25795 $single_parent_product->id,
25796 sprintf(
25797 $single_parent_product->has_addons ?
25798 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
25799 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
25800 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
25801 )
25802 );
25803
25804 $already_opted_in = sprintf(
25805 $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' ),
25806 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
25807 );
25808 } else {
25809 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
25810 $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' );
25811
25812 $products_and_add_ons = '';
25813 foreach ( $user_plugins as $user_plugin ) {
25814 if ( ! empty( $products_and_add_ons ) ) {
25815 $products_and_add_ons .= ', ';
25816 }
25817
25818 if ( ! $user_plugin->has_addons ) {
25819 $products_and_add_ons .= sprintf(
25820 "<span data-plugin-id='%d'>%s</span>",
25821 $user_plugin->id,
25822 $user_plugin->title
25823 );
25824 } else {
25825 $products_and_add_ons .= sprintf(
25826 "<span data-plugin-id='%d'>%s</span>",
25827 $user_plugin->id,
25828 sprintf(
25829 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
25830 $user_plugin->title
25831 )
25832 );
25833 }
25834 }
25835
25836 $multiple_products_text = sprintf(
25837 "<small class='products'><strong>%s:</strong> %s</small>",
25838 $this->get_text_inline( 'Products', 'products' ),
25839 $products_and_add_ons
25840 );
25841 }
25842
25843 $actions = sprintf(
25844 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
25845 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
25846 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
25847 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
25848 sprintf(
25849 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
25850 '<span class="underlined">',
25851 '</span>'
25852 )
25853 );
25854
25855 return sprintf(
25856 '%s %s %s',
25857 $thank_you,
25858 $already_opted_in,
25859 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>' ) .
25860 '<br><br>' .
25861 '<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>' .
25862 $actions .
25863 ( $is_single_parent_product ? '' : $multiple_products_text )
25864 );
25865 }
25866
25867 /**
25868 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
25869 * plugins and themes they've opted in to.
25870 *
25871 * @author Leo Fajardo (@leorw)
25872 * @since 2.1.0
25873 *
25874 * @param string $user_email
25875 * @param string $license_key
25876 * @param array $plugin_ids
25877 * @param string|null $license_key
25878 *
25879 * @return array|false
25880 */
25881 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
25882 $request = array(
25883 'method' => 'POST',
25884 'body' => array(),
25885 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
25886 );
25887
25888 if ( is_string( $user_email ) ) {
25889 $request['body']['email'] = $user_email;
25890 } else {
25891 $request['body']['license_key'] = $license_key;
25892 }
25893
25894 $result = array();
25895
25896 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
25897 $total_plugin_ids = count( $plugin_ids );
25898
25899 $plugin_ids_count_per_request = 10;
25900 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
25901 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
25902
25903 $request['body']['plugin_ids'] = $plugin_ids_set;
25904
25905 $response = self::safe_remote_post(
25906 $url,
25907 $request,
25908 WP_FS__TIME_24_HOURS_IN_SEC,
25909 WP_FS__TIME_12_HOURS_IN_SEC
25910 );
25911
25912 if ( ! is_wp_error( $response ) ) {
25913 $decoded = is_string( $response['body'] ) ?
25914 json_decode( $response['body'] ) :
25915 null;
25916
25917 if (
25918 !is_object($decoded) ||
25919 !isset($decoded->success) ||
25920 true !== $decoded->success ||
25921 !isset( $decoded->data ) ||
25922 !is_array( $decoded->data )
25923 ) {
25924 return false;
25925 }
25926
25927 $result = array_merge( $result, $decoded->data );
25928 }
25929 }
25930
25931 return $result;
25932 }
25933
25934 /**
25935 * @author Leo Fajardo (@leorw)
25936 * @since 2.1.0
25937 */
25938 function _maybe_show_gdpr_admin_notice() {
25939 if ( ! $this->is_user_in_admin() ) {
25940 return;
25941 }
25942
25943 if ( ! $this->should_handle_gdpr_admin_notice() ) {
25944 return;
25945 }
25946
25947 if ( ! $this->is_user_admin() ) {
25948 return;
25949 }
25950
25951 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25952
25953 $lock = FS_User_Lock::instance();
25954
25955 /**
25956 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
25957 */
25958 if ( ! $lock->try_lock( 60 ) ) {
25959 return;
25960 }
25961
25962 /**
25963 * @var $current_wp_user WP_User
25964 */
25965 $current_wp_user = self::_get_current_wp_user();
25966
25967 /**
25968 * @var FS_User $current_fs_user
25969 */
25970 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
25971
25972 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
25973
25974 if ( ! is_object( $current_fs_user ) ) {
25975 // 10-year lock.
25976 $lock->lock( $ten_years_in_sec );
25977
25978 return;
25979 }
25980
25981 $gdpr = FS_GDPR_Manager::instance();
25982
25983 if ( $gdpr->is_opt_in_notice_shown() ) {
25984 // 30-day lock.
25985 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
25986
25987 return;
25988 }
25989
25990 if ( ! $gdpr->should_show_opt_in_notice() ) {
25991 // 10-year lock.
25992 $lock->lock( $ten_years_in_sec );
25993
25994 return;
25995 }
25996
25997 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
25998 $was_notice_shown_before = ( false !== $last_time_notice_shown );
25999
26000 if ( $was_notice_shown_before &&
26001 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
26002 ) {
26003 // If the notice was shown before, show it again after 30 days from the last time it was shown.
26004 return;
26005 }
26006
26007 /**
26008 * Find all plugin IDs that were installed by the current admin.
26009 */
26010 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
26011
26012 if ( empty( $plugin_ids_map )) {
26013 $lock->lock( $ten_years_in_sec );
26014
26015 return;
26016 }
26017
26018 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26019 $current_fs_user->email,
26020 null,
26021 array_keys( $plugin_ids_map )
26022 );
26023
26024 if ( empty( $user_plugins ) ) {
26025 $lock->lock(
26026 is_array($user_plugins) ?
26027 $ten_years_in_sec :
26028 // Lock for 24-hours on errors.
26029 WP_FS__TIME_24_HOURS_IN_SEC
26030 );
26031
26032 return;
26033 }
26034
26035 $has_unset_marketing_optin = false;
26036
26037 foreach ( $user_plugins as $user_plugin ) {
26038 if ( true == $user_plugin->is_marketing_allowed ) {
26039 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
26040 }
26041
26042 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
26043 $has_unset_marketing_optin = true;
26044 }
26045 }
26046
26047 if ( empty( $plugin_ids_map ) ||
26048 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
26049 ) {
26050 $lock->lock( $ten_years_in_sec );
26051
26052 return;
26053 }
26054
26055 $modules = array_merge(
26056 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26057 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26058 );
26059
26060 foreach ( $modules as $module ) {
26061 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
26062 $plugin_ids_map[ $module->id ] = $module;
26063 }
26064 }
26065
26066 $plugin_title = null;
26067 if ( 1 === count( $plugin_ids_map ) ) {
26068 $module = reset( $plugin_ids_map );
26069 $plugin_title = $module->title;
26070 }
26071
26072 $gdpr->add_opt_in_sticky_notice(
26073 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
26074 $plugin_title
26075 );
26076
26077 $this->add_gdpr_optin_ajax_handler_and_style();
26078
26079 $gdpr->notice_was_just_shown();
26080
26081 // 30-day lock.
26082 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26083 }
26084
26085 /**
26086 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
26087 * marketing.
26088 *
26089 * @author Leo Fajardo (@leorw)
26090 * @since 2.1.0
26091 */
26092 private function disable_opt_in_notice_and_lock_user() {
26093 FS_GDPR_Manager::instance()->disable_opt_in_notice();
26094
26095 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26096
26097 // 10-year lock.
26098 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26099 }
26100
26101 /**
26102 * @author Leo Fajardo (@leorw)
26103 * @since 2.5.4
26104 */
26105 static function _add_api_connectivity_notice_handler_js() {
26106 fs_require_once_template( 'api-connectivity-message-js.php' );
26107 }
26108
26109 /**
26110 * @author Leo Fajardo (@leorw)
26111 * @since 2.1.0
26112 */
26113 function _add_gdpr_optin_js() {
26114 $vars = array( 'id' => $this->_module_id );
26115
26116 fs_require_once_template( 'gdpr-optin-js.php', $vars );
26117 }
26118
26119 /**
26120 * @author Leo Fajardo (@leorw)
26121 * @since 2.1.0
26122 */
26123 function enqueue_gdpr_optin_notice_style() {
26124 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
26125 }
26126
26127 /**
26128 * @author Leo Fajardo (@leorw)
26129 * @since 2.1.0
26130 */
26131 function _maybe_add_gdpr_optin_ajax_handler() {
26132 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
26133
26134 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
26135 $this->add_gdpr_optin_ajax_handler_and_style();
26136 }
26137 }
26138
26139 /**
26140 * @author Leo Fajardo (@leorw)
26141 * @since 2.1.0
26142 */
26143 function _fetch_is_marketing_required_flag_value_ajax_action() {
26144 $this->_logger->entrance();
26145
26146 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
26147
26148 $license_key = fs_request_get_raw( 'license_key' );
26149
26150 if ( empty($license_key) ) {
26151 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
26152 }
26153
26154 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26155 null,
26156 $license_key,
26157 array( $this->_module_id )
26158 );
26159
26160 if ( ! is_array( $user_plugins ) ||
26161 empty($user_plugins) ||
26162 !isset($user_plugins[0]->plugin_id) ||
26163 $user_plugins[0]->plugin_id != $this->_module_id
26164 ) {
26165 /**
26166 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
26167 *
26168 * @author Vova Feldman (@svovaf)
26169 */
26170 self::shoot_ajax_success( array(
26171 'is_marketing_allowed' => null,
26172 'license_owner_id' => null
26173 ) );
26174 }
26175
26176 self::shoot_ajax_success( array(
26177 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
26178 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
26179 ) );
26180 }
26181
26182 /**
26183 * @author Leo Fajardo (@leorw)
26184 * @since 2.3.2
26185 *
26186 * @param number[] $install_ids
26187 *
26188 * @return array {
26189 * An array of objects containing the installs' licenses owners data.
26190 *
26191 * @property number $id User ID.
26192 * @property string $email User email (can be masked email).
26193 * }
26194 */
26195 private function fetch_installs_licenses_owners_data( $install_ids ) {
26196 $this->_logger->entrance();
26197
26198 $response = $this->get_api_user_scope()->get(
26199 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
26200 );
26201
26202 $license_owners = array();
26203
26204 if ( $this->is_api_result_object( $response, 'owners' ) ) {
26205 $license_owners = $response->owners;
26206 }
26207
26208 return $license_owners;
26209 }
26210
26211 /**
26212 * @author Leo Fajardo (@leorw)
26213 * @since 2.1.0
26214 */
26215 private function add_gdpr_optin_ajax_handler_and_style() {
26216 // Add GDPR action AJAX callback.
26217 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
26218
26219 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
26220 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
26221 }
26222
26223 /**
26224 * @author Leo Fajardo (@leorw)
26225 * @since 2.1.0
26226 */
26227 function _gdpr_optin_ajax_action() {
26228 $this->_logger->entrance();
26229
26230 $this->check_ajax_referer( 'gdpr_optin_action' );
26231
26232 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
26233 self::shoot_ajax_failure();
26234 }
26235
26236 $current_wp_user = self::_get_current_wp_user();
26237
26238 $plugin_ids = fs_request_get( 'plugin_ids', array() );
26239 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
26240 self::shoot_ajax_failure();
26241 }
26242
26243 $modules = array_merge(
26244 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26245 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26246 );
26247
26248 foreach ( $modules as $key => $module ) {
26249 if ( ! in_array( $module->id, $plugin_ids ) ) {
26250 unset( $modules[ $key ] );
26251 }
26252 }
26253
26254 if ( empty( $modules ) ) {
26255 self::shoot_ajax_failure();
26256 }
26257
26258 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
26259
26260 foreach ( $modules as $module ) {
26261 $user_api->call( "?plugin_id={$module->id}", 'put', array(
26262 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
26263 ) );
26264 }
26265
26266 FS_GDPR_Manager::instance()->remove_opt_in_notice();
26267
26268 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26269
26270 // 10-year lock.
26271 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26272
26273 self::shoot_ajax_success();
26274 }
26275
26276 /**
26277 * 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.
26278 *
26279 * @author Vova Feldman (@svovaf)
26280 * @since 2.1.0
26281 *
26282 * @return bool
26283 */
26284 private function should_handle_gdpr_admin_notice() {
26285 return $this->apply_filters(
26286 'handle_gdpr_admin_notice',
26287 // Default to false.
26288 false
26289 );
26290 }
26291
26292 #endregion
26293
26294 #----------------------------------------------------------------------------------
26295 #region Marketing
26296 #----------------------------------------------------------------------------------
26297
26298 /**
26299 * Check if current user purchased any other plugins before.
26300 *
26301 * @author Vova Feldman (@svovaf)
26302 * @since 1.0.9
26303 *
26304 * @return bool
26305 */
26306 function has_purchased_before() {
26307 // TODO: Implement has_purchased_before() method.
26308 throw new Exception( 'not implemented' );
26309 }
26310
26311 /**
26312 * Check if current user classified as an agency.
26313 *
26314 * @author Vova Feldman (@svovaf)
26315 * @since 1.0.9
26316 *
26317 * @return bool
26318 */
26319 function is_agency() {
26320 // TODO: Implement is_agency() method.
26321 throw new Exception( 'not implemented' );
26322 }
26323
26324 /**
26325 * Check if current user classified as a developer.
26326 *
26327 * @author Vova Feldman (@svovaf)
26328 * @since 1.0.9
26329 *
26330 * @return bool
26331 */
26332 function is_developer() {
26333 // TODO: Implement is_developer() method.
26334 throw new Exception( 'not implemented' );
26335 }
26336
26337 /**
26338 * Check if current user classified as a business.
26339 *
26340 * @author Vova Feldman (@svovaf)
26341 * @since 1.0.9
26342 *
26343 * @return bool
26344 */
26345 function is_business() {
26346 // TODO: Implement is_business() method.
26347 throw new Exception( 'not implemented' );
26348 }
26349
26350 #endregion
26351
26352 #----------------------------------------------------------------------------------
26353 #region Helper
26354 #----------------------------------------------------------------------------------
26355
26356 /**
26357 * If running with a secret key, assume it's the developer and show pending plans as well.
26358 *
26359 * @author Vova Feldman (@svovaf)
26360 * @since 2.1.2
26361 *
26362 * @param string $path
26363 *
26364 * @return string
26365 */
26366 function add_show_pending( $path ) {
26367 if ( ! $this->has_secret_key() ) {
26368 return $path;
26369 }
26370
26371 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
26372 }
26373
26374 #endregion
26375 }
26376