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

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

26,369 lines 992.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Freemius
4 * @copyright Copyright (c) 2015, Freemius, Inc.
5 * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
6 * @since 1.0.3
7 */
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 // "final class"
13 class Freemius extends Freemius_Abstract {
14 /**
15 * SDK Version
16 *
17 * @var string
18 */
19 public $version = WP_FS__SDK_VERSION;
20
21 #region Plugin Info
22
23 /**
24 * @since 1.0.1
25 *
26 * @var string
27 */
28 private $_slug;
29
30 /**
31 * @since 1.0.0
32 *
33 * @var string
34 */
35 private $_plugin_basename;
36 /**
37 * @since 2.2.1
38 *
39 * @var string
40 */
41 private $_premium_plugin_basename;
42 /**
43 * @since 1.0.0
44 *
45 * @var string
46 */
47 private $_free_plugin_basename;
48 /**
49 * @since 1.0.0
50 *
51 * @var string
52 */
53 private $_plugin_dir_path;
54 /**
55 * @since 1.0.0
56 *
57 * @var string
58 */
59 private $_plugin_dir_name;
60 /**
61 * @since 1.0.0
62 *
63 * @var string
64 */
65 private $_plugin_main_file_path;
66 /**
67 * @var string[]
68 */
69 private $_plugin_data;
70 /**
71 * @since 1.0.9
72 *
73 * @var string
74 */
75 private $_plugin_name;
76 /**
77 * @since 1.2.2
78 *
79 * @var string
80 */
81 private $_module_type;
82
83 #endregion Plugin Info
84
85 /**
86 * @since 1.0.9
87 *
88 * @var bool If false, don't turn Freemius on.
89 */
90 private $_is_on;
91
92 /**
93 * @since 1.1.3
94 *
95 * @var bool If false, don't turn Freemius on.
96 */
97 private $_is_anonymous;
98
99 /**
100 * @since 1.0.9
101 * @var bool If false, issues with connectivity to Freemius API.
102 */
103 private $_has_api_connection;
104
105 /**
106 * @since 1.0.9
107 * @since 2.0.0 Default to true since we need the property during the instance construction, prior to the dynamic_init() execution.
108 * @var bool Hints the SDK if plugin can support anonymous mode (if skip connect is visible).
109 */
110 private $_enable_anonymous = true;
111
112 /**
113 * @since 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 sdk@freemius.com 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' => $_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_registered() && fs_request_has( 'purchase_completed' ) ) {
4544 $this->_admin_notices->add_sticky(
4545 sprintf(
4546 /* translators: %s: License type (e.g. you have a professional license) */
4547 $this->get_text_inline( 'You have purchased a %s license.', 'you-have-x-license' ),
4548 fs_request_get( 'purchased_plan' )
4549 ) .
4550 sprintf(
4551 $this->get_text_inline(" The %s's %sdownload link%s, license key, and installation instructions have been sent to %s. If you can't find the email after 5 min, please check your spam box.", 'post-purchase-email-sent-message' ),
4552 $this->get_module_label( true ),
4553 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? "products' " : '' ),
4554 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? 's' : '' ),
4555 sprintf(
4556 '<strong>%s</strong>',
4557 fs_request_get( 'purchase_email' )
4558 )
4559 ),
4560 'plan_purchased',
4561 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
4562 );
4563 }
4564
4565 if ( $this->is_addon() ) {
4566 if ( ! $this->is_parent_plugin_installed() ) {
4567 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4568
4569 if ( isset( $plugin_info['parent'] ) ) {
4570 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4571 }
4572
4573 $this->_admin_notices->add(
4574 ( ! empty( $parent_name ) ?
4575 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 ) :
4576 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() )
4577 ),
4578 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4579 'error'
4580 );
4581
4582 return;
4583 } else {
4584 $is_network_admin = fs_is_network_admin();
4585
4586 if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4587 // If add-on activated and parent not, automatically install parent for the user.
4588 $this->activate_parent_account( $this->_parent );
4589 } else if (
4590 $this->_parent->is_registered() &&
4591 ! $this->is_registered() &&
4592 /**
4593 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
4594 * * Network active and in network admin - network activate add-on account.
4595 * * Network active and not in network admin - activate add-on account for the current blog.
4596 * * Not network active and not in network admin - activate add-on account for the current blog.
4597 *
4598 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
4599 *
4600 * @author Leo Fajardo (@leorw)
4601 * @since 2.3.0
4602 */
4603 ( $this->is_network_active() || ! $is_network_admin )
4604 ) {
4605 $premium_license = null;
4606
4607 if (
4608 ! $this->has_free_plan() &&
4609 $this->is_bundle_license_auto_activation_enabled() &&
4610 $this->_parent->is_activated_with_bundle_license()
4611 ) {
4612 /**
4613 * If the add-on has no free plan, try to activate the account only when there's a bundle license.
4614 *
4615 * @author Leo Fajardo (@leorw)
4616 * @since 2.4.0
4617 */
4618 $bundle_license = $this->get_active_parent_license( $this->_parent->_get_license()->secret_key, false );
4619
4620 if (
4621 is_object( $bundle_license ) &&
4622 ! empty( $bundle_license->products ) &&
4623 in_array( $this->get_id(), $bundle_license->products )
4624 ) {
4625 $premium_license = $bundle_license;
4626 }
4627 }
4628
4629 if ( $this->has_free_plan() || is_object( $premium_license) ) {
4630 // If parent plugin activated, automatically install add-on for the user.
4631 $this->_activate_addon_account(
4632 $this->_parent,
4633 ( $this->is_network_active() && $is_network_admin ) ?
4634 true :
4635 get_current_blog_id(),
4636 $premium_license
4637 );
4638 }
4639 }
4640
4641 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4642 if ( $this->is_premium() ) {
4643 // Remove add-on download admin-notice.
4644 $this->_parent->_admin_notices->remove_sticky( array(
4645 'addon_plan_upgraded_' . $this->_slug,
4646 'no_addon_license_' . $this->_slug,
4647 ) );
4648 }
4649
4650 // $this->deactivate_premium_only_addon_without_license();
4651 }
4652 }
4653
4654 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
4655
4656 // if ( $this->is_registered() ||
4657 // $this->is_anonymous() ||
4658 // $this->is_pending_activation()
4659 // ) {
4660 // $this->_init_admin();
4661 // }
4662 }
4663
4664 /**
4665 * Should be called outside `$this->is_user_in_admin()` scope
4666 * because the updater has some logic that needs to be executed
4667 * during AJAX calls.
4668 *
4669 * Currently, we need to hook to the `http_request_host_is_external` filter.
4670 * In the future, there might be additional logic added.
4671 *
4672 * @author Vova Feldman
4673 * @since 1.2.1.6
4674 */
4675 if (
4676 $this->should_use_freemius_updater_and_dialog() &&
4677 (
4678 $this->is_premium() ||
4679 /**
4680 * If not premium but the premium version is installed, also instantiate the updater so that the
4681 * plugin information dialog of the premium version will have the information from the server.
4682 *
4683 * @author Leo Fajardo (@leorw)
4684 * @since 2.2.3
4685 */
4686 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
4687 ) &&
4688 $this->has_release_on_freemius() &&
4689 ( ! $this->is_unresolved_clone( true ) )
4690 ) {
4691 FS_Plugin_Updater::instance( $this );
4692 }
4693
4694 $this->do_action( 'initiated' );
4695
4696 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
4697 if ( isset( $this->_storage->prev_is_premium ) ) {
4698 $this->apply_filters(
4699 'after_code_type_change',
4700 // New code type.
4701 $this->_plugin->is_premium
4702 );
4703 } else {
4704 // Set for code type for the first time.
4705 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
4706 }
4707 }
4708
4709 if ( ! $this->is_addon() ) {
4710 if ( $this->is_registered() ) {
4711 // Fix for upgrade from versions < 1.0.9.
4712 if ( ! isset( $this->_storage->activation_timestamp ) ) {
4713 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
4714 }
4715
4716 $this->do_action( 'after_init_plugin_registered' );
4717 } else if ( $this->is_anonymous() ) {
4718 $this->do_action( 'after_init_plugin_anonymous' );
4719 } else if ( $this->is_pending_activation() ) {
4720 $this->do_action( 'after_init_plugin_pending_activations' );
4721 }
4722 } else {
4723 if ( $this->is_registered() ) {
4724 $this->do_action( 'after_init_addon_registered' );
4725 } else if ( $this->is_anonymous() ) {
4726 $this->do_action( 'after_init_addon_anonymous' );
4727 } else if ( $this->is_pending_activation() ) {
4728 $this->do_action( 'after_init_addon_pending_activations' );
4729 }
4730 }
4731 }
4732
4733 /**
4734 * @author Leo Fajardo (@leorw)
4735 * @since 2.2.3
4736 *
4737 * @return bool
4738 */
4739 private function should_use_freemius_updater_and_dialog() {
4740 return (
4741 /**
4742 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
4743 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
4744 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
4745 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
4746 * plugin details from .org).
4747 */
4748 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
4749 (
4750 ! self::is_plugin_install_page() &&
4751 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
4752 ( 'install-plugin' !== fs_request_get( 'action' ) )
4753 )
4754 );
4755 }
4756
4757 /**
4758 * @param string[] $permissions
4759 * @param bool $is_enabled
4760 * @param int|null $blog_id
4761 *
4762 * @return true|object `true` on success, API error object on failure.
4763 */
4764 private function update_site_permissions( array $permissions, $is_enabled, $blog_id = null ) {
4765 $this->_logger->entrance();
4766
4767 $params = array(
4768 'permissions' => implode( ',', $permissions ),
4769 'is_enabled' => $is_enabled,
4770 );
4771
4772 $current_blog_id = get_current_blog_id();
4773 $is_blog_switched = false;
4774 if ( is_numeric( $blog_id ) && $current_blog_id != $blog_id ) {
4775 $is_blog_switched = $this->switch_to_blog( $blog_id );
4776 }
4777
4778 $result = $this->api_site_call( '/permissions.json', 'put', $params );
4779
4780 if ( $is_blog_switched ) {
4781 $this->switch_to_blog( $current_blog_id );
4782 }
4783
4784 if (
4785 ! $this->is_api_result_object( $result ) ||
4786 ! isset( $result->install_id )
4787 ) {
4788 $this->_logger->api_error( $result );
4789
4790 return $result;
4791 }
4792
4793 return true;
4794 }
4795
4796 /**
4797 * @param string[] $permissions
4798 * @param bool $is_enabled
4799 * @param bool $has_site_delegated_connection
4800 *
4801 * @return true|object `true` on success, API error object on failure.
4802 */
4803 private function update_network_permissions(
4804 array $permissions,
4805 $is_enabled,
4806 &$has_site_delegated_connection
4807 ) {
4808 $this->_logger->entrance();
4809
4810 $install_id_2_blog_id = array();
4811 $install_by_blog_id = $this->get_blog_install_map();
4812
4813 $has_site_delegated_connection = false;
4814
4815 foreach ( $install_by_blog_id as $blog_id => $install ) {
4816 if ( $this->is_site_delegated_connection( $blog_id ) ) {
4817 // Only update permissions of non-delegated installs.
4818 $has_site_delegated_connection = true;
4819 continue;
4820 }
4821
4822 $install_id_2_blog_id[ $install->id ] = $blog_id;
4823 }
4824
4825 if ( empty( $install_id_2_blog_id ) ) {
4826 return true;
4827 }
4828
4829 $params = array(
4830 'permissions' => implode( ',', $permissions ),
4831 'is_enabled' => $is_enabled,
4832 'install_ids' => implode( ',', array_keys( $install_id_2_blog_id ) ),
4833 );
4834
4835 // Send update to FS.
4836 $result = $this->get_current_or_network_user_api_scope()->call(
4837 "/plugins/{$this->_module_id}/installs/permissions.json",
4838 'put',
4839 $params
4840 );
4841
4842 if ( ! $this->is_api_result_object( $result, 'installs_metadata' ) ) {
4843 $this->_logger->api_error( $result );
4844
4845 return $result;
4846 }
4847
4848 return true;
4849 }
4850
4851 /**
4852 * @param mixed $result
4853 *
4854 * @return string
4855 */
4856 private function get_api_error_message( $result ) {
4857 $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:',
4858 'unexpected-api-error' ), $this->_module_type ) . ' ';
4859
4860 if (
4861 $this->is_api_error( $result ) &&
4862 isset( $result->error )
4863 ) {
4864 $code = empty( $result->error->code ) ? '' : " Code: {$result->error->code}";
4865
4866 $error_message .= "<b>{$result->error->message}{$code}</b>";
4867 } else {
4868 $error_message .= var_export( $result, true );
4869 }
4870
4871 return $error_message;
4872 }
4873
4874 /**
4875 * @author Vova Feldman (@svovaf)
4876 * @since 2.5.1
4877 */
4878 function _toggle_permission_tracking_callback() {
4879 $this->_logger->entrance();
4880
4881 $this->check_ajax_referer( 'toggle_permission_tracking' );
4882
4883 if ( ! $this->is_registered( true ) ) {
4884 self::shoot_ajax_failure( 'User never opted-in.' );
4885 }
4886
4887 $is_enabled = fs_request_get_bool( 'is_enabled' );
4888 $permissions = fs_request_get( 'permissions' );
4889
4890 if ( ! is_string( $permissions ) ) {
4891 self::shoot_ajax_failure( 'The permissions param must be a string.' );
4892 }
4893
4894 $permissions = explode( ',', $permissions );
4895
4896 $result = $this->toggle_permission_tracking( $permissions, $is_enabled );
4897
4898 if ( true !== $result ) {
4899 self::shoot_ajax_failure( $this->get_api_error_message( $result ) );
4900 }
4901
4902 self::shoot_ajax_success();
4903 }
4904
4905 /**
4906 * @param string[] $permissions
4907 * @param bool $is_enabled
4908 * @param int|null $blog_id
4909 *
4910 * @return bool|mixed `true` if updated successfully or no update is needed.
4911 */
4912 private function toggle_permission_tracking( $permissions, $is_enabled, $blog_id = null ) {
4913 if ( ! $this->is_registered( true ) ) {
4914 // User never opted-in.
4915 return false;
4916 }
4917
4918 // Check if permissions are already set as needed.
4919 if ( FS_Permission_Manager::instance( $this )->are_permissions( $permissions, $is_enabled, $blog_id ) ) {
4920 /**
4921 * Note:
4922 * 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.
4923 */
4924 return true;
4925 }
4926
4927 $api_managed_permissions = array_intersect(
4928 $permissions,
4929 FS_Permission_Manager::get_api_managed_permission_ids()
4930 );
4931
4932 if (
4933 in_array( FS_Permission_Manager::PERMISSION_ESSENTIALS, $permissions ) &&
4934 ! in_array( FS_Permission_Manager::PERMISSION_SITE, $permissions )
4935 ) {
4936 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_SITE;
4937 }
4938
4939 if ( ! empty( $api_managed_permissions ) ) {
4940 $has_site_delegated_connection = false;
4941
4942 if (
4943 ! $is_enabled &&
4944 ! in_array( FS_Permission_Manager::PERMISSION_EXTENSIONS, $api_managed_permissions ) &&
4945 false === FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed( $blog_id )
4946 ) {
4947 /**
4948 * 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.
4949 *
4950 * @todo Remove this entire `if` after implementing granular opt-in that also sends the permissions to the API when opting in.
4951 */
4952 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_EXTENSIONS;
4953 }
4954
4955 if ( is_null( $blog_id ) && fs_is_network_admin() ) {
4956 $result = $this->update_network_permissions(
4957 $api_managed_permissions,
4958 $is_enabled,
4959 $has_site_delegated_connection
4960 );
4961 } else {
4962 $result = $this->update_site_permissions(
4963 $api_managed_permissions,
4964 $is_enabled,
4965 $blog_id
4966 );
4967 }
4968
4969 if ( true !== $result ) {
4970 return $result;
4971 }
4972
4973 if ( in_array( FS_Permission_Manager::PERMISSION_SITE, $api_managed_permissions ) ) {
4974 if ( $is_enabled ) {
4975 $this->schedule_sync_cron();
4976 } else {
4977 $this->clear_sync_cron( ! $has_site_delegated_connection );
4978 }
4979 }
4980
4981 if ( in_array( FS_Permission_Manager::PERMISSION_USER, $api_managed_permissions ) ) {
4982 $this->toggle_user_permission( $is_enabled, $blog_id );
4983 }
4984 }
4985
4986 $this->update_tracking_permissions(
4987 $permissions,
4988 $is_enabled,
4989 $blog_id
4990 );
4991
4992 return true;
4993 }
4994
4995 /**
4996 * @param bool $is_enabled
4997 * @param int|null $blog_id
4998 */
4999 private function toggle_user_permission( $is_enabled, $blog_id = null ) {
5000 $network_or_blog_ids = is_numeric( $blog_id ) ?
5001 $blog_id :
5002 fs_is_network_admin();
5003
5004 if ( $is_enabled ) {
5005 $this->reset_anonymous_mode( $network_or_blog_ids );
5006 } else {
5007 $this->skip_connection( $network_or_blog_ids );
5008 }
5009 }
5010
5011 /**
5012 * Opt-in back into usage tracking.
5013 *
5014 * Note: This will only work if the user opted-in previously.
5015 *
5016 * Returns:
5017 * 1. FALSE - If the user never opted-in.
5018 * 2. TRUE - If successfully opted-in back to usage tracking.
5019 * 3. object - API result on failure.
5020 *
5021 * @author Leo Fajardo (@leorw)
5022 * @since 1.2.1.5
5023 *
5024 * @bool $is_enabled
5025 *
5026 * @return bool|object
5027 */
5028 private function toggle_site_tracking( $is_enabled, $blog_id = null ) {
5029 $this->_logger->entrance();
5030
5031 return $this->toggle_permission_tracking(
5032 FS_Permission_Manager::instance( $this )->get_site_tracking_permission_names(),
5033 $is_enabled,
5034 $blog_id
5035 );
5036 }
5037
5038 /**
5039 * If user opted-in and later disabled usage-tracking,
5040 * re-allow tracking for licensing and updates.
5041 *
5042 * @author Leo Fajardo (@leorw)
5043 * @since 1.2.1.5
5044 *
5045 * @param bool $is_context_single_site
5046 */
5047 private function reconnect_locally( $is_context_single_site = false ) {
5048 $this->_logger->entrance();
5049
5050 if ( ! $this->is_registered() ) {
5051 return;
5052 }
5053
5054 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5055 if ( $this->is_tracking_prohibited() ) {
5056 FS_Permission_Manager::instance( $this )->update_site_tracking( true );
5057 }
5058 } else {
5059 $installs_map = $this->get_blog_install_map();
5060 foreach ( $installs_map as $blog_id => $install ) {
5061 /**
5062 * @var FS_Site $install
5063 */
5064 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
5065 FS_Permission_Manager::instance( $this )->update_site_tracking( true, $blog_id );
5066 }
5067 }
5068 }
5069 }
5070
5071 /**
5072 * 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.
5073 *
5074 * @param string[] $permissions
5075 * @param bool $is_enabled
5076 * @param int|null $blog_id
5077 *
5078 * @return array
5079 */
5080 private function update_tracking_permissions( $permissions, $is_enabled, $blog_id = null ) {
5081 // Alias.
5082 $permission_manager = FS_Permission_Manager::instance( $this );
5083
5084 $network_or_blog_ids = is_numeric( $blog_id ) ?
5085 $blog_id :
5086 fs_is_network_admin();
5087
5088 if ( true === $network_or_blog_ids ) {
5089 // Update the permission for all non-delegated sub-sites.
5090 $blog_ids = $this->get_non_delegated_blog_ids();
5091
5092 // Add the network-level to the array, to update the permission on the network-level storage.
5093 array_unshift( $blog_ids, null );
5094 }
5095 else
5096 {
5097 if ( false === $network_or_blog_ids ) {
5098 $network_or_blog_ids = null;
5099 }
5100
5101 $blog_ids = is_array( $network_or_blog_ids ) ?
5102 $network_or_blog_ids :
5103 array( $network_or_blog_ids );
5104 }
5105
5106 $result = array();
5107 foreach ( $permissions as $permission ) {
5108 $permission = trim( $permission );
5109 $is_permission_supported = true;
5110
5111 foreach ( $blog_ids as $id ) {
5112 $is_permission_supported = $permission_manager->update_permission_tracking_flag(
5113 $permission,
5114 $is_enabled,
5115 $id
5116 );
5117 }
5118
5119 if ( ! $is_permission_supported ) {
5120 $permission = 'no_match';
5121 }
5122
5123 $result[ $permission ] = $is_enabled;
5124 }
5125
5126 return $result;
5127 }
5128
5129 /**
5130 * Parse plugin's settings (as defined by the plugin dev).
5131 *
5132 * @author Vova Feldman (@svovaf)
5133 * @since 1.1.7.3
5134 *
5135 * @param array $plugin_info
5136 *
5137 * @throws \Freemius_Exception
5138 */
5139 private function parse_settings( &$plugin_info ) {
5140 $this->_logger->entrance();
5141
5142 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5143 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5144 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5145 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5146 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5147
5148 /**
5149 * @author Vova Feldman (@svovaf)
5150 * @since 1.1.9 Try to pull secret key from external config.
5151 */
5152 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5153 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5154 }
5155
5156 if ( isset( $plugin_info['parent'] ) ) {
5157 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5158 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5159 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5160 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5161 }
5162
5163 if ( false === $id ) {
5164 throw new Freemius_Exception( array(
5165 'error' => array(
5166 'type' => 'ParameterNotSet',
5167 'message' => 'Plugin id parameter is not set.',
5168 'code' => 'plugin_id_not_set',
5169 'http' => 500,
5170 )
5171 ) );
5172 }
5173 if ( false === $public_key ) {
5174 throw new Freemius_Exception( array(
5175 'error' => array(
5176 'type' => 'ParameterNotSet',
5177 'message' => 'Plugin public_key parameter is not set.',
5178 'code' => 'plugin_public_key_not_set',
5179 'http' => 500,
5180 )
5181 ) );
5182 }
5183
5184 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5185 $this->_plugin :
5186 new FS_Plugin();
5187
5188 $is_premium = $this->get_bool_option( $plugin_info, 'is_premium', true );
5189 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5190
5191 $module_type = $this->get_option( $plugin_info, 'type', $this->_module_type );
5192
5193 $parallel_activation = $this->get_option( $plugin_info, 'parallel_activation' );
5194
5195 if (
5196 ! $is_premium &&
5197 is_array( $parallel_activation ) &&
5198 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type ) &&
5199 $this->get_bool_option( $parallel_activation, 'enabled' )
5200 ) {
5201 $premium_basename = $this->get_option( $parallel_activation, 'premium_version_basename' );
5202
5203 if ( empty( $premium_basename ) ) {
5204 throw new Exception('You need to specify the premium version basename to enable parallel version activation.');
5205 }
5206
5207 $this->_premium_plugin_basename_from_parallel_activation = $premium_basename;
5208
5209 if ( is_plugin_active( $premium_basename ) ) {
5210 $is_premium = true;
5211 }
5212 }
5213
5214 $plugin->update( array(
5215 'id' => $id,
5216 'type' => $module_type,
5217 'public_key' => $public_key,
5218 'slug' => $this->_slug,
5219 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5220 'parent_plugin_id' => $parent_id,
5221 'version' => $this->get_plugin_version(),
5222 'title' => $this->get_plugin_name( $premium_suffix ),
5223 'file' => $this->_plugin_basename,
5224 'is_premium' => $is_premium,
5225 'premium_suffix' => $premium_suffix,
5226 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5227 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5228 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5229 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ),
5230 'opt_in_moderation' => $this->get_option(
5231 $plugin_info,
5232 'opt_in',
5233 // For backward compatibility, we support both parameter names: opt_in and opt_in_moderation.
5234 $this->get_option( $plugin_info, 'opt_in_moderation', null )
5235 ),
5236 ) );
5237
5238 if ( $plugin->is_updated() ) {
5239 // Update plugin details.
5240 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5241 }
5242 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5243 $this->_plugin->secret_key = $secret_key;
5244
5245 /**
5246 * 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).
5247 *
5248 * @author Vova Feldman
5249 * @since 2.4.5
5250 */
5251 if ( $this->is_network_active() && fs_is_network_admin() ) {
5252 if ( isset( $plugin_info['menu_network'] ) &&
5253 is_array( $plugin_info['menu_network'] ) &&
5254 ! empty( $plugin_info['menu_network'] )
5255 ) {
5256 $plugin_info['menu'] = $plugin_info['menu_network'];
5257 }
5258 }
5259
5260 if ( ! isset( $plugin_info['menu'] ) ) {
5261 $plugin_info['menu'] = array();
5262
5263 if ( ! empty( $this->_storage->sdk_last_version ) &&
5264 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5265 ) {
5266 // Backward compatibility to 1.1.2
5267 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5268 $plugin_info['menu_slug'] :
5269 $this->_slug;
5270 }
5271 }
5272
5273 $this->_menu = FS_Admin_Menu_Manager::instance(
5274 $this->_module_id,
5275 $this->_module_type,
5276 $this->get_unique_affix()
5277 );
5278
5279 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5280
5281 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5282 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5283 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5284 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5285 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5286 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5287 if ( $this->_is_premium_only ) {
5288 // If premium only plugin, disable anonymous mode.
5289 $this->_enable_anonymous = false;
5290 $this->_anonymous_mode = false;
5291 } else {
5292 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5293 $this->_anonymous_mode = (
5294 $this->get_bool_option( $plugin_info, 'anonymous_mode', false ) ||
5295 (
5296 $this->apply_filters( 'playground_anonymous_mode', true ) &&
5297 ! empty( $_SERVER['HTTP_HOST'] ) &&
5298 FS_Site::is_playground_wp_environment_by_host( $_SERVER['HTTP_HOST'] )
5299 )
5300 );
5301 }
5302 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5303 $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
5304
5305 if ( ! empty( $plugin_info['trial'] ) ) {
5306 $this->_trial_days = $this->get_numeric_option(
5307 $plugin_info['trial'],
5308 'days',
5309 // Default to 0 - trial without days specification.
5310 0
5311 );
5312
5313 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5314 }
5315
5316 $this->_navigation = $this->get_option(
5317 $plugin_info,
5318 'navigation',
5319 $this->is_free_wp_org_theme() ?
5320 self::NAVIGATION_TABS :
5321 self::NAVIGATION_MENU
5322 );
5323 }
5324
5325 /**
5326 * @param string[] $options
5327 * @param string $key
5328 * @param mixed $default
5329 *
5330 * @return bool
5331 */
5332 private function get_option( &$options, $key, $default = false ) {
5333 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5334 }
5335
5336 private function get_bool_option( &$options, $key, $default = false ) {
5337 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5338 }
5339
5340 private function get_numeric_option( &$options, $key, $default = false ) {
5341 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5342 }
5343
5344 /**
5345 * Gate keeper.
5346 *
5347 * @author Vova Feldman (@svovaf)
5348 * @since 1.1.7.3
5349 *
5350 * @return bool
5351 */
5352 private function should_stop_execution() {
5353 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5354 /**
5355 * Don't execute Freemius until plugin was fully loaded at least once,
5356 * to give the opportunity for the activation hook to run before pinging
5357 * the API for connectivity test. This logic is relevant for the
5358 * identification of new plugin install vs. plugin update.
5359 *
5360 * @author Vova Feldman (@svovaf)
5361 * @since 1.1.9
5362 */
5363 return true;
5364 }
5365
5366 if ( $this->is_activation_mode() ) {
5367 if ( ! is_admin() ) {
5368 /**
5369 * If in activation mode, don't execute Freemius outside the admin dashboard.
5370 *
5371 * @author Vova Feldman (@svovaf)
5372 * @since 1.1.7.3
5373 */
5374 return true;
5375 }
5376
5377 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5378 /**
5379 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5380 * then don't start Freemius.
5381 *
5382 * @author Vova Feldman (@svovaf)
5383 * @since 1.1.6.3
5384 *
5385 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5386 */
5387 return true;
5388 }
5389
5390 if ( self::is_cron() ) {
5391 /**
5392 * If in activation mode, don't execute Freemius during wp crons
5393 * (wp crons have HTTP context - called as HTTP request).
5394 *
5395 * @author Vova Feldman (@svovaf)
5396 * @since 1.1.7.3
5397 */
5398 return true;
5399 }
5400
5401 if ( self::is_ajax() ) {
5402 /**
5403 * During activation, if running in AJAX mode, unless there's a sticky
5404 * connectivity issue notice, don't run Freemius.
5405 *
5406 * @author Vova Feldman (@svovaf)
5407 * @since 1.1.7.3
5408 */
5409 return true;
5410 }
5411 }
5412
5413 return false;
5414 }
5415
5416 /**
5417 * Triggered after code type has changed.
5418 *
5419 * @author Vova Feldman (@svovaf)
5420 * @since 1.1.9.1
5421 */
5422 function _after_code_type_change() {
5423 $this->_logger->entrance();
5424
5425 if ( $this->is_theme() ) {
5426 // Expire the cache of the previous tabs since the theme may
5427 // have setting updates after code type has changed.
5428 $this->_cache->expire( 'tabs' );
5429 $this->_cache->expire( 'tabs_stylesheets' );
5430 }
5431
5432 if ( ! $this->is_addon() ) {
5433 add_action(
5434 is_admin() ? 'admin_init' : 'init',
5435 array( &$this, '_plugin_code_type_changed' )
5436 );
5437 }
5438
5439 if ( $this->is_registered() && $this->is_premium() ) {
5440 // Purge cached payments after switching to the premium version.
5441 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5442 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5443 }
5444 }
5445
5446 /**
5447 * Handles plugin's code type change (free <--> premium).
5448 *
5449 * @author Vova Feldman (@svovaf)
5450 * @since 1.0.9
5451 */
5452 function _plugin_code_type_changed() {
5453 $this->_logger->entrance();
5454
5455 if ( $this->is_premium() ) {
5456 $this->reconnect_locally();
5457
5458 // Activated premium code.
5459 $this->do_action( 'after_premium_version_activation' );
5460
5461 // Remove all sticky messages related to download of the premium version.
5462 $this->_admin_notices->remove_sticky( array(
5463 'trial_started',
5464 'plan_upgraded',
5465 'plan_changed',
5466 'license_activated',
5467 ) );
5468
5469 $notice = '';
5470 if ( ! $this->is_only_premium() ) {
5471 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5472 }
5473
5474 $license_notice = $this->get_license_network_activation_notice();
5475 if ( ! empty( $license_notice ) ) {
5476 $notice .= ' ' . $license_notice;
5477 }
5478
5479 if ( ! empty( $notice ) ) {
5480 $this->_admin_notices->add_sticky(
5481 trim( $notice ),
5482 'premium_activated',
5483 $this->get_text_x_inline( 'W00t',
5484 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5485 );
5486 }
5487 } else {
5488 // Remove sticky message related to premium code activation.
5489 $this->_admin_notices->remove_sticky( 'premium_activated' );
5490
5491 // Activated free code (after had the premium before).
5492 $this->do_action( 'after_free_version_reactivation' );
5493
5494 if ( $this->is_paying() && ! $this->is_premium() ) {
5495 $this->add_complete_upgrade_instructions_notice(
5496 sprintf(
5497 /* translators: %s: License type (e.g. you have a professional license) */
5498 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5499 $this->get_plan_title()
5500 ),
5501 'plan_upgraded'
5502 );
5503 }
5504 }
5505
5506 if ( $this->is_registered() ) {
5507 // Schedule code type changes event.
5508 $this->maybe_schedule_install_sync_cron();
5509 }
5510
5511 /**
5512 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5513 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5514 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5515 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5516 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5517 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5518 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5519 */
5520 $this->unregister_uninstall_hook();
5521
5522 $this->clear_module_main_file_cache();
5523
5524 // Update is_premium of latest version.
5525 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5526 }
5527
5528 #endregion
5529
5530 #----------------------------------------------------------------------------------
5531 #region Add-ons
5532 #----------------------------------------------------------------------------------
5533
5534 /**
5535 * Check if add-on installed and activated on site.
5536 *
5537 * @author Vova Feldman (@svovaf)
5538 * @since 1.0.6
5539 *
5540 * @param string|number $id_or_slug
5541 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5542 *
5543 * @return bool
5544 */
5545 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5546 $this->_logger->entrance();
5547
5548 $addon_id = self::get_module_id( $id_or_slug );
5549 $is_activated = self::has_instance( $addon_id );
5550
5551 if ( ! $is_activated ) {
5552 return false;
5553 }
5554
5555 if ( is_bool( $is_premium ) ) {
5556 // Check if the specified code version is activate.
5557 $addon = $this->get_addon_instance( $addon_id );
5558 $is_activated = ( $is_premium === $addon->is_premium() );
5559 }
5560
5561 return $is_activated;
5562 }
5563
5564 /**
5565 * Check if add-on was connected to install
5566 *
5567 * @author Vova Feldman (@svovaf)
5568 * @since 1.1.7
5569 *
5570 * @param string|number $id_or_slug
5571 *
5572 * @return bool
5573 */
5574 function is_addon_connected( $id_or_slug ) {
5575 $this->_logger->entrance();
5576
5577 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5578
5579 $addon_id = self::get_module_id( $id_or_slug );
5580 $addon = $this->get_addon( $addon_id );
5581 $slug = $addon->slug;
5582 if ( ! isset( $sites[ $slug ] ) ) {
5583 return false;
5584 }
5585
5586 $site = $sites[ $slug ];
5587
5588 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5589
5590 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5591 // The given slug do NOT belong to any of the plugin's add-ons.
5592 return false;
5593 }
5594
5595 return ( is_object( $site ) &&
5596 is_numeric( $site->id ) &&
5597 is_numeric( $site->user_id ) &&
5598 FS_Plugin_Plan::is_valid_id( $site->plan_id )
5599 );
5600 }
5601
5602 /**
5603 * Determines if add-on installed.
5604 *
5605 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
5606 *
5607 * @author Vova Feldman (@svovaf)
5608 * @since 1.0.6
5609 *
5610 * @param string|number $id_or_slug
5611 *
5612 * @return bool
5613 */
5614 function is_addon_installed( $id_or_slug ) {
5615 $this->_logger->entrance();
5616
5617 $addon_id = self::get_module_id( $id_or_slug );
5618
5619 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
5620 }
5621
5622 /**
5623 * Get add-on basename.
5624 *
5625 * @author Vova Feldman (@svovaf)
5626 * @since 1.0.6
5627 *
5628 * @param string|number $id_or_slug
5629 *
5630 * @return string
5631 */
5632 function get_addon_basename( $id_or_slug ) {
5633 $addon_id = self::get_module_id( $id_or_slug );
5634
5635 if ( $this->is_addon_activated( $addon_id ) ) {
5636 return self::instance( $addon_id )->get_plugin_basename();
5637 }
5638
5639 $addon = $this->get_addon( $addon_id );
5640 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
5641
5642 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
5643 return $premium_basename;
5644 }
5645
5646 $all_plugins = $this->get_all_plugins();
5647
5648 foreach ( $all_plugins as $basename => $data ) {
5649 if ( $addon->slug === $data['slug'] ||
5650 $addon->premium_slug === $data['slug']
5651 ) {
5652 return $basename;
5653 }
5654 }
5655
5656 $free_basename = "{$addon->slug}/{$addon->slug}.php";
5657
5658 return $free_basename;
5659 }
5660
5661 /**
5662 * Get installed add-ons instances.
5663 *
5664 * @author Vova Feldman (@svovaf)
5665 * @since 1.0.6
5666 *
5667 * @return Freemius[]
5668 */
5669 function get_installed_addons() {
5670 if ( $this->is_addon() ) {
5671 // Add-on cannot have add-ons.
5672 return array();
5673 }
5674
5675 $installed_addons = array();
5676
5677 foreach ( self::$_instances as $instance ) {
5678 if ( $instance->is_addon_of( $this->_plugin->id ) ) {
5679 $installed_addons[] = $instance;
5680 }
5681 }
5682
5683 return $installed_addons;
5684 }
5685
5686 /**
5687 * Check if any add-ons of the plugin are installed.
5688 *
5689 * @author Leo Fajardo (@leorw)
5690 * @since 1.1.1
5691 *
5692 * @return bool
5693 */
5694 function has_installed_addons() {
5695 if ( ! $this->has_addons() ) {
5696 return false;
5697 }
5698
5699 foreach ( self::$_instances as $instance ) {
5700 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
5701 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
5702 return true;
5703 }
5704 }
5705 }
5706
5707 return false;
5708 }
5709
5710 /**
5711 * Tell Freemius that the current plugin is an add-on.
5712 *
5713 * @author Vova Feldman (@svovaf)
5714 * @since 1.0.6
5715 *
5716 * @param number $parent_plugin_id The parent plugin ID
5717 */
5718 function init_addon( $parent_plugin_id ) {
5719 $this->_plugin->parent_plugin_id = $parent_plugin_id;
5720 }
5721
5722 /**
5723 * @author Vova Feldman (@svovaf)
5724 * @since 1.0.6
5725 *
5726 * @return bool
5727 */
5728 function is_addon() {
5729 return (
5730 isset( $this->_plugin->parent_plugin_id ) &&
5731 is_numeric( $this->_plugin->parent_plugin_id )
5732 );
5733 }
5734
5735 /**
5736 * @author Vova Feldman (@svovaf)
5737 * @since 2.3.2
5738 *
5739 * @param number $parent_product_id
5740 *
5741 * @return bool
5742 */
5743 function is_addon_of( $parent_product_id ) {
5744 return (
5745 $this->is_addon() &&
5746 $parent_product_id == $this->_plugin->parent_plugin_id
5747 );
5748 }
5749
5750 /**
5751 * Deactivate add-on if it's premium only and the user does't have a valid license.
5752 *
5753 * @param bool $is_after_trial_cancel
5754 *
5755 * @return bool If add-on was deactivated.
5756 */
5757 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
5758 if ( ! $this->has_free_plan() &&
5759 ! $this->has_features_enabled_license() &&
5760 ! $this->_has_premium_license()
5761 ) {
5762 if ( $this->is_registered() ) {
5763 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
5764 // if (empty($this->_storage->activation_timestamp) ||
5765 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
5766 // ) {
5767 /**
5768 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
5769 *
5770 * Retry syncing the user add-on licenses.
5771 */
5772 // Sync licenses.
5773 $this->_sync_licenses();
5774 // }
5775
5776 // Try to activate premium license.
5777 $this->_activate_license( true );
5778 }
5779
5780 if ( ! $this->has_free_plan() &&
5781 ! $this->has_features_enabled_license() &&
5782 ! $this->_has_premium_license()
5783 ) {
5784 // @todo Check if deactivate plugins also call the deactivation hook.
5785
5786 $this->_parent->_admin_notices->add_sticky(
5787 sprintf(
5788 ( $is_after_trial_cancel ?
5789 $this->_parent->get_text_inline(
5790 '%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.',
5791 'addon-trial-cancelled-message'
5792 ) :
5793 $this->_parent->get_text_inline(
5794 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
5795 'addon-no-license-message'
5796 )
5797 ),
5798 '<b>' . $this->_plugin->title . '</b>'
5799 ) . ' ' . sprintf(
5800 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
5801 $this->_parent->addon_url( $this->_slug ),
5802 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
5803 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
5804 ),
5805 'no_addon_license_' . $this->_slug,
5806 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
5807 ( $is_after_trial_cancel ? 'success' : 'error' )
5808 );
5809
5810 deactivate_plugins( array( $this->_plugin_basename ), true );
5811
5812 return true;
5813 }
5814 }
5815
5816 return false;
5817 }
5818
5819 #endregion
5820
5821 #----------------------------------------------------------------------------------
5822 #region Sandbox
5823 #----------------------------------------------------------------------------------
5824
5825 /**
5826 * Set Freemius into sandbox mode for debugging.
5827 *
5828 * @author Vova Feldman (@svovaf)
5829 * @since 1.0.4
5830 *
5831 * @param string $secret_key
5832 */
5833 function init_sandbox( $secret_key ) {
5834 $this->_plugin->secret_key = $secret_key;
5835
5836 // Update plugin details.
5837 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
5838 }
5839
5840 /**
5841 * Check if running payments in sandbox mode.
5842 *
5843 * @author Vova Feldman (@svovaf)
5844 * @since 1.0.4
5845 *
5846 * @return bool
5847 */
5848 function is_payments_sandbox() {
5849 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
5850 }
5851
5852 #endregion
5853
5854 /**
5855 * Check if running test vs. live plugin.
5856 *
5857 * @author Vova Feldman (@svovaf)
5858 * @since 1.0.5
5859 *
5860 * @return bool
5861 */
5862 function is_live() {
5863 return $this->_plugin->is_live;
5864 }
5865
5866 /**
5867 * Check if super-admin skipped connection for all sites in the network.
5868 *
5869 * @author Vova Feldman (@svovaf)
5870 * @since 2.0.0
5871 */
5872 function is_network_anonymous() {
5873 if ( ! $this->_is_network_active ) {
5874 return false;
5875 }
5876
5877 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
5878
5879 if ( empty( $is_anonymous_ms ) ) {
5880 return false;
5881 }
5882
5883 return $is_anonymous_ms['is'];
5884 }
5885
5886 /**
5887 * Check if super-admin opted-in for all sites in the network.
5888 *
5889 * @author Vova Feldman (@svovaf)
5890 * @since 2.0.0
5891 */
5892 function is_network_connected() {
5893 if ( ! $this->_is_network_active ) {
5894 return false;
5895 }
5896
5897 return $this->_storage->get( 'is_network_connected' );
5898 }
5899
5900 /**
5901 * Check if the user skipped connecting the account with Freemius.
5902 *
5903 * @author Vova Feldman (@svovaf)
5904 * @since 1.0.7
5905 *
5906 * @return bool
5907 */
5908 function is_anonymous() {
5909 if ( ! isset( $this->_is_anonymous ) ) {
5910 if ( $this->is_network_anonymous() ) {
5911 $this->_is_anonymous = true;
5912 } else if ( fs_is_network_admin() ) {
5913 /**
5914 * When not-network-anonymous, yet, running in the network admin, consider as anonymous only when ALL non-delegated sites are set to anonymous.
5915 */
5916 $non_delegated_sites = $this->get_non_delegated_blog_ids();
5917
5918 foreach ( $non_delegated_sites as $blog_id ) {
5919 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
5920
5921 if ( empty( $is_anonymous ) || false === $is_anonymous[ 'is' ] ) {
5922 $this->_is_anonymous = false;
5923 break;
5924 }
5925 }
5926
5927 if ( false !== $this->_is_anonymous ) {
5928 $this->_is_anonymous = true;
5929 }
5930 } else {
5931 if ( ! isset( $this->_storage->is_anonymous ) ) {
5932 // Not skipped.
5933 $this->_is_anonymous = false;
5934 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
5935 // For back compatibility, since the variable was boolean before.
5936 $this->_is_anonymous = $this->_storage->is_anonymous;
5937
5938 // Upgrade stored data format to 1.1.3 format.
5939 $this->set_anonymous_mode( $this->_storage->is_anonymous );
5940 } else {
5941 // Version 1.1.3 and later.
5942 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
5943 }
5944 }
5945 }
5946
5947 return $this->_is_anonymous;
5948 }
5949
5950 /**
5951 * Check if the user skipped the connection of a specified site.
5952 *
5953 * @author Vova Feldman (@svovaf)
5954 * @since 2.0.0
5955 *
5956 * @param int $blog_id
5957 *
5958 * @return bool
5959 */
5960 function is_anonymous_site( $blog_id = 0 ) {
5961 if ( $this->is_network_anonymous() ) {
5962 return true;
5963 }
5964
5965 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
5966
5967 if ( empty( $is_anonymous ) ) {
5968 return false;
5969 }
5970
5971 return $is_anonymous['is'];
5972 }
5973
5974 /**
5975 * Check if user connected his account and install pending email activation.
5976 *
5977 * @author Vova Feldman (@svovaf)
5978 * @since 1.0.7
5979 *
5980 * @return bool
5981 */
5982 function is_pending_activation() {
5983 return $this->_storage->get( 'is_pending_activation', false );
5984 }
5985
5986 /**
5987 * @author Leo Fajardo (@leorw)
5988 * @since 2.5.0
5989 */
5990 private function clear_pending_activation_mode() {
5991 // Remove the pending activation sticky notice (if it still exists).
5992 $this->_admin_notices->remove_sticky( 'activation_pending' );
5993
5994 // Clear the plugin's pending activation mode.
5995 unset( $this->_storage->is_pending_activation );
5996 }
5997
5998 /**
5999 * Check if plugin must be WordPress.org compliant.
6000 *
6001 * @since 1.0.7
6002 *
6003 * @return bool
6004 */
6005 function is_org_repo_compliant() {
6006 return $this->_is_org_compliant;
6007 }
6008
6009 #--------------------------------------------------------------------------------
6010 #region WP Cron Common
6011 #--------------------------------------------------------------------------------
6012
6013 /**
6014 * @author Vova Feldman (@svovaf)
6015 * @since 2.0.0
6016 *
6017 * @param string $name Cron name.
6018 *
6019 * @return object
6020 */
6021 private function get_cron_data( $name ) {
6022 $this->_logger->entrance( $name );
6023
6024 /**
6025 * @var object $cron_data
6026 */
6027 return $this->_storage->get( "{$name}_cron", null );
6028 }
6029
6030 /**
6031 * @author Vova Feldman (@svovaf)
6032 * @since 2.0.0
6033 *
6034 * @param string $name Cron name.
6035 */
6036 private function clear_cron_data( $name ) {
6037 $this->_logger->entrance( $name );
6038
6039 $this->_storage->remove( "{$name}_cron" );
6040 }
6041
6042 /**
6043 * @author Vova Feldman (@svovaf)
6044 * @since 2.0.0
6045 *
6046 * @param string $name Cron name.
6047 * @param int $cron_blog_id The cron executing blog ID.
6048 */
6049 private function set_cron_data( $name, $cron_blog_id = 0 ) {
6050 $this->_logger->entrance( $name );
6051
6052 $this->_storage->store( "{$name}_cron", (object) array(
6053 'version' => $this->get_plugin_version(),
6054 'blog_id' => $cron_blog_id,
6055 'sdk_version' => $this->version,
6056 'timestamp' => WP_FS__SCRIPT_START_TIME,
6057 'on' => true,
6058 ) );
6059 }
6060
6061 /**
6062 * Get the cron's executing blog ID.
6063 *
6064 * @author Vova Feldman (@svovaf)
6065 * @since 2.0.0
6066 *
6067 * @param string $name Cron name.
6068 *
6069 * @return int
6070 */
6071 private function get_cron_blog_id( $name ) {
6072 $this->_logger->entrance( $name );
6073
6074 if ( ! is_multisite() ) {
6075 // Not a multisite.
6076 return 0;
6077 }
6078
6079 $cron_data = $this->get_cron_data( $name );
6080
6081 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6082 $cron_data->blog_id :
6083 0;
6084 }
6085
6086 /**
6087 * @author Vova Feldman (@svovaf)
6088 * @since 2.0.0
6089 *
6090 * @param string $name Cron name.
6091 *
6092 * @return bool
6093 */
6094 private function is_cron_on( $name ) {
6095 $this->_logger->entrance( $name );
6096
6097 /**
6098 * @var object $cron_data
6099 */
6100 $cron_data = $this->get_cron_data( $name );
6101
6102 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6103 }
6104
6105 /**
6106 * Unix timestamp for previous cron execution or false if never executed.
6107 *
6108 * @author Vova Feldman (@svovaf)
6109 * @since 2.0.0
6110 *
6111 * @param string $name Cron name.
6112 *
6113 * @return int|false
6114 */
6115 private function cron_last_execution( $name ) {
6116 $this->_logger->entrance( $name );
6117
6118 return $this->_storage->get( "{$name}_timestamp" );
6119 }
6120
6121 /**
6122 * Set cron execution time to now.
6123 *
6124 * @author Vova Feldman (@svovaf)
6125 * @since 2.0.0
6126 *
6127 * @param string $name Cron name.
6128 */
6129 private function set_cron_execution_timestamp( $name ) {
6130 $this->_logger->entrance( $name );
6131
6132 $this->_storage->store( "{$name}_timestamp", time() );
6133 }
6134
6135 /**
6136 * Sets the keepalive time to now.
6137 *
6138 * @author Leo Fajardo (@leorw)
6139 * @since 2.2.3
6140 *
6141 * @param bool|null $use_network_level_storage
6142 */
6143 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6144 $this->_logger->entrance();
6145
6146 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6147 }
6148
6149 /**
6150 * Check if cron was executed in the last $period of seconds.
6151 *
6152 * @author Vova Feldman (@svovaf)
6153 * @since 2.0.0
6154 *
6155 * @param string $name Cron name.
6156 * @param int $period In seconds
6157 *
6158 * @return bool
6159 */
6160 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6161 $this->_logger->entrance( $name );
6162
6163 $last_execution = $this->cron_last_execution( $name );
6164
6165 if ( ! is_numeric( $last_execution ) ) {
6166 return false;
6167 }
6168
6169 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6170 }
6171
6172 /**
6173 * WP Cron is executed on a site level. When running in a multisite network environment
6174 * with the network integration activated, for optimization reasons, we are consolidating
6175 * the installs data sync cron to be executed only from a single site.
6176 *
6177 * @author Vova Feldman (@svovaf)
6178 * @since 2.0.0
6179 *
6180 * @param int $except_blog_id Target any except the excluded blog ID.
6181 *
6182 * @return int
6183 */
6184 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6185 if ( ! is_multisite() ) {
6186 return 0;
6187 }
6188
6189 if ( $this->_is_network_active ) {
6190 $network_install_blog_id = $this->_storage->network_install_blog_id;
6191
6192 if (
6193 is_numeric( $network_install_blog_id ) &&
6194 $except_blog_id != $network_install_blog_id &&
6195 self::is_site_active( $network_install_blog_id )
6196 ) {
6197 // Try to run cron from the main network blog.
6198 $install = $this->get_install_by_blog_id( $network_install_blog_id );
6199
6200 if (
6201 is_object( $install ) &&
6202 $this->is_tracking_allowed( $network_install_blog_id, $install )
6203 ) {
6204 return $network_install_blog_id;
6205 }
6206 }
6207 }
6208
6209 // Get first opted-in blog ID with active tracking.
6210 $installs = $this->get_blog_install_map();
6211 foreach ( $installs as $blog_id => $install ) {
6212 if ( $except_blog_id != $blog_id &&
6213 self::is_site_active( $blog_id ) &&
6214 $this->is_tracking_allowed( $blog_id, $install )
6215 ) {
6216 return $blog_id;
6217 }
6218 }
6219
6220 return 0;
6221 }
6222
6223 /**
6224 * @author Vova Feldman (@svovaf)
6225 * @since 2.0.0
6226 *
6227 * @param string $name Cron name.
6228 * @param string $action_tag Callback action tag.
6229 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6230 */
6231 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6232 $this->_logger->entrance( $name );
6233
6234 if ( ! $this->is_cron_on( $name ) ) {
6235 return;
6236 }
6237
6238 $clear_cron = true;
6239 if ( ! $is_network_clear && $this->_is_network_active ) {
6240 $installs = $this->get_blog_install_map();
6241
6242 foreach ( $installs as $blog_id => $install ) {
6243 /**
6244 * @var FS_Site $install
6245 */
6246 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6247 $clear_cron = false;
6248 break;
6249 }
6250 }
6251 }
6252
6253 if ( ! $clear_cron ) {
6254 return;
6255 }
6256
6257 $cron_blog_id = $this->get_cron_blog_id( $name );
6258
6259 $this->clear_cron_data( $name );
6260
6261 if ( 0 < $cron_blog_id ) {
6262 switch_to_blog( $cron_blog_id );
6263 }
6264
6265 if ( empty( $action_tag ) ) {
6266 $action_tag = $name;
6267 }
6268
6269 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6270
6271 if ( 0 < $cron_blog_id ) {
6272 restore_current_blog();
6273 }
6274 }
6275
6276 /**
6277 * Unix timestamp for next cron execution or false if not scheduled.
6278 *
6279 * @author Vova Feldman (@svovaf)
6280 * @since 2.0.0
6281 *
6282 * @param string $name Cron name.
6283 * @param string $action_tag Callback action tag.
6284 *
6285 * @return int|false
6286 */
6287 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6288 $this->_logger->entrance( $name );
6289
6290 if ( ! $this->is_cron_on( $name ) ) {
6291 return false;
6292 }
6293
6294 $cron_blog_id = $this->get_cron_blog_id( $name );
6295
6296 if ( 0 < $cron_blog_id ) {
6297 switch_to_blog( $cron_blog_id );
6298 }
6299
6300 if ( empty( $action_tag ) ) {
6301 $action_tag = $name;
6302 }
6303
6304 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6305
6306 if ( 0 < $cron_blog_id ) {
6307 restore_current_blog();
6308 }
6309
6310 return $next_scheduled;
6311 }
6312
6313 /**
6314 * @author Vova Feldman (@svovaf)
6315 * @since 2.0.0
6316 *
6317 * @param string $name Cron name.
6318 * @param string $action_tag Callback action tag.
6319 * @param string $recurrence 'single' or 'daily'.
6320 * @param int $start_at Defaults to now.
6321 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6322 * @param int $except_blog_id Target any except the excluded blog ID.
6323 */
6324 private function schedule_cron(
6325 $name,
6326 $action_tag = '',
6327 $recurrence = 'single',
6328 $start_at = WP_FS__SCRIPT_START_TIME,
6329 $randomize_start = true,
6330 $except_blog_id = 0
6331 ) {
6332 $this->_logger->entrance( $name );
6333
6334 $this->clear_cron( $name, $action_tag, true );
6335
6336 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6337
6338 if ( is_multisite() && 0 == $cron_blog_id ) {
6339 // Don't schedule cron since couldn't find a target blog.
6340 return;
6341 }
6342
6343 if ( 0 < $cron_blog_id ) {
6344 switch_to_blog( $cron_blog_id );
6345 }
6346
6347 if ( 'daily' === $recurrence ) {
6348 if ( $randomize_start ) {
6349 // Schedule first sync with a random 12 hour time range from now.
6350 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6351 }
6352
6353 // Schedule daily WP cron.
6354 wp_schedule_event(
6355 $start_at,
6356 'daily',
6357 $this->get_action_tag( $action_tag )
6358 );
6359 } else if ( 'single' === $recurrence ) {
6360 // Schedule single cron.
6361 wp_schedule_single_event(
6362 $start_at,
6363 $this->get_action_tag( $action_tag )
6364 );
6365 }
6366
6367 $this->set_cron_data( $name, $cron_blog_id );
6368
6369 if ( 0 < $cron_blog_id ) {
6370 restore_current_blog();
6371 }
6372 }
6373
6374 /**
6375 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6376 * that doesn't halt page loading.
6377 *
6378 * @author Vova Feldman (@svovaf)
6379 * @since 2.0.0
6380 *
6381 * @param string $name Cron name.
6382 * @param callable $callable The function that should be executed.
6383 */
6384 private function execute_cron( $name, $callable ) {
6385 $this->_logger->entrance( $name );
6386
6387 // Store the last time data sync was executed.
6388 $this->set_cron_execution_timestamp( $name );
6389
6390 // Check if API is temporary down.
6391 if ( FS_Api::is_temporary_down() ) {
6392 return;
6393 }
6394
6395 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6396
6397 $users_2_blog_ids = array();
6398
6399 if ( ! is_multisite() ) {
6400 // Add dummy blog.
6401 $users_2_blog_ids[0] = array( 0 );
6402 } else {
6403 $installs = $this->get_blog_install_map();
6404 foreach ( $installs as $blog_id => $install ) {
6405 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6406 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6407 $users_2_blog_ids[ $install->user_id ] = array();
6408 }
6409
6410 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6411 }
6412 }
6413 }
6414
6415 $current_blog_id = get_current_blog_id();
6416
6417 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6418 if ( 0 < $blog_ids[0] ) {
6419 $this->switch_to_blog( $blog_ids[0] );
6420 }
6421
6422 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6423
6424 foreach ( $blog_ids as $blog_id ) {
6425 $this->do_action( "after_{$name}_cron", $blog_id );
6426 }
6427 }
6428
6429 if ( is_multisite() ) {
6430 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6431
6432 $this->do_action( "after_{$name}_cron_multisite" );
6433 }
6434 }
6435
6436 #endregion
6437
6438 #----------------------------------------------------------------------------------
6439 #region Daily Sync Cron
6440 #----------------------------------------------------------------------------------
6441
6442
6443 /**
6444 * @author Vova Feldman (@svovaf)
6445 * @since 2.0.0
6446 *
6447 * @return bool
6448 */
6449 private function is_sync_cron_scheduled() {
6450 return $this->is_cron_on( 'sync' );
6451 }
6452
6453 /**
6454 * Get the sync cron's executing blog ID.
6455 *
6456 * @author Vova Feldman (@svovaf)
6457 * @since 2.0.0
6458 *
6459 * @return int
6460 */
6461 private function get_sync_cron_blog_id() {
6462 return $this->get_cron_blog_id( 'sync' );
6463 }
6464
6465 /**
6466 * @author Vova Feldman (@svovaf)
6467 * @since 1.1.7.3
6468 */
6469 private function run_manual_sync() {
6470 if ( ! $this->is_user_admin() ) {
6471 return;
6472 }
6473
6474 // Run manual sync.
6475 $this->_sync_cron();
6476
6477 // Reschedule next cron to run 24 hours from now (performance optimization).
6478 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6479 }
6480
6481 /**
6482 * Data sync cron job. Replaces the background sync non blocking HTTP request
6483 * that doesn't halt page loading.
6484 *
6485 * @author Vova Feldman (@svovaf)
6486 * @since 1.1.7.3
6487 * @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.
6488 */
6489 function _sync_cron() {
6490 $this->_logger->entrance();
6491
6492 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6493 }
6494
6495 /**
6496 * The actual data sync cron logic.
6497 *
6498 * @author Vova Feldman (@svovaf)
6499 * @since 2.0.0
6500 *
6501 * @param int[] $blog_ids
6502 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6503 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6504 * updates for a single site in case `execute_cron` has switched to a different blog.
6505 */
6506 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6507 if ( $this->is_registered() ) {
6508 if ( $this->has_paid_plan() ) {
6509 // Initiate background plan sync.
6510 $this->_sync_license( true, false, $current_blog_id );
6511
6512 if ( $this->is_paying() ) {
6513 // Check for premium plugin updates.
6514 $this->check_updates( true );
6515 }
6516 } else {
6517 // Sync install(s) (only if something changed locally).
6518 if ( 1 < count( $blog_ids ) ) {
6519 $this->sync_installs();
6520 } else {
6521 $this->sync_install();
6522 }
6523
6524 $this->maybe_sync_install_user();
6525 }
6526 }
6527 }
6528
6529 /**
6530 * Check if sync was executed in the last $period of seconds.
6531 *
6532 * @author Vova Feldman (@svovaf)
6533 * @since 1.1.7.3
6534 *
6535 * @param int $period In seconds
6536 *
6537 * @return bool
6538 */
6539 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6540 return $this->is_cron_executed( 'sync', $period );
6541 }
6542
6543 /**
6544 * @author Vova Feldman (@svovaf)
6545 * @since 1.1.7.3
6546 *
6547 * @return bool
6548 */
6549 private function is_sync_cron_on() {
6550 return $this->is_cron_on( 'sync' );
6551 }
6552
6553 /**
6554 * @author Leo Fajardo (@leorw)
6555 * @since 2.5.0
6556 */
6557 private function maybe_schedule_sync_cron() {
6558 $next_schedule = $this->next_sync_cron();
6559
6560 // The event is properly scheduled, so no need to reschedule it.
6561 if (
6562 is_numeric( $next_schedule ) &&
6563 $next_schedule > time()
6564 ) {
6565 return;
6566 }
6567
6568 $this->schedule_sync_cron();
6569 }
6570
6571 /**
6572 * Instead of running blocking install sync event, execute non blocking scheduled cron job.
6573 *
6574 * @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.
6575 *
6576 * @author Leo Fajardo (@leorw)
6577 * @since 2.9.1
6578 */
6579 private function maybe_schedule_install_sync_cron( $except_blog_id = 0 ) {
6580 if ( ! $this->is_user_in_admin() ) {
6581 return;
6582 }
6583
6584 if ( $this->is_clone() ) {
6585 return;
6586 }
6587
6588 if (
6589 // The event has been properly scheduled, so no need to reschedule it.
6590 is_numeric( $this->next_install_sync() )
6591 ) {
6592 return;
6593 }
6594
6595 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
6596 }
6597
6598 /**
6599 * @author Vova Feldman (@svovaf)
6600 * @since 1.1.7.3
6601 *
6602 * @param int $start_at Defaults to now.
6603 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6604 * @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.
6605 */
6606 private function schedule_sync_cron(
6607 $start_at = WP_FS__SCRIPT_START_TIME,
6608 $randomize_start = true,
6609 $except_blog_id = 0
6610 ) {
6611 $this->schedule_cron(
6612 'sync',
6613 'data_sync',
6614 'daily',
6615 $start_at,
6616 $randomize_start,
6617 $except_blog_id
6618 );
6619 }
6620
6621 /**
6622 * Add the actual sync function to the cron job hook.
6623 *
6624 * @author Vova Feldman (@svovaf)
6625 * @since 1.1.7.3
6626 */
6627 private function hook_callback_to_sync_cron() {
6628 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6629 }
6630
6631 /**
6632 * @author Vova Feldman (@svovaf)
6633 * @since 1.1.7.3
6634 *
6635 * @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.
6636 */
6637 private function clear_sync_cron( $is_network_clear = false ) {
6638 $this->_logger->entrance();
6639
6640 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6641 }
6642
6643 /**
6644 * Unix timestamp for next sync cron execution or false if not scheduled.
6645 *
6646 * @author Vova Feldman (@svovaf)
6647 * @since 1.1.7.3
6648 *
6649 * @return int|false
6650 */
6651 function next_sync_cron() {
6652 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6653 }
6654
6655 /**
6656 * Unix timestamp for previous sync cron execution or false if never executed.
6657 *
6658 * @author Vova Feldman (@svovaf)
6659 * @since 1.1.7.3
6660 *
6661 * @return int|false
6662 */
6663 function last_sync_cron() {
6664 return $this->cron_last_execution( 'sync' );
6665 }
6666
6667 #endregion Daily Sync Cron ------------------------------------------------------------------
6668
6669 #----------------------------------------------------------------------------------
6670 #region Async Install Sync
6671 #----------------------------------------------------------------------------------
6672
6673 /**
6674 * @author Vova Feldman (@svovaf)
6675 * @since 1.1.7.3
6676 *
6677 * @return bool
6678 */
6679 private function is_install_sync_scheduled() {
6680 return $this->is_cron_on( 'install_sync' );
6681 }
6682
6683 /**
6684 * Get the sync cron's executing blog ID.
6685 *
6686 * @author Vova Feldman (@svovaf)
6687 * @since 2.0.0
6688 *
6689 * @return int
6690 */
6691 private function get_install_sync_cron_blog_id() {
6692 return $this->get_cron_blog_id( 'install_sync' );
6693 }
6694
6695 /**
6696 * Unix timestamp for previous install sync cron execution or false if never executed.
6697 *
6698 * @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.
6699 *
6700 * @author Vova Feldman (@svovaf)
6701 * @since 1.1.7.3
6702 *
6703 * @return int|false
6704 */
6705 function last_install_sync() {
6706 return $this->cron_last_execution( 'install_sync' );
6707 }
6708
6709 /**
6710 * Unix timestamp for next install sync cron execution or false if not scheduled.
6711 *
6712 * @author Vova Feldman (@svovaf)
6713 * @since 1.1.7.3
6714 *
6715 * @return int|false
6716 */
6717 function next_install_sync() {
6718 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
6719 }
6720
6721 /**
6722 * Add the actual install sync function to the cron job hook.
6723 *
6724 * @author Vova Feldman (@svovaf)
6725 * @since 1.1.7.3
6726 */
6727 private function hook_callback_to_install_sync() {
6728 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
6729 }
6730
6731 /**
6732 * @author Vova Feldman (@svovaf)
6733 * @since 1.1.7.3
6734 *
6735 * @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.
6736 */
6737 private function clear_install_sync_cron( $is_network_clear = false ) {
6738 $this->_logger->entrance();
6739
6740 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
6741 }
6742
6743 /**
6744 * @author Vova Feldman (@svovaf)
6745 * @since 1.1.7.3
6746 * @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.
6747 */
6748 public function _run_sync_install() {
6749 $this->_logger->entrance();
6750
6751 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
6752 }
6753
6754 /**
6755 * The actual install(s) sync cron logic.
6756 *
6757 * @author Vova Feldman (@svovaf)
6758 * @since 2.0.0
6759 *
6760 * @param int[] $blog_ids
6761 * @param int|null $current_blog_id
6762 */
6763 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
6764 if ( $this->is_registered() ) {
6765 if ( 1 < count( $blog_ids ) ) {
6766 $this->sync_installs( array(), true );
6767 } else {
6768 $this->sync_install( array(), true );
6769 }
6770
6771 $this->maybe_sync_install_user();
6772 }
6773 }
6774
6775 #endregion Async Install Sync ------------------------------------------------------------------
6776
6777 /**
6778 * Show a notice that activation is currently pending.
6779 *
6780 * @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.
6781 *
6782 * @author Vova Feldman (@svovaf)
6783 * @since 1.0.7
6784 *
6785 * @param bool|string $email_address
6786 * @param bool $is_pending_trial Since 1.2.1.5
6787 * @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.
6788 * @param bool $has_upgrade_context Since 2.5.3
6789 * @param bool $support_email_address Since 2.5.3
6790 */
6791 function _add_pending_activation_notice(
6792 $email_address = false,
6793 $is_pending_trial = false,
6794 $is_suspicious_email = false,
6795 $has_upgrade_context = false,
6796 $support_email_address = false
6797 ) {
6798 if ( ! is_string( $email_address ) ) {
6799 $current_user = self::_get_current_wp_user();
6800 $email_address = $current_user->user_email;
6801 }
6802
6803 $formatted_message_args = array(
6804 "<b>{$this->get_plugin_name()}</b>",
6805 "<b>{$email_address}</b>",
6806 );
6807
6808 if ( ! $has_upgrade_context || ! fs_is_network_admin() ) {
6809 /* translators: %3$s: action (e.g.: "start the trial" or "complete the opt-in") */
6810 $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' );
6811
6812 $formatted_message_args[] = $is_pending_trial ?
6813 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
6814 $this->get_text_inline( 'complete the opt-in', 'complete-the-opt-in' );
6815
6816 $notice_title = $this->get_text_inline( 'Thanks!', 'thanks' );
6817 } else {
6818 /* translators: %3$s: What the user is expected to receive via email (e.g.: "the installation instructions" or "a license key") */
6819 $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.' );
6820
6821 if ( $this->has_release_on_freemius() ) {
6822 $formatted_message_args[] = $this->get_text_x_inline(
6823 'the installation instructions',
6824 'Part of the message telling the user what they should receive via email.',
6825 'the-installation-instructions-phrase'
6826 );
6827 } else {
6828 $formatted_message_args[] = $this->get_text_x_inline(
6829 'a license key',
6830 'Part of the message telling the user what they should receive via email.',
6831 'a-license-key-phrase'
6832 );
6833
6834 $formatted_message .= ( ' ' . sprintf(
6835 /* translators: %s: activation link (e.g.: <a>Click here</a>) */
6836 $this->get_text_inline( '%s to activate the license once you get it.', 'license-activation-link-message' ),
6837 sprintf(
6838 '<b><a href="%s">%s</a></b>',
6839 $this->get_activation_url( array(
6840 'fs_action' => 'reset_pending_activation_mode',
6841 'require_license' => 'true',
6842 'fs_unique_affix' => $this->get_unique_affix(),
6843 ) ),
6844 $this->get_text_x_inline( 'Click here', 'Part of an activation link message.', 'click-here' )
6845 )
6846 ) );
6847 }
6848
6849 $formatted_message_args[] = ( ! empty( $support_email_address ) ) ?
6850 ( "<b>{$support_email_address}</b>" ) :
6851 $this->get_text_x_inline(
6852 "the product's support email address",
6853 'Part of the message that tells the user to check their spam folder for a specific email.',
6854 'product-support-email-address-phrase'
6855 );
6856
6857 $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' ) );
6858
6859 $notice_title = $this->get_text_inline( 'Thanks for upgrading.', 'after-upgrade-thank-you-message' );
6860 }
6861
6862 $this->_admin_notices->add_sticky(
6863 vsprintf( $formatted_message, $formatted_message_args ),
6864 'activation_pending',
6865 $notice_title
6866 );
6867 }
6868
6869 /**
6870 * Check if currently in plugin activation.
6871 *
6872 * @author Vova Feldman (@svovaf)
6873 * @since 1.1.4
6874 *
6875 * @return bool
6876 */
6877 function is_plugin_activation() {
6878 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
6879
6880 return !empty($result);
6881 }
6882
6883 /**
6884 *
6885 * NOTE: admin_menu action executed before admin_init.
6886 *
6887 * @author Vova Feldman (@svovaf)
6888 * @since 1.0.7
6889 */
6890 function _admin_init_action() {
6891 $is_migration = $this->is_migration();
6892
6893 /**
6894 * Automatically redirect to connect/activation page after plugin activation.
6895 *
6896 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
6897 */
6898 if ( $this->is_plugin_activation() ) {
6899 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
6900
6901 if ( isset( $_GET['activate-multi'] ) ) {
6902 /**
6903 * Don't redirect if activating multiple plugins at once (bulk activation).
6904 */
6905 } else if (
6906 self::is_deactivation_snoozed() &&
6907 (
6908 // Either running the free code base.
6909 ! $this->is_premium() ||
6910 // Or if has a free version.
6911 ! $this->is_only_premium() ||
6912 // If premium only, don't redirect if license is activated.
6913 ( $this->is_registered() && ! $this->can_use_premium_code() )
6914 )
6915 ) {
6916 /**
6917 * 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.
6918 */
6919 } else if ( ! $is_migration ) {
6920 $this->_redirect_on_activation_hook();
6921 return;
6922 }
6923 }
6924
6925 if ( $is_migration ) {
6926 return;
6927 }
6928
6929 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
6930 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
6931
6932 $this->skip_connection( fs_is_network_admin() );
6933
6934 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
6935 }
6936
6937 if ( $this->is_network_activation_mode() &&
6938 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
6939 ) {
6940 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
6941
6942 $this->delegate_connection();
6943
6944 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
6945 }
6946
6947 $this->_add_upgrade_action_link();
6948
6949 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
6950 (
6951 ( true === $this->_storage->require_license_activation ) ||
6952 // Not registered nor anonymous.
6953 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
6954 // OR, network level and in network upgrade mode.
6955 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
6956 )
6957 ) {
6958 if ( ! $this->is_pending_activation() ) {
6959 if ( ! $this->is_activation_page() ) {
6960 /**
6961 * If a user visits any other admin page before activating the premium-only theme with a valid
6962 * license, reactivate the previous theme.
6963 *
6964 * @author Leo Fajardo (@leorw)
6965 * @since 1.2.2
6966 */
6967 if ( $this->is_theme() &&
6968 ! $this->has_settings_menu() &&
6969 ! isset( $_REQUEST['fs_action'] ) &&
6970 $this->can_activate_previous_theme()
6971 ) {
6972 if ( $this->is_only_premium() ) {
6973 $this->activate_previous_theme();
6974 return;
6975 }
6976
6977 if ( true === $this->_storage->require_license_activation ) {
6978 $this->_storage->require_license_activation = false;
6979 }
6980 }
6981
6982 if ( ! fs_is_network_admin() &&
6983 $this->is_network_activation_mode() &&
6984 ! $this->is_delegated_connection()
6985 ) {
6986 return;
6987 }
6988
6989 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
6990 if ( ! $this->_anonymous_mode &&
6991 ( ! $this->is_addon() || ! $this->_parent->is_anonymous() ) ) {
6992 // Show notice for new plugin installations.
6993 $this->_admin_notices->add(
6994 sprintf(
6995 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
6996 sprintf( '<b><a href="%s">%s</a></b>',
6997 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
6998 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
6999 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
7000 )
7001 ),
7002 '',
7003 'update-nag'
7004 );
7005 }
7006 } else {
7007 if ( $this->should_add_sticky_optin_notice() ) {
7008 $this->add_sticky_optin_admin_notice();
7009 }
7010
7011 if ( $this->has_filter( 'optin_pointer_element' ) ) {
7012 // Don't show admin nag if plugin update.
7013 wp_enqueue_script( 'wp-pointer' );
7014 wp_enqueue_style( 'wp-pointer' );
7015
7016 $this->_enqueue_connect_essentials();
7017
7018 add_action( 'admin_print_footer_scripts', array(
7019 $this,
7020 '_add_connect_pointer_script'
7021 ) );
7022 }
7023 }
7024 }
7025 }
7026
7027 if ( $this->show_opt_in_on_themes_page() &&
7028 $this->is_activation_page()
7029 ) {
7030 $this->_show_theme_activation_optin_dialog();
7031 }
7032 }
7033 }
7034
7035 /**
7036 * @author Vova Feldman (@svovaf)
7037 * @since 2.0.0
7038 *
7039 * @return bool
7040 */
7041 private function should_add_sticky_optin_notice() {
7042 if ( $this->is_addon() && $this->_parent->is_anonymous() ) {
7043 return false;
7044 }
7045
7046 if ( fs_is_network_admin() ) {
7047 if ( ! $this->_is_network_active ) {
7048 return false;
7049 }
7050
7051 if ( ! $this->is_network_activation_mode() ) {
7052 return false;
7053 }
7054
7055 return ! isset( $this->_storage->sticky_optin_added_ms );
7056 }
7057
7058 if ( ! $this->is_activation_mode() ) {
7059 return false;
7060 }
7061
7062 // If running from a blog admin and delegated the connection.
7063 return ! isset( $this->_storage->sticky_optin_added );
7064 }
7065
7066 /**
7067 * @author Leo Fajardo (@leorw)
7068 * @since 2.0.0
7069 */
7070 private function add_sticky_optin_admin_notice() {
7071 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7072 $this->_storage->sticky_optin_added = true;
7073 } else {
7074 $this->_storage->sticky_optin_added_ms = true;
7075 }
7076
7077 // Show notice for new plugin installations.
7078 $this->_admin_notices->add_sticky(
7079 sprintf(
7080 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7081 $this->_module_type,
7082 sprintf( '<b><a href="%s">%s</a></b>',
7083 $this->get_activation_url(),
7084 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7085 )
7086 ),
7087 'connect_account',
7088 '',
7089 'update-nag'
7090 );
7091 }
7092
7093 /**
7094 * Enqueue connect requires scripts and styles.
7095 *
7096 * @author Vova Feldman (@svovaf)
7097 * @since 1.1.4
7098 */
7099 function _enqueue_connect_essentials() {
7100 wp_enqueue_script( 'jquery' );
7101 wp_enqueue_script( 'json2' );
7102
7103 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.js' );
7104 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7105 }
7106
7107 /**
7108 * Add connect / opt-in pointer.
7109 *
7110 * @author Vova Feldman (@svovaf)
7111 * @since 1.1.4
7112 */
7113 function _add_connect_pointer_script() {
7114 $vars = array( 'id' => $this->_module_id );
7115 $pointer_content = fs_get_template( 'connect.php', $vars );
7116 ?>
7117 <script type="text/javascript">// <![CDATA[
7118 jQuery(document).ready(function ($) {
7119 if ('undefined' !== typeof(jQuery().pointer)) {
7120
7121 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7122
7123 if (element.length > 0) {
7124 var optin = $(element).pointer($.extend(true, {}, {
7125 content : <?php echo json_encode( $pointer_content ) ?>,
7126 position : {
7127 edge : 'left',
7128 align: 'center'
7129 },
7130 buttons : function () {
7131 // Don't show pointer buttons.
7132 return '';
7133 },
7134 pointerWidth: 482
7135 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7136
7137 <?php
7138 echo $this->apply_filters( 'optin_pointer_execute', "
7139
7140 optin.pointer('open');
7141
7142 // Tag the opt-in pointer with custom class.
7143 $('.wp-pointer #fs_connect')
7144 .parents('.wp-pointer.wp-pointer-top')
7145 .addClass('fs-opt-in-pointer');
7146
7147 ", 'element', 'optin' ) ?>
7148 }
7149 }
7150 });
7151 // ]]></script>
7152 <?php
7153 }
7154
7155 /**
7156 * Return current page's URL.
7157 *
7158 * @author Vova Feldman (@svovaf)
7159 * @since 1.0.7
7160 *
7161 * @return string
7162 */
7163 static function current_page_url() {
7164 $url = 'http';
7165
7166 if ( isset( $_SERVER["HTTPS"] ) ) {
7167 if ( $_SERVER["HTTPS"] == "on" ) {
7168 $url .= "s";
7169 }
7170 }
7171 $url .= "://";
7172 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7173 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7174 } else {
7175 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7176 }
7177
7178 return esc_url( $url );
7179 }
7180
7181 /**
7182 * Check if the current page is the plugin's main admin settings page.
7183 *
7184 * @author Vova Feldman (@svovaf)
7185 * @since 1.0.7
7186 *
7187 * @return bool
7188 */
7189 function _is_plugin_page() {
7190 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7191 fs_is_plugin_page( $this->_slug );
7192 }
7193
7194 /* Events
7195 ------------------------------------------------------------------------------------------------------------------*/
7196 /**
7197 * Delete site install from Database.
7198 *
7199 * @author Vova Feldman (@svovaf)
7200 * @since 1.0.1
7201 *
7202 * @param bool $store
7203 * @param int|null $blog_id Since 2.0.0
7204 *
7205 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7206 */
7207 function _delete_site( $store = true, $blog_id = null ) {
7208 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7209 }
7210
7211 /**
7212 * Delete site install from Database.
7213 *
7214 * @author Vova Feldman (@svovaf)
7215 * @since 1.2.2.7
7216 *
7217 * @param string $slug
7218 * @param string $module_type
7219 * @param bool $store
7220 * @param int|null $blog_id Since 2.0.0
7221 *
7222 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7223 */
7224 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7225 $sites = self::get_all_sites( $module_type, $blog_id );
7226
7227 $install_id = false;
7228
7229 if ( isset( $sites[ $slug ] ) ) {
7230 if ( is_object( $sites[ $slug ] ) ) {
7231 $install_id = $sites[ $slug ]->id;
7232 }
7233
7234 unset( $sites[ $slug ] );
7235
7236 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7237 }
7238
7239 return $install_id;
7240 }
7241
7242 /**
7243 * Delete plugin's plans information.
7244 *
7245 * @param bool $store Flush to Database if true.
7246 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7247 *
7248 * @author Vova Feldman (@svovaf)
7249 * @since 1.0.9
7250 */
7251 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7252 $this->_logger->entrance();
7253
7254 $plans = self::get_all_plans( $this->_module_type );
7255
7256 $plans_to_keep = array();
7257
7258 if ( $keep_associated_plans ) {
7259 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7260 foreach ( $plans_ids_to_keep as $plan_id ) {
7261 $plan = self::_get_plan_by_id( $plan_id );
7262 if ( is_object( $plan ) ) {
7263 $plans_to_keep[] = self::_encrypt_entity( $plan );
7264 }
7265 }
7266 }
7267
7268 if ( ! empty( $plans_to_keep ) ) {
7269 $plans[ $this->_slug ] = $plans_to_keep;
7270 } else {
7271 unset( $plans[ $this->_slug ] );
7272 }
7273
7274 $this->set_account_option( 'plans', $plans, $store );
7275 }
7276
7277 /**
7278 * Delete all plugin licenses.
7279 *
7280 * @author Vova Feldman (@svovaf)
7281 * @since 1.0.9
7282 *
7283 * @param bool $store
7284 */
7285 private function _delete_licenses( $store = true ) {
7286 $this->_logger->entrance();
7287
7288 $all_licenses = self::get_all_licenses();
7289
7290 unset( $all_licenses[ $this->_module_id ] );
7291
7292 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7293 }
7294
7295 /**
7296 * Check if Freemius was added on new plugin installation.
7297 *
7298 * @author Vova Feldman (@svovaf)
7299 * @since 1.1.5
7300 *
7301 * @return bool
7302 */
7303 function is_plugin_new_install() {
7304 return isset( $this->_storage->is_plugin_new_install ) &&
7305 $this->_storage->is_plugin_new_install;
7306 }
7307
7308 /**
7309 * Check if it's the first plugin release that is running Freemius.
7310 *
7311 * @author Vova Feldman (@svovaf)
7312 * @since 1.2.1.5
7313 *
7314 * @return bool
7315 */
7316 function is_first_freemius_powered_version() {
7317 return empty( $this->_storage->plugin_last_version );
7318 }
7319
7320 /**
7321 * @author Leo Fajardo (@leorw)
7322 * @since 1.2.2
7323 *
7324 * @return bool|string
7325 */
7326 private function get_previous_theme_slug() {
7327 return isset( $this->_storage->previous_theme ) ?
7328 $this->_storage->previous_theme :
7329 false;
7330 }
7331
7332 /**
7333 * @author Leo Fajardo (@leorw)
7334 * @since 1.2.2
7335 *
7336 * @return bool
7337 */
7338 private function can_activate_previous_theme() {
7339 return $this->can_activate_theme( $this->get_previous_theme_slug() );
7340 }
7341
7342 /**
7343 * @author Leo Fajardo (@leorw)
7344 * @since 2.5.0
7345 *
7346 * @return bool
7347 */
7348 private function can_activate_theme( $slug ) {
7349 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7350 $theme_instance = wp_get_theme( $slug );
7351
7352 return $theme_instance->exists();
7353 }
7354
7355 return false;
7356 }
7357
7358 /**
7359 * @author Leo Fajardo (@leorw)
7360 * @since 1.2.2
7361 */
7362 private function activate_previous_theme() {
7363 switch_theme( $this->get_previous_theme_slug() );
7364 unset( $this->_storage->previous_theme );
7365
7366 global $pagenow;
7367 if ( 'themes.php' === $pagenow ) {
7368 /**
7369 * Refresh the active theme information.
7370 *
7371 * @author Leo Fajardo (@leorw)
7372 * @since 1.2.2
7373 */
7374 fs_redirect( $this->admin_url( $pagenow ) );
7375 }
7376 }
7377
7378 /**
7379 * @author Leo Fajardo (@leorw)
7380 * @since 1.2.2
7381 *
7382 * @return string
7383 */
7384 function get_previous_theme_activation_url() {
7385 if ( ! $this->can_activate_previous_theme() ) {
7386 return '';
7387 }
7388
7389 /**
7390 * Activation URL
7391 *
7392 * @author Leo Fajardo (@leorw)
7393 * @since 1.2.2
7394 */
7395 return wp_nonce_url(
7396 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7397 'switch-theme_' . $this->get_previous_theme_slug()
7398 );
7399 }
7400
7401 /**
7402 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7403 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7404 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7405 * theme doesn't exist, then there will be no close button.
7406 *
7407 * @author Leo Fajardo (@leorw)
7408 * @since 1.2.2
7409 *
7410 * @param string $slug_or_name Old theme's slug or name.
7411 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7412 */
7413 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7414 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7415 $old_theme->get_stylesheet() :
7416 $slug_or_name;
7417
7418 $this->_activate_plugin_event_hook();
7419 }
7420
7421 /**
7422 * Plugin activated hook.
7423 *
7424 * @author Vova Feldman (@svovaf)
7425 * @since 1.0.1
7426 *
7427 * @uses FS_Api
7428 */
7429 function _activate_plugin_event_hook() {
7430 $this->_logger->entrance( 'slug = ' . $this->_slug );
7431
7432 if ( ! $this->is_user_admin() ) {
7433 return;
7434 }
7435
7436 $this->unregister_uninstall_hook();
7437
7438 // Clear API cache on activation.
7439 FS_Api::clear_cache();
7440
7441 $is_premium_version_activation = $this->is_plugin() ?
7442 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7443 $this->is_premium();
7444
7445 if ( $is_premium_version_activation && $this->is_pending_activation() ) {
7446 $this->clear_pending_activation_mode();
7447 }
7448
7449 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7450
7451 if ( $this->is_plugin() ) {
7452 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7453 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7454 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7455 $other_version_basename = $is_premium_version_activation ?
7456 $this->_free_plugin_basename :
7457 $this->premium_plugin_basename();
7458
7459 if ( ! $this->_is_network_active ) {
7460 /**
7461 * Themes are always network activated, but the ACTUAL activation is per site.
7462 *
7463 * During the activation, the plugin isn't yet active, therefore,
7464 * _is_network_active will be set to false even if it's a network level
7465 * activation. So we need to fix that by looking at the is_network_admin() value.
7466 *
7467 * @author Vova Feldman
7468 */
7469 $this->_is_network_active = (
7470 $this->_is_multisite_integrated &&
7471 fs_is_network_admin()
7472 );
7473 }
7474
7475 /**
7476 * If the other module version is active, deactivate it.
7477 *
7478 * is_plugin_active() checks if the plugin is active on the site or the network level and
7479 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7480 *
7481 * @author Leo Fajardo (@leorw)
7482 * @since 1.2.2
7483 */
7484 if (
7485 is_plugin_active( $other_version_basename ) &&
7486 $this->apply_filters( 'deactivate_on_activation', ! $this->is_parallel_activation() )
7487 ) {
7488 deactivate_plugins( $other_version_basename );
7489 }
7490 }
7491
7492 if ( $this->is_registered() ) {
7493 if ( $is_premium_version_activation ) {
7494 $this->reconnect_locally();
7495 }
7496
7497
7498 // Schedule re-activation event and sync.
7499 // $this->sync_install( array(), true );
7500 $this->maybe_schedule_install_sync_cron();
7501
7502 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7503 if ( $is_premium_version_activation ) {
7504 $this->_admin_notices->add(
7505 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7506 $this->get_text_x_inline( 'W00t',
7507 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7508 );
7509 }
7510 } else if ( $this->is_anonymous() ) {
7511 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7512 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7513 $network = true;
7514 } else {
7515 $plugin_version = isset( $this->_storage->is_anonymous ) ?
7516 $this->_storage->is_anonymous['version'] :
7517 null;
7518 $network = false;
7519 }
7520
7521 /**
7522 * Reset "skipped" click cache on the following:
7523 * 1. Freemius DEV mode.
7524 * 2. WordPress DEBUG mode.
7525 * 3. If a plugin and the user skipped the exact same version before.
7526 *
7527 * @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).
7528 *
7529 * @todo 4. If explicitly asked to retry after every activation.
7530 */
7531 if ( WP_FS__DEV_MODE ||
7532 (
7533 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7534 $this->get_plugin_version() == $plugin_version
7535 )
7536 ) {
7537 $this->reset_anonymous_mode( $network );
7538 }
7539 }
7540
7541 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7542
7543 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7544 /**
7545 * When activating an add-on, try to also activate a license.
7546 *
7547 * @author Leo Fajardo (@leorw)
7548 * @since 2.3.0
7549 */
7550 if ( ! $this->_is_network_active ) {
7551 $this->maybe_activate_addon_license();
7552 } else {
7553 $this->maybe_network_activate_addon_license();
7554 }
7555
7556 /**
7557 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7558 *
7559 * @author Leo Fajardo (@leorw)
7560 * @since 2.3.0
7561 */
7562 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7563
7564 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7565 $this->_storage->require_license_activation = false;
7566 }
7567 }
7568
7569 if (
7570 $is_premium_version_activation &&
7571 (
7572 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7573 (
7574 $this->is_registered() &&
7575 ! $is_trial_or_has_features_enabled_license
7576 )
7577 )
7578 ) {
7579 $this->_storage->require_license_activation = true;
7580 }
7581
7582 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7583 /**
7584 * If no previous version of plugin's version exist, it means that it's either
7585 * the first time that the plugin installed on the site, or the plugin was installed
7586 * before but didn't have Freemius integrated.
7587 *
7588 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7589 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7590 * only after immediate activation.
7591 *
7592 * @since 1.1.4
7593 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7594 */
7595 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7596 }
7597
7598 /**
7599 * Also flush when activating the premium version so that even if Freemius was off before, the API
7600 * connectivity test can be run again.
7601 *
7602 * @author Leo Fajardo (@leorw)
7603 * @since 2.2.3.1
7604 */
7605 $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation );
7606
7607 if ( ! $this->_anonymous_mode &&
7608 ( false !== $has_api_connectivity ) &&
7609 ! $this->_isAutoInstall
7610 ) {
7611 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7612 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7613 }
7614
7615 /**
7616 * Activation hook is executed after the plugin's main file is loaded, therefore,
7617 * after the plugin was loaded. The logic is located at activate_plugin()
7618 * ./wp-admin/includes/plugin.php.
7619 *
7620 * @author Vova Feldman (@svovaf)
7621 * @since 1.1.9
7622 */
7623 $this->_storage->was_plugin_loaded = true;
7624 }
7625
7626 /**
7627 * @author Leo Fajardo (@leorw)
7628 * @since 2.3.0
7629 */
7630 private function maybe_activate_addon_license() {
7631 $parent_fs = $this->get_parent_instance();
7632
7633 if (
7634 ! is_object( $parent_fs ) ||
7635 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7636 ) {
7637 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7638 return;
7639 }
7640
7641 $license = $this->get_active_parent_license();
7642 if ( ! is_object( $license ) ) {
7643 return;
7644 }
7645
7646 if (
7647 $this->is_bundle_license_auto_activation_enabled() &&
7648 ! empty( $license->products )
7649 ) {
7650 $this->activate_bundle_license( $license );
7651
7652 return;
7653 }
7654
7655 if ( ! $this->is_registered() ) {
7656 // Opt in with a license key.
7657 $this->opt_in(
7658 $parent_fs->get_current_or_network_user()->email,
7659 false,
7660 false,
7661 $license->secret_key,
7662 false,
7663 false,
7664 false,
7665 null,
7666 array(),
7667 true,
7668 $license->user_id
7669 );
7670 } else {
7671 // Activate the license.
7672 $install = $this->api_site_call(
7673 '/',
7674 'put',
7675 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7676 );
7677
7678 if ( ! FS_Api::is_api_error( $install ) ) {
7679 $this->_sync_addon_license( $this->get_id(), true );
7680 }
7681 }
7682 }
7683
7684 /**
7685 * @author Leo Fajardo (@leorw)
7686 * @since 2.3.0
7687 *
7688 * @param FS_Plugin_License $license
7689 */
7690 private function maybe_network_activate_addon_license( $license = null ) {
7691 $parent_fs = $this->get_parent_instance();
7692 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7693 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7694 return;
7695 }
7696
7697 $license = ( ! is_null( $license ) ) ?
7698 $license :
7699 $this->get_active_parent_license();
7700
7701 if ( ! is_object( $license ) ) {
7702 return;
7703 }
7704
7705 if (
7706 $this->is_bundle_license_auto_activation_enabled() &&
7707 ! empty( $license->products )
7708 ) {
7709 $this->activate_bundle_license( $license );
7710
7711 return;
7712 }
7713
7714 if ( ! $this->is_network_registered() ) {
7715 $sites = $this->get_sites_for_network_level_optin();
7716
7717 if ( count( $sites ) > $license->left() ) {
7718 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
7719 return;
7720 }
7721
7722 // Opt in with a license key.
7723 $this->opt_in(
7724 $parent_fs->get_user()->email,
7725 false,
7726 false,
7727 $license->secret_key,
7728 false,
7729 false,
7730 false,
7731 null,
7732 $sites,
7733 true,
7734 $license->user_id
7735 );
7736 } else {
7737 $blog_2_install_map = array();
7738 $site_ids = array();
7739
7740 $all_sites = Freemius::get_sites();
7741
7742 foreach ( $all_sites as $site ) {
7743 $blog_id = Freemius::get_site_blog_id( $site );
7744 $install = $this->get_install_by_blog_id( $blog_id );
7745
7746 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
7747 // Skip license activation for installs that are already associated with a license.
7748 continue;
7749 }
7750
7751 if ( is_object( $install ) ) {
7752 $blog_2_install_map[ $blog_id ] = $install;
7753 } else {
7754 $site_ids[] = $blog_id;
7755 }
7756 }
7757
7758 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
7759 return;
7760 }
7761
7762 $user = $this->get_current_or_network_user();
7763
7764 if ( ! empty( $blog_2_install_map ) ) {
7765 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
7766
7767 if ( true !== $result ) {
7768 return;
7769 }
7770 }
7771
7772 if ( ! empty( $site_ids ) ) {
7773 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
7774 }
7775 }
7776 }
7777
7778 /**
7779 * 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.
7780 *
7781 * @author Leo Fajardo (@leorw)
7782 * @since 2.4.0
7783 *
7784 * @param FS_Plugin_License $license
7785 * @param array $sites
7786 * @param int $blog_id
7787 */
7788 private function maybe_activate_bundle_license( $license = null, $sites = array(), $blog_id = 0 ) {
7789 if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
7790 $license = $this->_license;
7791 }
7792
7793 if ( ! is_object( $license ) ) {
7794 return;
7795 }
7796
7797 $parent_license = ( ! empty( $license->products ) ) ?
7798 $license :
7799 $this->get_active_parent_license( $license->secret_key );
7800
7801 if ( is_object( $parent_license ) ) {
7802 $this->activate_bundle_license( $parent_license, $sites, $blog_id );
7803 }
7804 }
7805
7806 /**
7807 * Try to activate a bundle license for all the bundle products installed on the site.
7808 * (1) If a child product install already has a license, the bundle license won't be activated.
7809 * (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.
7810 * (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.
7811 *
7812 * @author Leo Fajardo (@leorw)
7813 * @since 2.4.0
7814 *
7815 * @param FS_Plugin_License $license
7816 * @param array $sites
7817 * @param int $current_blog_id
7818 */
7819 private function activate_bundle_license( $license, $sites = array(), $current_blog_id = 0 ) {
7820 $is_network_admin = fs_is_network_admin();
7821
7822 $installs_by_blog_map = array();
7823 $site_info_by_blog_map = array();
7824
7825 /**
7826 * Try to activate the license for all supported products.
7827 *
7828 * @author Leo Fajardo
7829 */
7830 foreach ( $license->products as $product_id ) {
7831 $fs = self::get_instance_by_id( $product_id );
7832
7833 if ( ! is_object( $fs ) ) {
7834 continue;
7835 }
7836
7837 if ( ! $fs->has_paid_plan() ) {
7838 continue;
7839 }
7840
7841 if (
7842 ! $fs->is_addon() &&
7843 ! FS_Plan_Manager::instance()->has_paid_plan( $fs->_plans )
7844 ) {
7845 /**
7846 * The parent product can be free-only but can have its `has_paid_plan` flag set to `true` when
7847 * there is a context bundle.
7848 */
7849 continue;
7850 }
7851
7852 if ( $current_blog_id > 0 ) {
7853 $fs->switch_to_blog( $current_blog_id );
7854 }
7855
7856 if ( $fs->has_active_valid_license() ) {
7857 continue;
7858 }
7859
7860 if ( ! $is_network_admin || $current_blog_id > 0 ) {
7861 if ( $fs->is_network_active() && ! $fs->is_delegated_connection( $current_blog_id ) ) {
7862 // Do not try to activate the license in the site level if the product is network active and the connection was not delegated.
7863 continue;
7864 }
7865 } else {
7866 if ( ! $fs->is_network_active() ) {
7867 // Do not try to activate the license in the network level if the product is not network active.
7868 continue;
7869 }
7870
7871 if ( $fs->is_network_delegated_connection() ) {
7872 // Do not try to activate the license in the network level if the activation has been delegated to site admins.
7873 continue;
7874 }
7875
7876 $has_install_with_license = false;
7877
7878 // 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.
7879 $filtered_sites = array();
7880
7881 if ( empty( $sites ) ) {
7882 $all_sites = self::get_sites();
7883
7884 foreach ( $all_sites as $site ) {
7885 $sites[] = array( 'blog_id' => self::get_site_blog_id( $site ) );
7886 }
7887 } else {
7888 // Populate the map here to avoid calling `$fs->get_site_info( $site );` in the other `for` loop below.
7889 foreach ( $sites as $site ) {
7890 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
7891 continue;
7892 }
7893
7894 $site_info_by_blog_map[ $site['blog_id'] ] = $site;
7895 }
7896 }
7897
7898 foreach ( $sites as $site ) {
7899 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
7900 continue;
7901 }
7902
7903 $blog_id = $site['blog_id'];
7904
7905 if ( ! isset( $installs_by_blog_map[ $blog_id ] ) ) {
7906 $installs_by_blog_map[ $blog_id ] = self::get_all_sites( $fs->get_module_type(), $blog_id );
7907 }
7908
7909 $installs = $installs_by_blog_map[ $blog_id ];
7910 $install = null;
7911
7912 if ( isset( $installs[ $fs->get_slug() ] ) ) {
7913 $install = $installs[ $fs->get_slug() ];
7914
7915 if (
7916 is_object( $install ) &&
7917 (
7918 ! FS_Site::is_valid_id( $install->id ) ||
7919 ! FS_User::is_valid_id( $install->user_id ) ||
7920 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
7921 )
7922 ) {
7923 $install = null;
7924 }
7925 }
7926
7927 if (
7928 is_object( $install ) &&
7929 FS_Plugin_License::is_valid_id( $install->license_id )
7930 ) {
7931 $has_install_with_license = true;
7932 break;
7933 }
7934
7935 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
7936 // Site activation delegated, don't activate bundle license on the site in the network admin.
7937 continue;
7938 }
7939
7940 if ( ! isset( $site_info_by_blog_map[ $blog_id ] ) ) {
7941 $site_info_by_blog_map[ $blog_id ] = $fs->get_site_info( $site );
7942 }
7943
7944 $filtered_sites[] = $site_info_by_blog_map[ $blog_id ];
7945 }
7946
7947 if ( $has_install_with_license || empty( $filtered_sites ) ) {
7948 // 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.
7949 continue;
7950 }
7951
7952 $sites = $filtered_sites;
7953 }
7954
7955 $fs->activate_migrated_license(
7956 $license->secret_key,
7957 null,
7958 null,
7959 $sites,
7960 ( $current_blog_id > 0 ? $current_blog_id : null ),
7961 $license->user_id
7962 );
7963 }
7964 }
7965
7966 /**
7967 * Returns a parent license that can be activated for the context product.
7968 *
7969 * @author Leo Fajardo (@leorw)
7970 * @since 2.3.0
7971 *
7972 * @param string|null $license_key
7973 * @param bool $flush
7974 *
7975 * @return FS_Plugin_License
7976 */
7977 function get_active_parent_license( $license_key = null, $flush = true ) {
7978 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
7979
7980 $fs = $this;
7981
7982 if ( $this->is_addon() ) {
7983 $parent_instance = $this->get_parent_instance();
7984
7985 if ( is_object( $parent_instance ) && $parent_instance->is_registered() ) {
7986 $fs = $parent_instance;
7987 }
7988 }
7989
7990 $foreign_licenses = $fs->get_foreign_licenses_info(
7991 self::get_all_licenses( $this->get_parent_id() )
7992 );
7993
7994 if ( ! empty ( $foreign_licenses ) ) {
7995 $foreign_licenses = array(
7996 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
7997 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
7998 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
7999 );
8000
8001 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
8002 }
8003
8004 $result = $fs->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, $flush );
8005
8006 if (
8007 ! $this->is_api_result_object( $result, 'licenses' ) ||
8008 ! is_array( $result->licenses ) ||
8009 empty( $result->licenses )
8010 ) {
8011 return null;
8012 }
8013
8014 $parent_license = null;
8015
8016 if ( empty( $license_key ) ) {
8017 $parent_license = $result->licenses[0];
8018 } else {
8019 foreach ( $result->licenses as $license ) {
8020 if ( $license_key === $license->secret_key ) {
8021 $parent_license = $license;
8022 break;
8023 }
8024 }
8025 }
8026
8027 if ( ! is_null( $parent_license ) ) {
8028 $parent_license = new FS_Plugin_License( $parent_license );
8029 }
8030
8031 return $parent_license;
8032 }
8033
8034 /**
8035 * @author Leo Fajardo (@leorw)
8036 * @since 2.3.0
8037 *
8038 * @return array
8039 */
8040 function get_sites_for_network_level_optin() {
8041 $sites = array();
8042 $all_sites = self::get_sites();
8043
8044 foreach ( $all_sites as $site ) {
8045 $blog_id = self::get_site_blog_id( $site );
8046
8047 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
8048 ! $this->is_installed_on_site( $blog_id )
8049 ) {
8050 $sites[] = $this->get_site_info( $site );
8051 }
8052 }
8053
8054 return $sites;
8055 }
8056
8057 /**
8058 * Delete account.
8059 *
8060 * @author Vova Feldman (@svovaf)
8061 * @since 1.0.3
8062 *
8063 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8064 */
8065 function delete_account_event( $check_user = true ) {
8066 $this->_logger->entrance( 'slug = ' . $this->_slug );
8067
8068 if ( $check_user && ! $this->is_user_admin() ) {
8069 return;
8070 }
8071
8072 $this->do_action( 'before_account_delete' );
8073
8074 // Clear all admin notices.
8075 $this->_admin_notices->clear_all_sticky( false );
8076
8077 $this->_delete_site( false );
8078
8079 $delete_network_common_data = true;
8080
8081 if ( $this->_is_network_active ) {
8082 $installs = $this->get_blog_install_map();
8083
8084 // Don't delete common network data unless no other installs left.
8085 $delete_network_common_data = empty( $installs );
8086 }
8087
8088 if ( $delete_network_common_data ) {
8089 $this->_delete_plans( false );
8090
8091 $this->_delete_licenses( false );
8092
8093 // Delete add-ons related to plugin's account.
8094 $this->_delete_account_addons( false );
8095 }
8096
8097 // @todo Delete plans and licenses of add-ons.
8098
8099 self::$_accounts->store();
8100
8101 /**
8102 * IMPORTANT:
8103 * Clear crons must be executed before clearing all storage.
8104 * Otherwise, the cron will not be cleared.
8105 */
8106 if ( $delete_network_common_data ) {
8107 $this->clear_sync_cron();
8108 }
8109
8110 $this->clear_install_sync_cron();
8111
8112 // Clear all storage data.
8113 $this->_storage->clear_all( true, array(
8114 'is_delegated_connection',
8115 'connectivity_test',
8116 'is_on',
8117 ), false );
8118
8119 // Send delete event.
8120 $this->get_api_site_scope()->call( '/', 'delete' );
8121
8122 $this->do_action( 'after_account_delete' );
8123 }
8124
8125 /**
8126 * Delete network level account.
8127 *
8128 * @author Vova Feldman (@svovaf)
8129 * @since 2.0.0
8130 *
8131 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8132 */
8133 function delete_network_account_event( $check_user = true ) {
8134 $this->_logger->entrance( 'slug = ' . $this->_slug );
8135
8136 if ( $check_user && ! $this->is_user_admin() ) {
8137 return;
8138 }
8139
8140 $this->do_action( 'before_network_account_delete' );
8141
8142 // Clear all admin notices.
8143 $this->_admin_notices->clear_all_sticky();
8144
8145 $this->_delete_plans( false, false );
8146
8147 $this->_delete_licenses( false );
8148
8149 // Delete add-ons related to plugin's account.
8150 $this->_delete_account_addons( false );
8151
8152 // @todo Delete plans and licenses of add-ons.
8153
8154 self::$_accounts->store( true );
8155
8156 /**
8157 * IMPORTANT:
8158 * Clear crons must be executed before clearing all storage.
8159 * Otherwise, the cron will not be cleared.
8160 */
8161 $this->clear_sync_cron( true );
8162 $this->clear_install_sync_cron( true );
8163
8164 $sites = self::get_sites();
8165
8166 $install_ids = array();
8167 foreach ( $sites as $site ) {
8168 $blog_id = self::get_site_blog_id( $site );
8169
8170 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8171 continue;
8172 }
8173
8174 $install_id = $this->_delete_site( true, $blog_id );
8175
8176 // Clear all storage data.
8177 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8178
8179 if ( FS_Site::is_valid_id( $install_id ) ) {
8180 $install_ids[] = $install_id;
8181 }
8182
8183 switch_to_blog( $blog_id );
8184
8185 $this->do_action( 'after_account_delete' );
8186
8187 restore_current_blog();
8188 }
8189
8190 $this->_storage->clear_all( true, array(
8191 'connectivity_test',
8192 'is_on',
8193 ), true );
8194
8195 // Send delete event.
8196 if ( ! empty( $install_ids ) ) {
8197 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8198 }
8199
8200 $this->do_action( 'after_network_account_delete' );
8201 }
8202
8203 /**
8204 * Plugin deactivation hook.
8205 *
8206 * @author Vova Feldman (@svovaf)
8207 * @since 1.0.1
8208 */
8209 function _deactivate_plugin_hook() {
8210 $this->_logger->entrance( 'slug = ' . $this->_slug );
8211
8212 if ( ! $this->is_user_admin() ) {
8213 return;
8214 }
8215
8216 $is_network_deactivation = fs_is_network_admin();
8217 $storage_keys_for_removal = array();
8218
8219 $this->_admin_notices->clear_all_sticky();
8220
8221 $storage_keys_for_removal[] = 'sticky_optin_added';
8222 if ( isset( $this->_storage->sticky_optin_added ) ) {
8223 unset( $this->_storage->sticky_optin_added );
8224 }
8225
8226 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8227 // Remember that plugin was already installed.
8228 $this->_storage->is_plugin_new_install = false;
8229 }
8230
8231 // Hook to plugin uninstall.
8232 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8233
8234 $this->clear_module_main_file_cache();
8235 $this->clear_sync_cron( $this->_is_network_active );
8236 $this->clear_install_sync_cron();
8237
8238 if ( $this->is_registered() ) {
8239 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8240 FS_Plugin_Updater::instance( $this )->delete_update_data();
8241 }
8242
8243 if ( $is_network_deactivation ) {
8244 // Send deactivation event.
8245 $this->sync_installs( array(
8246 'is_active' => false,
8247 ) );
8248 } else {
8249 // Send deactivation event.
8250 $this->sync_install( array(
8251 'is_active' => false,
8252 ) );
8253 }
8254 } else {
8255 if ( false === $this->has_api_connectivity() && ! $this->is_premium() ) {
8256 // Reset connectivity test cache.
8257 $this->clear_connectivity_info();
8258
8259 $storage_keys_for_removal[] = 'connectivity_test';
8260 }
8261 }
8262
8263 if ( $is_network_deactivation ) {
8264 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8265 unset( $this->_storage->sticky_optin_added_ms );
8266 }
8267
8268 if ( ! empty( $storage_keys_for_removal ) ) {
8269 $sites = self::get_sites();
8270
8271 foreach ( $sites as $site ) {
8272 $blog_id = self::get_site_blog_id( $site );
8273
8274 foreach ( $storage_keys_for_removal as $key ) {
8275 $this->_storage->remove( $key, false, $blog_id );
8276 }
8277
8278 $this->_storage->save( $blog_id );
8279 }
8280 }
8281 }
8282
8283 // Clear API cache on deactivation.
8284 FS_Api::clear_cache();
8285
8286 $this->remove_sdk_reference();
8287 }
8288
8289 /**
8290 * @author Vova Feldman (@svovaf)
8291 * @since 1.1.6
8292 */
8293 private function remove_sdk_reference() {
8294 global $fs_active_plugins;
8295
8296 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8297 if ( $this->_plugin_basename == $data->plugin_path ) {
8298 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8299 break;
8300 }
8301 }
8302
8303 fs_fallback_to_newest_active_sdk();
8304 }
8305
8306 /**
8307 * @author Vova Feldman (@svovaf)
8308 * @since 1.1.3
8309 *
8310 * @param bool $is_anonymous
8311 * @param bool|int $network_or_blog_id Since 2.0.0
8312 */
8313 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8314 // Store information regarding skip to try and opt-in the user
8315 // again in the future.
8316 $skip_info = array(
8317 'is' => $is_anonymous,
8318 'timestamp' => WP_FS__SCRIPT_START_TIME,
8319 'version' => $this->get_plugin_version(),
8320 );
8321
8322 if ( true === $network_or_blog_id ) {
8323 $this->_storage->is_anonymous_ms = $skip_info;
8324 } else {
8325 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8326 }
8327
8328 $this->network_upgrade_mode_completed();
8329
8330 // Update anonymous mode cache.
8331 $this->_is_anonymous = $is_anonymous;
8332 }
8333
8334 /**
8335 * @author Vova Feldman (@svovaf)
8336 * @since 2.5.1
8337 *
8338 * @param bool|int $network_or_blog_id
8339 */
8340 private function unset_anonymous_mode( $network_or_blog_id = 0 ) {
8341 if ( true === $network_or_blog_id ) {
8342 unset( $this->_storage->is_anonymous_ms );
8343 } else {
8344 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8345 }
8346 }
8347
8348 /**
8349 * @author Vova Feldman (@svovaf)
8350 * @since 2.0.0
8351 *
8352 * @param int $blog_id Site ID.
8353 * @param int $user_id User ID.
8354 * @param string $domain Site domain.
8355 * @param string $path Site path.
8356 * @param int $network_id Network ID. Only relevant on multi-network installations.
8357 * @param array $meta Metadata. Used to set initial site options.
8358 *
8359 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8360 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8361 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8362 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8363 */
8364 public function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8365 $this->_logger->entrance();
8366
8367 if ( ! $this->_is_network_active ) {
8368 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
8369 return;
8370 }
8371
8372 $site = null;
8373 $new_blog_id = $blog_id;
8374
8375 if ( $this->is_premium() &&
8376 $this->is_network_connected() &&
8377 is_object( $this->_license ) &&
8378 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8379 $this->is_license_network_active( $blog_id )
8380 ) {
8381 /**
8382 * 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.
8383 */
8384 $current_blog_id = get_current_blog_id();
8385 $license = clone $this->_license;
8386
8387 $this->switch_to_blog( $blog_id );
8388
8389 // Opt-in with network user.
8390 $this->install_with_user(
8391 $this->get_network_user(),
8392 $license->secret_key,
8393 false,
8394 false,
8395 false
8396 );
8397
8398 if ( is_object( $this->_site ) ) {
8399 if ( $this->_site->license_id == $license->id ) {
8400 /**
8401 * If the license was activated successfully, sync the license data from the remote server.
8402 */
8403 $this->_license = $license;
8404 $this->sync_site_license();
8405 }
8406 }
8407
8408 $site = $this->_site;
8409
8410 $this->switch_to_blog( $current_blog_id );
8411
8412 if ( is_object( $site ) ) {
8413 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id, $site );
8414
8415 // Already connected (with or without a license), so no need to continue.
8416 return;
8417 }
8418 }
8419
8420 if ( $this->is_network_anonymous() ) {
8421 /**
8422 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8423 */
8424 $this->skip_site_connection( $blog_id );
8425 } else if ( $this->is_network_delegated_connection() ) {
8426 /**
8427 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8428 */
8429 $this->delegate_site_connection( $blog_id );
8430 } else if ( $this->is_network_connected() ) {
8431 /**
8432 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8433 */
8434 $current_blog_id = get_current_blog_id();
8435
8436 $this->switch_to_blog( $blog_id );
8437
8438 // Opt-in with network user.
8439 $this->install_with_user(
8440 $this->get_network_user(),
8441 false,
8442 false,
8443 false,
8444 false
8445 );
8446
8447 $site = $this->_site;
8448
8449 $this->switch_to_blog( $current_blog_id );
8450 } else {
8451 /**
8452 * If the super-admin mixed different options (connect, skip, delegated):
8453 * a) If at least one site connection was delegated, then automatically delegate connection.
8454 * 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.
8455 */
8456 $has_delegated_site = false;
8457
8458 $sites = self::get_sites();
8459 foreach ( $sites as $wp_site ) {
8460 $blog_id = self::get_site_blog_id( $wp_site );
8461
8462 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8463 $has_delegated_site = true;
8464 break;
8465 }
8466 }
8467
8468 if ( $has_delegated_site ) {
8469 $this->delegate_site_connection( $blog_id );
8470 } else {
8471 $this->skip_site_connection( $blog_id );
8472 }
8473 }
8474
8475 /**
8476 * 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.
8477 *
8478 * @author Leo Fajardo (@leorw)
8479 * @since 2.5.0
8480 */
8481 FS_Clone_Manager::instance()->store_blog_install_info( $new_blog_id, $site );
8482 }
8483
8484 /**
8485 * @author Vova Feldman (@svovaf)
8486 * @since 2.5.0
8487 *
8488 * @param \WP_Site $new_site
8489 * @param array $args
8490 */
8491 public function _after_wp_initialize_site_callback( WP_Site $new_site, $args ) {
8492 $this->_logger->entrance();
8493
8494 $this->_after_new_blog_callback(
8495 $new_site->id,
8496 // Dummy user ID (not in use).
8497 0,
8498 $new_site->domain,
8499 $new_site->path,
8500 $new_site->network_id,
8501 // Dummy meta, not in use.
8502 array()
8503 );
8504 }
8505
8506 /**
8507 * @author Vova Feldman (@svovaf)
8508 * @since 1.1.3
8509 *
8510 * @param bool|int|int[] $network_or_blog_ids Since 2.0.0.
8511 */
8512 private function reset_anonymous_mode( $network_or_blog_ids = false ) {
8513 if ( true === $network_or_blog_ids ) {
8514 $this->unset_anonymous_mode( true );
8515
8516 if ( fs_is_network_admin() ) {
8517 $this->_is_anonymous = null;
8518 }
8519
8520 // Rest anonymous mode for all non-delegated sub-sites.
8521 $blog_ids = $this->get_non_delegated_blog_ids();
8522 }
8523 else
8524 {
8525 if ( false === $network_or_blog_ids ) {
8526 $network_or_blog_ids = 0;
8527 }
8528
8529 $blog_ids = is_array( $network_or_blog_ids ) ?
8530 $network_or_blog_ids :
8531 array( $network_or_blog_ids );
8532
8533 foreach ( $blog_ids as $blog_id ) {
8534 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8535 $this->_is_anonymous = null;
8536 }
8537 }
8538 }
8539
8540 foreach ( $blog_ids as $blog_id ) {
8541 $this->unset_anonymous_mode( $blog_id );
8542 }
8543
8544 /**
8545 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8546 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8547 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8548 *
8549 * @author Leo Fajardo (@leorw)
8550 * @since 1.2.2
8551 */
8552 if ( ! $this->_is_network_active ) {
8553 $this->_is_anonymous = null;
8554 }
8555 }
8556
8557 /**
8558 * @author Leo Fajardo (@leorw)
8559 * @since 2.5.3
8560 */
8561 private function update_license_required_permissions_if_anonymous() {
8562 if ( ! $this->is_anonymous() ) {
8563 return;
8564 }
8565
8566 $this->reset_anonymous_mode( fs_is_network_admin() );
8567
8568 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
8569 'essentials' => true,
8570 'events' => true,
8571 'diagnostic' => false,
8572 'extensions' => false,
8573 'site' => false,
8574 ) );
8575 }
8576
8577 /**
8578 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8579 * deleting the account in the network level, the URL of the page to redirect to is correct.
8580 *
8581 * @author Leo Fajardo (@leorw)
8582 *
8583 * @since 2.1.3
8584 */
8585 private function maybe_set_slug_and_network_menu_exists_flag() {
8586 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8587 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8588 $this->_menu->get_slug() :
8589 $this->_slug
8590 );
8591 }
8592 }
8593
8594 /**
8595 * Clears the anonymous mode and redirects to the opt-in screen.
8596 *
8597 * @author Vova Feldman (@svovaf)
8598 * @since 1.1.7
8599 */
8600 function connect_again() {
8601 if ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) {
8602 return;
8603 }
8604
8605 if ( $this->is_anonymous() ) {
8606 $this->reset_anonymous_mode( fs_is_network_admin() );
8607 }
8608
8609 $activation_url_params = array();
8610
8611 if ( $this->is_pending_activation() ) {
8612 $this->clear_pending_activation_mode();
8613
8614 if ( fs_request_get_bool( 'require_license' ) ) {
8615 $activation_url_params['require_license'] = true;
8616 }
8617 }
8618
8619 $this->maybe_set_slug_and_network_menu_exists_flag();
8620
8621 fs_redirect( $this->get_activation_url( $activation_url_params ) );
8622 }
8623
8624 /**
8625 * Skip account connect, and set anonymous mode.
8626 *
8627 * @author Vova Feldman (@svovaf)
8628 * @since 1.1.1
8629 *
8630 * @param bool|int|int[] $network_or_blog_ids Since 2.5.1
8631 */
8632 function skip_connection( $network_or_blog_ids = false ) {
8633 $this->_logger->entrance();
8634
8635 $this->_admin_notices->remove_sticky( 'connect_account' );
8636
8637 if ( true === $network_or_blog_ids ) {
8638 $this->set_anonymous_mode( true, true );
8639
8640 if ( fs_is_network_admin() ) {
8641 $this->_is_anonymous = null;
8642 }
8643
8644 // Rest anonymous mode for all non-delegated sub-sites.
8645 $blog_ids = $this->get_non_delegated_blog_ids();
8646 }
8647 else
8648 {
8649 if ( false === $network_or_blog_ids ) {
8650 $network_or_blog_ids = 0;
8651 }
8652
8653 $blog_ids = is_array( $network_or_blog_ids ) ?
8654 $network_or_blog_ids :
8655 array( $network_or_blog_ids );
8656
8657 foreach ( $blog_ids as $blog_id ) {
8658 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8659 $this->_is_anonymous = null;
8660 }
8661 }
8662 }
8663
8664 foreach ( $blog_ids as $blog_id ) {
8665 $this->skip_site_connection( $blog_id );
8666 }
8667
8668 $this->network_upgrade_mode_completed();
8669 }
8670
8671 /**
8672 * Skip connection for specific site in the network.
8673 *
8674 * @author Vova Feldman (@svovaf)
8675 * @since 2.0.0
8676 *
8677 * @param int|null $blog_id
8678 * @param bool $send_skip
8679 */
8680 private function skip_site_connection( $blog_id = null ) {
8681 $this->_logger->entrance();
8682
8683 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8684
8685 $this->set_anonymous_mode( true, $blog_id );
8686 }
8687
8688 /**
8689 * Plugin version update hook.
8690 *
8691 * @author Vova Feldman (@svovaf)
8692 * @since 1.0.4
8693 */
8694 private function update_plugin_version_event() {
8695 $this->_logger->entrance();
8696
8697 if ( ! $this->is_registered() ) {
8698 return;
8699 }
8700
8701 $this->maybe_schedule_install_sync_cron();
8702 // $this->sync_install( array(), true );
8703 }
8704
8705 /**
8706 * Generate an MD5 signature of a plugins collection.
8707 * This helper methods used to identify changes in a plugins collection.
8708 *
8709 * @author Vova Feldman (@svovaf)
8710 * @since 2.0.0
8711 *
8712 * @param array [string]array $plugins
8713 *
8714 * @return string
8715 */
8716 private function get_plugins_thumbprint( $plugins ) {
8717 ksort( $plugins );
8718
8719 $thumbprint = '';
8720 foreach ( $plugins as $basename => $data ) {
8721 $thumbprint .= $data['slug'] . ',' .
8722 $data['Version'] . ',' .
8723 ( $data['is_active'] ? '1' : '0' ) . ';';
8724 }
8725
8726 return md5( $thumbprint );
8727 }
8728
8729 /**
8730 * Return a list of modified plugins since the last sync.
8731 *
8732 * Note:
8733 * There's no point to store a plugins counter since even if the number of
8734 * plugins didn't change, we still need to check if the versions are all the
8735 * same and the activity state is similar.
8736 *
8737 * @author Vova Feldman (@svovaf)
8738 * @since 1.1.8
8739 *
8740 * @return array|false
8741 */
8742 private function get_plugins_data_for_api() {
8743 // Alias.
8744 $site_active_plugins_option_name = 'active_plugins';
8745 $network_plugins_option_name = 'all_plugins';
8746
8747 /**
8748 * Collection of all site level active plugins.
8749 */
8750 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8751
8752 if ( ! is_object( $site_active_plugins_cache ) ) {
8753 $site_active_plugins_cache = (object) array(
8754 'timestamp' => '',
8755 'md5' => '',
8756 'plugins' => array(),
8757 );
8758 }
8759
8760 $time = time();
8761
8762 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
8763 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8764 ) {
8765 // Don't send plugin updates if last update was in the past 5 min.
8766 return false;
8767 }
8768
8769 // Write timestamp to lock the logic.
8770 $site_active_plugins_cache->timestamp = $time;
8771 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8772
8773 // Reload options from DB.
8774 self::$_accounts->load( true );
8775 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8776
8777 if ( $time != $site_active_plugins_cache->timestamp ) {
8778 // If timestamp is different, then another thread captured the lock.
8779 return false;
8780 }
8781
8782 /**
8783 * Collection of all plugins (network level).
8784 */
8785 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
8786
8787 if ( ! is_object( $network_plugins_cache ) ) {
8788 $network_plugins_cache = (object) array(
8789 'timestamp' => '',
8790 'md5' => '',
8791 'plugins' => array(),
8792 );
8793 }
8794
8795 // Check if there's a change in plugins.
8796 $network_plugins = self::get_network_plugins();
8797 $site_active_plugins = self::get_site_active_plugins();
8798
8799 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
8800 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
8801
8802 // Check if plugins status changed (version or active/inactive).
8803 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
8804 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
8805
8806 if ( ! $network_plugins_changed &&
8807 ! $site_active_plugins_changed
8808 ) {
8809 // No changes.
8810 return array();
8811 }
8812
8813 $plugins_update_data = array();
8814
8815 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
8816 if ( ! isset( $network_plugins[ $basename ] ) ) {
8817 // Plugin uninstalled.
8818 $uninstalled_plugin_data = $data;
8819 $uninstalled_plugin_data['is_active'] = false;
8820 $uninstalled_plugin_data['is_uninstalled'] = true;
8821 $plugins_update_data[] = $uninstalled_plugin_data;
8822
8823 unset( $network_plugins[ $basename ] );
8824
8825 unset( $network_plugins_cache->plugins[ $basename ] );
8826 unset( $site_active_plugins_cache->plugins[ $basename ] );
8827
8828 continue;
8829 }
8830
8831 $was_active = $data['is_active'] ||
8832 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8833 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
8834 $is_active = $network_plugins[ $basename ]['is_active'] ||
8835 ( isset( $site_active_plugins[ $basename ] ) &&
8836 $site_active_plugins[ $basename ]['is_active'] );
8837
8838 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8839 isset( $site_active_plugins[ $basename ] )
8840 ) {
8841 // Plugin was site level activated.
8842 $site_active_plugins_cache->plugins[ $basename ] = array(
8843 'slug' => $network_plugins[ $basename ]['slug'],
8844 'version' => $network_plugins[ $basename ]['Version'],
8845 'title' => $network_plugins[ $basename ]['Name'],
8846 'is_active' => $is_active,
8847 'is_uninstalled' => false,
8848 );
8849 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8850 ! isset( $site_active_plugins[ $basename ] )
8851 ) {
8852 // Plugin was site level deactivated.
8853 unset( $site_active_plugins_cache->plugins[ $basename ] );
8854 }
8855
8856 $prev_version = $data['version'];
8857 $current_version = $network_plugins[ $basename ]['Version'];
8858
8859 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
8860 // Plugin activated or deactivated, or version changed.
8861
8862 if ( $was_active !== $is_active ) {
8863 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
8864 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
8865 }
8866 }
8867
8868 if ( $prev_version !== $current_version ) {
8869 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
8870 }
8871
8872 $updated_plugin_data = $data;
8873 $updated_plugin_data['is_active'] = $is_active;
8874 $updated_plugin_data['version'] = $current_version;
8875 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
8876 $plugins_update_data[] = $updated_plugin_data;
8877 }
8878 }
8879
8880 // Find new plugins that weren't yet seen before.
8881 foreach ( $network_plugins as $basename => $data ) {
8882 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
8883 // New plugin.
8884 $new_plugin = array(
8885 'slug' => $data['slug'],
8886 'version' => $data['Version'],
8887 'title' => $data['Name'],
8888 'is_active' => $data['is_active'],
8889 'is_uninstalled' => false,
8890 );
8891
8892 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
8893
8894 $is_site_level_active = (
8895 isset( $site_active_plugins[ $basename ] ) &&
8896 $site_active_plugins[ $basename ]['is_active']
8897 );
8898
8899 /**
8900 * If not network active, set the activity status based on the site-level plugin status.
8901 */
8902 if ( ! $new_plugin['is_active'] ) {
8903 $new_plugin['is_active'] = $is_site_level_active;
8904 }
8905
8906 $plugins_update_data[] = $new_plugin;
8907
8908 if ( isset( $site_active_plugins[ $basename ] ) ) {
8909 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
8910 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
8911 }
8912 }
8913 }
8914
8915 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
8916 $site_active_plugins_cache->timestamp = $time;
8917 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8918
8919 $network_plugins_cache->md5 = $network_plugins_thumbprint;
8920 $network_plugins_cache->timestamp = $time;
8921 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
8922
8923 return $plugins_update_data;
8924 }
8925
8926 /**
8927 * Return a list of modified themes since the last sync.
8928 *
8929 * Note:
8930 * There's no point to store a themes counter since even if the number of
8931 * themes didn't change, we still need to check if the versions are all the
8932 * same and the activity state is similar.
8933 *
8934 * @author Vova Feldman (@svovaf)
8935 * @since 1.1.8
8936 *
8937 * @return array|false
8938 */
8939 private function get_themes_data_for_api() {
8940 // Alias.
8941 $option_name = 'all_themes';
8942
8943 $all_cached_themes = self::$_accounts->get_option( $option_name );
8944
8945 if ( ! is_object( $all_cached_themes ) ) {
8946 $all_cached_themes = (object) array(
8947 'timestamp' => '',
8948 'md5' => '',
8949 'themes' => array(),
8950 );
8951 }
8952
8953 $time = time();
8954
8955 if ( ! empty( $all_cached_themes->timestamp ) &&
8956 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8957 ) {
8958 // Don't send theme updates if last update was in the past 5 min.
8959 return false;
8960 }
8961
8962 // Write timestamp to lock the logic.
8963 $all_cached_themes->timestamp = $time;
8964 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
8965
8966 // Reload options from DB.
8967 self::$_accounts->load( true );
8968 $all_cached_themes = self::$_accounts->get_option( $option_name );
8969
8970 if ( $time != $all_cached_themes->timestamp ) {
8971 // If timestamp is different, then another thread captured the lock.
8972 return false;
8973 }
8974
8975 // Get active theme.
8976 $active_theme = wp_get_theme();
8977 $active_theme_stylesheet = $active_theme->get_stylesheet();
8978
8979 // Check if there's a change in themes.
8980 $all_themes = wp_get_themes();
8981
8982 // Check if themes changed.
8983 ksort( $all_themes );
8984
8985 $themes_signature = '';
8986 foreach ( $all_themes as $slug => $data ) {
8987 $is_active = ( $slug === $active_theme_stylesheet );
8988 $themes_signature .= $slug . ',' .
8989 $data->version . ',' .
8990 ( $is_active ? '1' : '0' ) . ';';
8991 }
8992
8993 // Check if themes status changed (version or active/inactive).
8994 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
8995
8996 $themes_update_data = array();
8997
8998 if ( $themes_changed ) {
8999 // Change in themes, report changes.
9000
9001 // Update existing themes info.
9002 foreach ( $all_cached_themes->themes as $slug => $data ) {
9003 $is_active = ( $slug === $active_theme_stylesheet );
9004
9005 if ( ! isset( $all_themes[ $slug ] ) ) {
9006 // Plugin uninstalled.
9007 $uninstalled_theme_data = $data;
9008 $uninstalled_theme_data['is_active'] = false;
9009 $uninstalled_theme_data['is_uninstalled'] = true;
9010 $themes_update_data[] = $uninstalled_theme_data;
9011
9012 unset( $all_themes[ $slug ] );
9013 unset( $all_cached_themes->themes[ $slug ] );
9014 } else if ( $data['is_active'] !== $is_active ||
9015 $data['version'] !== $all_themes[ $slug ]->version
9016 ) {
9017 // Plugin activated or deactivated, or version changed.
9018
9019 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
9020 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
9021
9022 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
9023 }
9024 }
9025
9026 // Find new themes that weren't yet seen before.
9027 foreach ( $all_themes as $slug => $data ) {
9028 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
9029 $is_active = ( $slug === $active_theme_stylesheet );
9030
9031 // New plugin.
9032 $new_plugin = array(
9033 'slug' => $slug,
9034 'version' => $data->version,
9035 'title' => $data->name,
9036 'is_active' => $is_active,
9037 'is_uninstalled' => false,
9038 );
9039
9040 $themes_update_data[] = $new_plugin;
9041 $all_cached_themes->themes[ $slug ] = $new_plugin;
9042 }
9043 }
9044
9045 $all_cached_themes->md5 = md5( $themes_signature );
9046 $all_cached_themes->timestamp = time();
9047 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9048 }
9049
9050 return $themes_update_data;
9051 }
9052
9053 /**
9054 * Get site data for API install request.
9055 *
9056 * @author Vova Feldman (@svovaf)
9057 * @since 1.1.2
9058 *
9059 * @param string[] $override
9060 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9061 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9062 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, title, and URL).
9063 *
9064 * @return array
9065 */
9066 private function get_install_data_for_api(
9067 array $override,
9068 $include_plugins = true,
9069 $include_themes = true,
9070 $include_blog_data = true
9071 ) {
9072 // Alias.
9073 $permissions = FS_Permission_Manager::instance( $this );
9074
9075 if ( $permissions->is_extensions_tracking_allowed() ) {
9076 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
9077 /**
9078 * @since 1.1.8 Also send plugin updates.
9079 */
9080 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9081 $plugins = $this->get_plugins_data_for_api();
9082 if ( ! empty( $plugins ) ) {
9083 $override['plugins'] = $plugins;
9084 }
9085 }
9086 }
9087
9088 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
9089 /**
9090 * @since 1.1.8 Also send themes updates.
9091 */
9092 if ( $include_themes && ! isset( $override['themes'] ) ) {
9093 $themes = $this->get_themes_data_for_api();
9094 if ( ! empty( $themes ) ) {
9095 $override['themes'] = $themes;
9096 }
9097 }
9098 }
9099 }
9100
9101 $versions = $this->get_versions();
9102
9103 $blog_data = array();
9104 if ( $include_blog_data ) {
9105 $blog_data['url'] = self::get_unfiltered_site_url();
9106
9107 if ( $permissions->is_diagnostic_tracking_allowed() ) {
9108 $blog_data = array_merge( $blog_data, array(
9109 'language' => self::get_sanitized_language(),
9110 'title' => get_bloginfo( 'name' ),
9111 ) );
9112 }
9113 }
9114
9115 return array_merge( $versions, $blog_data, array(
9116 'version' => $this->get_plugin_version(),
9117 'is_premium' => $this->is_premium(),
9118 // Special params.
9119 'is_active' => true,
9120 'is_uninstalled' => false,
9121 ), $override );
9122 }
9123
9124 /**
9125 * Update installs details.
9126 *
9127 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
9128 *
9129 * @author Vova Feldman (@svovaf)
9130 * @since 2.0.0
9131 *
9132 * @param string[] string $override
9133 * @param bool $only_diff
9134 * @param bool $is_keepalive
9135 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9136 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9137 *
9138 * @return array
9139 */
9140 private function get_installs_data_for_api(
9141 array $override,
9142 $only_diff = false,
9143 $is_keepalive = false,
9144 $include_plugins = true,
9145 $include_themes = true
9146 ) {
9147 /**
9148 * @since 1.1.8 Also send plugin updates.
9149 */
9150 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9151 // $plugins = $this->get_plugins_data_for_api();
9152 // if ( ! empty( $plugins ) ) {
9153 // $override['plugins'] = $plugins;
9154 // }
9155 // }
9156 /**
9157 * @since 1.1.8 Also send themes updates.
9158 */
9159 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9160 // $themes = $this->get_themes_data_for_api();
9161 // if ( ! empty( $themes ) ) {
9162 // $override['themes'] = $themes;
9163 // }
9164 // }
9165
9166 // Common properties.
9167 $versions = $this->get_versions();
9168 $common = array_merge( $versions, array(
9169 'version' => $this->get_plugin_version(),
9170 'is_premium' => $this->is_premium(),
9171 ), $override );
9172
9173
9174 $is_common_diff_for_any_site = false;
9175 $common_diff_union = array();
9176
9177 $installs_data = array();
9178
9179 $sites = self::get_sites();
9180
9181 $subsite_data_for_api_by_install_id = array();
9182 $install_url_by_install_id = array();
9183 $subsite_registration_date_by_install_id = array();
9184
9185 foreach ( $sites as $site ) {
9186 $blog_id = self::get_site_blog_id( $site );
9187
9188 $install = $this->get_install_by_blog_id( $blog_id );
9189
9190 if ( is_object( $install ) ) {
9191 if ( $install->user_id != $this->_user->id ) {
9192 // Install belongs to a different owner.
9193 continue;
9194 }
9195
9196 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
9197 // Don't send updates regarding opted-out installs.
9198 continue;
9199 }
9200
9201 $install_data = $this->get_site_info( $site, true );
9202
9203 if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $install_data['blog_id'] ) ) {
9204 continue;
9205 }
9206
9207 $uid = $install_data['uid'];
9208 $url = $install_data['url'];
9209 $registration_date = $install_data['registration_date'];
9210
9211 if ( isset( $subsite_data_for_api_by_install_id[ $install->id ] ) ) {
9212 $clone_subsite_data = $subsite_data_for_api_by_install_id[ $install->id ];
9213 $clone_install_url = $install_url_by_install_id[ $install->id ];
9214 $clone_subsite_registration_date = $subsite_registration_date_by_install_id[ $install->id ];
9215
9216 $skip = false;
9217
9218 if (
9219 ! empty( $install_data['registration_date'] ) &&
9220 ! empty( $clone_subsite_registration_date )
9221 ) {
9222 /**
9223 * 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.
9224 *
9225 * @author Leo Fajardo (@leorw)
9226 * @since 2.5.1
9227 */
9228 $skip = ( strtotime( $install_data['registration_date'] ) > strtotime( $clone_subsite_registration_date ) );
9229 } else if (
9230 /**
9231 * 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.
9232 *
9233 * @author Leo Fajardo (@leorw)
9234 * @since 2.5.0
9235 */
9236 fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) ||
9237 fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $url ) )
9238 ) {
9239 $skip = true;
9240 }
9241
9242 if ( $skip ) {
9243 // 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.
9244 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
9245 continue;
9246 }
9247 }
9248
9249 unset( $install_data['blog_id'] );
9250 unset( $install_data['uid'] );
9251 unset( $install_data['url'] );
9252 unset( $install_data['registration_date'] );
9253
9254 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9255 $install_data['is_uninstalled'] = $install->is_uninstalled;
9256
9257 $common_diff = null;
9258 $is_common_diff = false;
9259 if ( $only_diff ) {
9260 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9261 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9262
9263 $is_common_diff = ! empty( $common_diff );
9264
9265 if ( $is_common_diff ) {
9266 foreach ( $common_diff as $k => $v ) {
9267 if ( ! isset( $common_diff_union[ $k ] ) ) {
9268 $common_diff_union[ $k ] = $v;
9269 }
9270 }
9271 }
9272
9273 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9274 }
9275
9276 if ( ! empty( $install_data ) || $is_common_diff || $is_keepalive ) {
9277 // Add install ID and site unique ID.
9278 $install_data['id'] = $install->id;
9279 $install_data['uid'] = $uid;
9280 $install_data['url'] = $url;
9281
9282 $subsite_data_for_api_by_install_id[ $install->id ] = $install_data;
9283 $install_url_by_install_id[ $install->id ] = $install->url;
9284 $subsite_registration_date_by_install_id[ $install->id ] = $registration_date;
9285 }
9286 }
9287 }
9288
9289 restore_current_blog();
9290
9291 $installs_data = array_merge(
9292 $installs_data,
9293 array_values( $subsite_data_for_api_by_install_id )
9294 );
9295
9296 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9297 if ( ! $only_diff ) {
9298 $installs_data[] = $common;
9299 } else if ( ! empty( $common_diff_union ) ) {
9300 $installs_data[] = $common_diff_union;
9301 }
9302 }
9303
9304 foreach ( $installs_data as &$data ) {
9305 $data = (object) $data;
9306 }
9307
9308 return $installs_data;
9309 }
9310
9311 /**
9312 * Compare site actual data to the stored install data and return the differences for an API data sync.
9313 *
9314 * @author Vova Feldman (@svovaf)
9315 * @since 2.0.0
9316 *
9317 * @param array $site
9318 * @param FS_Site $install
9319 * @param string[] string $override
9320 *
9321 * @return array
9322 */
9323 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9324 $diff = array();
9325 $special = array();
9326 $special_override = false;
9327
9328 foreach ( $site as $p => $v ) {
9329 if ( property_exists( $install, $p ) ) {
9330 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9331 $install->{$p} != $v
9332 ) {
9333 $val = self::get_api_sanitized_property( $p, $v );
9334
9335 if ( $install->{$p} != $val ) {
9336 $install->{$p} = $val;
9337 $diff[ $p ] = $val;
9338 }
9339 }
9340 } else {
9341 $special[ $p ] = $v;
9342
9343 if ( isset( $override[ $p ] ) ||
9344 'plugins' === $p ||
9345 'themes' === $p
9346 ) {
9347 $special_override = true;
9348 }
9349 }
9350 }
9351
9352 if ( $special_override || 0 < count( $diff ) ) {
9353 // Add special params only if has at least one
9354 // standard param, or if explicitly requested to
9355 // override a special param or a param which is not exist
9356 // in the install object.
9357 $diff = array_merge( $diff, $special );
9358 }
9359
9360 return $diff;
9361 }
9362
9363 /**
9364 * @author Leo Fajardo (@leorw)
9365 * @since 2.5.1
9366 */
9367 private function send_pending_clone_update_once() {
9368 $this->_logger->entrance();
9369
9370 if ( ! empty( $this->_storage->clone_id ) ) {
9371 return;
9372 }
9373
9374 $install_clone = $this->get_api_site_scope()->call(
9375 '/clones',
9376 'post',
9377 array( 'site_url' => self::get_unfiltered_site_url() )
9378 );
9379
9380 if ( $this->is_api_result_entity( $install_clone ) ) {
9381 $this->_storage->clone_id = $install_clone->id;
9382 }
9383 }
9384
9385 /**
9386 * @author Leo Fajardo (@leorw)
9387 * @since 2.5.1
9388 *
9389 * @param string $resolution_type
9390 * @param FS_Site $clone_context_install
9391 */
9392 function send_clone_resolution_update( $resolution_type, $clone_context_install ) {
9393 $this->_logger->entrance();
9394
9395 if ( empty( $this->_storage->clone_id ) ) {
9396 return;
9397 }
9398
9399 $new_install_id = null;
9400 $current_site = null;
9401
9402 $flush = false;
9403
9404 /**
9405 * 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.
9406 */
9407 if ( $clone_context_install->id != $this->_site->id ) {
9408 $new_install_id = $this->_site->id;
9409 $current_site = $this->_site;
9410 $this->_site = $clone_context_install;
9411
9412 $flush = true;
9413 }
9414
9415 $this->get_api_site_scope( $flush )->call(
9416 "/clones/{$this->_storage->clone_id}",
9417 'put',
9418 array(
9419 'resolution' => $resolution_type,
9420 'new_install_id' => $new_install_id,
9421 )
9422 );
9423
9424 if ( is_object( $current_site ) ) {
9425 /**
9426 * Ensure that the install scope entity is updated back to the previous install entity.
9427 */
9428 $this->_site = $current_site;
9429
9430 // Restore the previous install scope entity of the API.
9431 $this->get_api_site_scope( true );
9432 }
9433 }
9434
9435 /**
9436 * Update install only if changed.
9437 *
9438 * @author Vova Feldman (@svovaf)
9439 * @since 1.0.9
9440 *
9441 * @param string[] string $override
9442 * @param bool $flush
9443 * @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.
9444 *
9445 * @return false|object|string
9446 */
9447 private function send_install_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9448 $this->_logger->entrance();
9449
9450 $check_properties = $this->get_install_data_for_api( $override );
9451
9452 if ( $flush ) {
9453 $params = $check_properties;
9454 } else {
9455 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9456 }
9457
9458 if ( empty( $params ) ) {
9459 $keepalive_only_update = $this->should_send_keepalive_update();
9460
9461 if ( ! $keepalive_only_update ) {
9462 /**
9463 * There are no updates to send including keepalive.
9464 *
9465 * @author Leo Fajardo (@leorw)
9466 * @since 2.2.3
9467 */
9468 return false;
9469 }
9470 }
9471
9472 if ( $is_two_way_sync ) {
9473 /**
9474 * Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9475 *
9476 * @author Leo Fajardo (@leorw)
9477 * @since 2.2.3
9478 */
9479 if ( ! is_multisite() ) {
9480 // Update last install sync timestamp.
9481 $this->set_cron_execution_timestamp( 'install_sync' );
9482 }
9483
9484 $params['uid'] = $this->get_anonymous_id();
9485 }
9486
9487 $this->set_keepalive_timestamp();
9488
9489 // Send updated values to FS.
9490 $site = $this->api_site_call( '/', 'put', $params, true );
9491
9492 if ( $is_two_way_sync && $this->is_api_result_entity( $site ) ) {
9493 /**
9494 * Clear scheduled install sync after a two-way sync call.
9495 *
9496 * @author Leo Fajardo (@leorw)
9497 * @since 2.2.3
9498 */
9499 if ( ! is_multisite() ) {
9500 // I successfully sent install update, clear scheduled sync if exist.
9501 $this->clear_install_sync_cron();
9502 }
9503 }
9504
9505 return $site;
9506 }
9507
9508 /**
9509 * Update installs only if changed.
9510 *
9511 * @author Vova Feldman (@svovaf)
9512 * @since 2.0.0
9513 *
9514 * @param string[] string $override
9515 * @param bool $flush
9516 * @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.
9517 *
9518 * @return false|object|string
9519 */
9520 private function send_installs_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9521 $this->_logger->entrance();
9522
9523 /**
9524 * Pass `true` to use the network level storage since the update is for many installs.
9525 *
9526 * @author Leo Fajardo (@leorw)
9527 * @since 2.2.3
9528 */
9529 $should_send_keepalive = $this->should_send_keepalive_update( true );
9530
9531 $installs_data = $this->get_installs_data_for_api( $override, ! $flush, $should_send_keepalive );
9532
9533 if ( empty( $installs_data ) ) {
9534 return false;
9535 }
9536
9537 if ( $is_two_way_sync ) {
9538 // Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9539 $this->set_cron_execution_timestamp( 'install_sync' );
9540 }
9541
9542 /**
9543 * Pass `true` to use the network level storage since the update is for many installs.
9544 *
9545 * @author Leo Fajardo (@leorw)
9546 * @since 2.2.3
9547 */
9548 $this->set_keepalive_timestamp( true );
9549
9550 // Send updated values to FS.
9551 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9552
9553 if ( $is_two_way_sync && $this->is_api_result_object( $result, 'installs' ) ) {
9554 // I successfully sent a two-way installs update, clear the scheduled install sync if it exists.
9555 $this->clear_install_sync_cron();
9556 }
9557
9558 return $result;
9559 }
9560
9561 /**
9562 * @author Leo Fajardo (@leorw)
9563 *
9564 * @param bool|null $use_network_level_storage
9565 *
9566 * @return bool
9567 */
9568 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9569 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9570
9571 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9572 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9573 return true;
9574 } else {
9575 // 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.
9576 return ( 7 == rand( 1, 7 ) );
9577 }
9578 }
9579
9580 /**
9581 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9582 *
9583 * @author Leo Fajardo (@leorw)
9584 * @since 2.3.2
9585 */
9586 private function maybe_sync_install_user() {
9587 if ( $this->_user->id == $this->_site->user_id ) {
9588 return;
9589 }
9590
9591 // Fetch user data and store if found.
9592 $this->sync_user_by_current_install();
9593 }
9594
9595 /**
9596 * Update install only if changed.
9597 *
9598 * @author Vova Feldman (@svovaf)
9599 * @since 1.0.9
9600 *
9601 * @param string[] string $override
9602 * @param bool $flush
9603 */
9604 function sync_install( $override = array(), $flush = false ) {
9605 $this->_logger->entrance();
9606
9607 $site = $this->send_install_update( $override, $flush, true );
9608
9609 if ( false === $site ) {
9610 // No sync required.
9611 return;
9612 }
9613
9614 if ( ! $this->is_api_result_entity( $site ) ) {
9615 // Failed to sync, don't update locally.
9616 return;
9617 }
9618
9619 $this->_site = new FS_Site( $site );
9620
9621 $this->_store_site( true );
9622 }
9623
9624 /**
9625 * Update install only if changed.
9626 *
9627 * @author Vova Feldman (@svovaf)
9628 * @since 1.0.9
9629 *
9630 * @param string[] string $override
9631 * @param bool $flush
9632 */
9633 private function sync_installs( $override = array(), $flush = false ) {
9634 $this->_logger->entrance();
9635
9636 $result = $this->send_installs_update( $override, $flush, true );
9637
9638 if ( false === $result ) {
9639 // No sync required.
9640 return;
9641 }
9642
9643 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9644 // Failed to sync, don't update locally.
9645 return;
9646 }
9647
9648 $address_to_blog_map = $this->get_address_to_blog_map();
9649
9650 foreach ( $result->installs as $install ) {
9651 $this->_site = new FS_Site( $install );
9652
9653 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9654 $blog_id = $address_to_blog_map[ $address ];
9655
9656 $this->_store_site( true, $blog_id );
9657 }
9658 }
9659
9660 /**
9661 * Track install's custom event.
9662 *
9663 * IMPORTANT:
9664 * Custom event tracking is currently only supported for specific clients.
9665 * If you are not one of them, please don't use this method. If you will,
9666 * the API will simply ignore your request based on the plugin ID.
9667 *
9668 * Need custom tracking for your plugin or theme?
9669 * If you are interested in custom event tracking please contact yo@freemius.com
9670 * for further details.
9671 *
9672 * @author Vova Feldman (@svovaf)
9673 * @since 1.2.1
9674 *
9675 * @param string $name Event name.
9676 * @param array $properties Associative key/value array with primitive values only
9677 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9678 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9679 *
9680 * @return object|false Event data or FALSE on failure.
9681 *
9682 * @throws \Freemius_InvalidArgumentException
9683 */
9684 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9685 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9686
9687 if ( ! $this->is_registered() ) {
9688 return false;
9689 }
9690
9691 $event = array( 'type' => $name );
9692
9693 if ( is_numeric( $process_at ) && $process_at > time() ) {
9694 $event['process_at'] = $process_at;
9695 }
9696
9697 if ( $once ) {
9698 $event['once'] = true;
9699 }
9700
9701 if ( ! empty( $properties ) ) {
9702 // Verify associative array values are primitive.
9703 foreach ( $properties as $k => $v ) {
9704 if ( ! is_scalar( $v ) ) {
9705 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9706 }
9707 }
9708
9709 $event['properties'] = $properties;
9710 }
9711
9712 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9713
9714 return $this->is_api_error( $result ) ?
9715 false :
9716 $result;
9717 }
9718
9719 /**
9720 * Track install's custom event only once, but it still triggers the API call.
9721 *
9722 * IMPORTANT:
9723 * Custom event tracking is currently only supported for specific clients.
9724 * If you are not one of them, please don't use this method. If you will,
9725 * the API will simply ignore your request based on the plugin ID.
9726 *
9727 * Need custom tracking for your plugin or theme?
9728 * If you are interested in custom event tracking please contact yo@freemius.com
9729 * for further details.
9730 *
9731 * @author Vova Feldman (@svovaf)
9732 * @since 1.2.1
9733 *
9734 * @param string $name Event name.
9735 * @param array $properties Associative key/value array with primitive values only
9736 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9737 *
9738 * @return object|false Event data or FALSE on failure.
9739 *
9740 * @throws \Freemius_InvalidArgumentException
9741 *
9742 * @user Freemius::track_event()
9743 */
9744 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9745 return $this->track_event( $name, $properties, $process_at, true );
9746 }
9747
9748 /**
9749 * Plugin uninstall hook.
9750 *
9751 * @author Vova Feldman (@svovaf)
9752 * @since 1.0.1
9753 *
9754 * @param bool $check_user Enforce checking if user have plugins activation privileges.
9755 */
9756 function _uninstall_plugin_event( $check_user = true ) {
9757 $this->_logger->entrance( 'slug = ' . $this->_slug );
9758
9759 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
9760 return;
9761 }
9762
9763 $params = array();
9764 $uninstall_reason = null;
9765 if ( isset( $this->_storage->uninstall_reason ) ) {
9766 $uninstall_reason = $this->_storage->uninstall_reason;
9767 $params['reason_id'] = $uninstall_reason->id;
9768 $params['reason_info'] = $uninstall_reason->info;
9769 }
9770
9771 if ( ! $this->is_registered() ) {
9772 // Send anonymous uninstall event only if user submitted a feedback.
9773 if ( isset( $uninstall_reason ) ) {
9774 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
9775 $this->opt_in( false, false, false, false, true );
9776 } else {
9777 $params['uid'] = $this->get_anonymous_id();
9778 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
9779 }
9780 }
9781 } else {
9782 $params = array_merge( $params, array(
9783 'is_active' => false,
9784 'is_uninstalled' => true,
9785 ) );
9786
9787 if ( $this->_is_network_active ) {
9788 // Send uninstall event.
9789 $this->send_installs_update( $params );
9790 } else {
9791 // Send uninstall event and handle the result.
9792 $this->sync_install( $params );
9793 }
9794 }
9795
9796 // @todo Decide if we want to delete plugin information from db.
9797 }
9798
9799 /**
9800 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
9801 *
9802 * @author Vova Feldman (@svovaf)
9803 * @since 2.2.1
9804 *
9805 * @param string $is_premium
9806 * @param string $caller
9807 *
9808 * @return void
9809 */
9810 function set_basename( $is_premium, $caller ) {
9811 $basename = plugin_basename( $caller );
9812
9813 $current_basename = $is_premium ?
9814 $this->_premium_plugin_basename :
9815 $this->_free_plugin_basename;
9816
9817 if ( $current_basename == $basename ) {
9818 // Basename value set correctly.
9819 return;
9820 }
9821
9822 if ( $is_premium ) {
9823 $this->_premium_plugin_basename = $basename;
9824 } else {
9825 $this->_free_plugin_basename = $basename;
9826 }
9827
9828 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
9829
9830 register_activation_hook(
9831 $plugin_dir . $basename,
9832 array( &$this, '_activate_plugin_event_hook' )
9833 );
9834 }
9835
9836 /**
9837 * @author Vova Feldman (@svovaf)
9838 * @since 1.1.1
9839 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
9840 *
9841 * @return string
9842 */
9843 function premium_plugin_basename() {
9844 if ( ! isset( $this->_premium_plugin_basename ) ) {
9845 $this->_premium_plugin_basename = $this->is_premium() ?
9846 // The product is premium, so use the current basename.
9847 $this->_plugin_basename :
9848 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
9849 }
9850
9851 return $this->_premium_plugin_basename;
9852 }
9853
9854 /**
9855 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
9856 *
9857 * @author Vova Feldman (@svovaf)
9858 * @since 1.0.2
9859 */
9860 public static function _uninstall_plugin_hook() {
9861 self::_load_required_static();
9862
9863 self::$_static_logger->entrance();
9864
9865 if ( ! current_user_can( 'activate_plugins' ) ) {
9866 return;
9867 }
9868
9869 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
9870
9871 self::$_static_logger->info( 'plugin = ' . $plugin_file );
9872
9873 define( 'WP_FS__UNINSTALL_MODE', true );
9874
9875 $fs = self::get_instance_by_file( $plugin_file );
9876
9877 if ( is_object( $fs ) ) {
9878 $fs->remove_sdk_reference();
9879
9880 self::require_plugin_essentials();
9881
9882 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
9883 is_plugin_active( $fs->premium_plugin_basename() )
9884 ) {
9885 // Deleting Free or Premium plugin version while the other version still installed.
9886 return;
9887 }
9888
9889 if (
9890 ! $fs->is_clone() &&
9891 /**
9892 * 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).
9893 *
9894 * @author Leo Fajardo
9895 */
9896 ( ! is_object( $fs->_site ) || $fs->is_registered() )
9897 ) {
9898 $fs->_uninstall_plugin_event();
9899 }
9900
9901 $fs->do_action( 'after_uninstall' );
9902 }
9903 }
9904
9905 #----------------------------------------------------------------------------------
9906 #region Plugin Information
9907 #----------------------------------------------------------------------------------
9908
9909 /**
9910 * Load WordPress core plugin.php essential module.
9911 *
9912 * @author Vova Feldman (@svovaf)
9913 * @since 1.1.1
9914 */
9915 private static function require_plugin_essentials() {
9916 if ( ! function_exists( 'get_plugins' ) ) {
9917 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
9918
9919 require_once ABSPATH . 'wp-admin/includes/plugin.php';
9920 }
9921 }
9922
9923 /**
9924 * Load WordPress core pluggable.php module.
9925 *
9926 * @author Vova Feldman (@svovaf)
9927 * @since 1.1.2
9928 */
9929 private static function require_pluggable_essentials() {
9930 if ( ! function_exists( 'wp_get_current_user' ) ) {
9931 require_once ABSPATH . 'wp-includes/pluggable.php';
9932 }
9933 }
9934
9935 /**
9936 * Return plugin data.
9937 *
9938 * @author Vova Feldman (@svovaf)
9939 * @since 1.0.1
9940 *
9941 * @param bool $reparse_plugin_metadata
9942 *
9943 * @return array
9944 */
9945 function get_plugin_data( $reparse_plugin_metadata = false ) {
9946 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
9947 self::require_plugin_essentials();
9948
9949 if ( $this->is_plugin() ) {
9950 /**
9951 * @author Vova Feldman (@svovaf)
9952 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
9953 *
9954 * @link https://github.com/Freemius/wordpress-sdk/issues/77
9955 */
9956 $plugin_data = get_plugin_data(
9957 $this->_plugin_main_file_path,
9958 false,
9959 false
9960 );
9961 } else {
9962 $theme_data = wp_get_theme();
9963
9964 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
9965 $parent_theme = $theme_data->parent();
9966
9967 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
9968 $theme_data = $parent_theme;
9969 }
9970 }
9971
9972 $plugin_data = array(
9973 'Name' => $theme_data->get( 'Name' ),
9974 'Version' => $theme_data->get( 'Version' ),
9975 'Author' => $theme_data->get( 'Author' ),
9976 'Description' => $theme_data->get( 'Description' ),
9977 'PluginURI' => $theme_data->get( 'ThemeURI' ),
9978 );
9979 }
9980
9981 $this->_plugin_data = $plugin_data;
9982 }
9983
9984 return $this->_plugin_data;
9985 }
9986
9987 /**
9988 * @author Vova Feldman (@svovaf)
9989 * @since 1.0.1
9990 * @since 1.2.2.5 If slug not set load slug by module ID.
9991 *
9992 * @return string Plugin slug.
9993 */
9994 function get_slug() {
9995 if ( ! isset( $this->_slug ) ) {
9996 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
9997 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
9998 }
9999
10000 return $this->_slug;
10001 }
10002
10003 /**
10004 * @author Leo Fajardo (@leorw)
10005 * @since 2.2.1
10006 *
10007 * @return string
10008 */
10009 function get_premium_slug() {
10010 return ( is_object( $this->_plugin ) && ! empty( $this->_plugin->premium_slug ) ) ?
10011 $this->_plugin->premium_slug :
10012 "{$this->_slug}-premium";
10013 }
10014
10015 /**
10016 * Retrieve the desired folder name for the product.
10017 *
10018 * @author Vova Feldman (@svovaf)
10019 * @since 1.2.1.7
10020 *
10021 * @return string Plugin slug.
10022 */
10023 function get_target_folder_name() {
10024 return $this->can_use_premium_code() ?
10025 $this->_plugin->premium_slug :
10026 $this->_slug;
10027 }
10028
10029 /**
10030 * @author Vova Feldman (@svovaf)
10031 * @since 1.0.1
10032 *
10033 * @return number Plugin ID.
10034 */
10035 function get_id() {
10036 return $this->_plugin->id;
10037 }
10038
10039 /**
10040 * @author Leo Fajardo (@leorw)
10041 * @since 2.2.4
10042 *
10043 * @return number|null Bundle ID.
10044 */
10045 function get_bundle_id() {
10046 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
10047 $this->_plugin->bundle_id :
10048 null;
10049 }
10050
10051 /**
10052 * @author Vova Feldman (@svovaf)
10053 * @since 2.3.1
10054 *
10055 * @return string|null Bundle public key.
10056 */
10057 function get_bundle_public_key() {
10058 return isset( $this->_plugin->bundle_public_key ) ?
10059 $this->_plugin->bundle_public_key :
10060 null;
10061 }
10062
10063 /**
10064 * Get whether the SDK has been initiated in the context of a Bundle.
10065 *
10066 * This will return true, if `bundle_id` is present in the SDK init parameters.
10067 *
10068 * ```php
10069 * $my_fs = fs_dynamic_init( array(
10070 * // ...
10071 * 'bundle_id' => 'XXXX', // Will return true since we have bundle id.
10072 * 'bundle_public_key' => 'pk_XXXX',
10073 * ) );
10074 * ```
10075 *
10076 * @author Swashata Ghosh (@swashata)
10077 * @since 2.5.0
10078 *
10079 * @return bool True if we are running in bundle context, false otherwise.
10080 */
10081 private function has_bundle_context() {
10082 return ! is_null( $this->get_bundle_id() );
10083 }
10084
10085 /**
10086 * @author Vova Feldman (@svovaf)
10087 * @since 1.2.1.5
10088 *
10089 * @return string Freemius SDK version
10090 */
10091 function get_sdk_version() {
10092 return $this->version;
10093 }
10094
10095 /**
10096 * @author Vova Feldman (@svovaf)
10097 * @since 1.2.1.5
10098 *
10099 * @return number Parent plugin ID (if parent exist).
10100 */
10101 function get_parent_id() {
10102 return $this->is_addon() ?
10103 $this->get_parent_instance()->get_id() :
10104 $this->_plugin->id;
10105 }
10106
10107 /**
10108 * @author Vova Feldman (@svovaf)
10109 * @since 2.3.1
10110 *
10111 * @return string
10112 */
10113 function get_usage_tracking_terms_url() {
10114 return $this->apply_filters(
10115 'usage_tracking_terms_url',
10116 "https://freemius.com/product/opt-in/{$this->_plugin->id}/{$this->_slug}/"
10117 );
10118 }
10119
10120 /**
10121 * @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.
10122 * @since 2.5.8
10123 *
10124 * @return string
10125 */
10126 function get_license_activation_terms_url() {
10127 return $this->apply_filters(
10128 'license_activation_terms_url',
10129 "https://freemius.com/product/license-activation/{$this->_plugin->id}/{$this->_slug}/"
10130 );
10131 }
10132
10133 /**
10134 * @author Vova Feldman (@svovaf)
10135 * @since 2.3.1
10136 *
10137 * @return string
10138 */
10139 function get_eula_url() {
10140 return $this->apply_filters(
10141 'eula_url',
10142 "https://freemius.com/product/{$this->_plugin->id}/{$this->_slug}/legal/eula/"
10143 );
10144 }
10145
10146 /**
10147 * @author Vova Feldman (@svovaf)
10148 * @since 1.0.1
10149 *
10150 * @return string Plugin public key.
10151 */
10152 function get_public_key() {
10153 return $this->_plugin->public_key;
10154 }
10155
10156 /**
10157 * Will be available only on sandbox mode.
10158 *
10159 * @author Vova Feldman (@svovaf)
10160 * @since 1.0.4
10161 *
10162 * @return mixed Plugin secret key.
10163 */
10164 function get_secret_key() {
10165 return $this->_plugin->secret_key;
10166 }
10167
10168 /**
10169 * @author Vova Feldman (@svovaf)
10170 * @since 1.1.1
10171 *
10172 * @return bool
10173 */
10174 function has_secret_key() {
10175 return ! empty( $this->_plugin->secret_key );
10176 }
10177
10178 /**
10179 * @author Vova Feldman (@svovaf)
10180 * @since 1.0.9
10181 *
10182 * @param string|bool $premium_suffix
10183 *
10184 * @return string
10185 */
10186 function get_plugin_name( $premium_suffix = false ) {
10187 $this->_logger->entrance();
10188
10189 /**
10190 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10191 *
10192 * @author Vova Feldman
10193 */
10194 if ( ! isset( $this->_plugin_name ) ) {
10195 // Name is not yet set.
10196 $this->set_name( $premium_suffix );
10197 } else if (
10198 ! empty( $premium_suffix ) &&
10199 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10200 ) {
10201 // Name is already set, but there's a change in the premium suffix.
10202 $this->set_name( $premium_suffix );
10203 }
10204
10205 return $this->_plugin_name;
10206 }
10207
10208 /**
10209 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10210 *
10211 * @author Vova Feldman (@svovaf)
10212 * @since 2.2.1
10213 *
10214 * @param string $premium_suffix
10215 */
10216 private function set_name( $premium_suffix = '' ) {
10217 $plugin_data = $this->get_plugin_data();
10218
10219 // Get name.
10220 $this->_plugin_name = $plugin_data['Name'];
10221
10222 if ( is_string( $premium_suffix ) ) {
10223 $premium_suffix = trim( $premium_suffix );
10224
10225 if ( ! empty( $premium_suffix ) ) {
10226 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10227 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10228 $suffix_len = strlen( $suffix );
10229
10230 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10231 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10232 ) {
10233 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10234 }
10235 }
10236 }
10237
10238 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10239 }
10240
10241 /**
10242 * @author Vova Feldman (@svovaf)
10243 * @since 1.0.0
10244 *
10245 * @param bool $reparse_plugin_metadata
10246 *
10247 * @return string
10248 */
10249 function get_plugin_version( $reparse_plugin_metadata = false ) {
10250 $this->_logger->entrance();
10251
10252 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10253
10254 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10255
10256 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10257 }
10258
10259 /**
10260 * @author Vova Feldman (@svovaf)
10261 * @since 1.2.1.7
10262 *
10263 * @return string
10264 */
10265 function get_plugin_title() {
10266 $this->_logger->entrance();
10267
10268 $title = $this->_plugin->title;
10269
10270 return $this->apply_filters( 'plugin_title', $title );
10271 }
10272
10273 /**
10274 * @author Vova Feldman (@svovaf)
10275 * @since 1.2.2.7
10276 *
10277 * @param bool $lowercase
10278 *
10279 * @return string
10280 */
10281 function get_module_label( $lowercase = false ) {
10282 $label = $this->is_addon() ?
10283 $this->get_text_inline( 'Add-On', 'addon' ) :
10284 ( $this->is_plugin() ?
10285 $this->get_text_inline( 'Plugin', 'plugin' ) :
10286 $this->get_text_inline( 'Theme', 'theme' ) );
10287
10288 if ( $lowercase ) {
10289 $label = strtolower( $label );
10290 }
10291
10292 return $label;
10293 }
10294
10295 /**
10296 * @author Vova Feldman (@svovaf)
10297 * @since 1.0.4
10298 *
10299 * @return string
10300 */
10301 function get_plugin_basename() {
10302 if ( ! isset( $this->_plugin_basename ) ) {
10303 if ( $this->is_plugin() ) {
10304 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10305 } else {
10306 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10307 }
10308 }
10309
10310 return $this->_plugin_basename;
10311 }
10312
10313 function get_plugin_folder_name() {
10314 $this->_logger->entrance();
10315
10316 $plugin_folder = $this->_plugin_basename;
10317
10318 while ( '.' !== dirname( $plugin_folder ) ) {
10319 $plugin_folder = dirname( $plugin_folder );
10320 }
10321
10322 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10323
10324 return $plugin_folder;
10325 }
10326
10327 #endregion ------------------------------------------------------------------
10328
10329 /* Account
10330 ------------------------------------------------------------------------------------------------------------------*/
10331
10332 /**
10333 * Find plugin's slug by plugin's basename.
10334 *
10335 * @author Vova Feldman (@svovaf)
10336 * @since 1.0.9
10337 *
10338 * @param string $plugin_base_name
10339 *
10340 * @return false|string
10341 */
10342 private static function find_slug_by_basename( $plugin_base_name ) {
10343 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10344
10345 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10346 return false;
10347 }
10348
10349 return $file_slug_map[ $plugin_base_name ];
10350 }
10351
10352 /**
10353 * Store the map between the plugin's basename to the slug.
10354 *
10355 * @author Vova Feldman (@svovaf)
10356 * @since 1.0.9
10357 */
10358 private function store_file_slug_map() {
10359 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10360
10361 if ( ! array( $file_slug_map ) ) {
10362 $file_slug_map = array();
10363 }
10364
10365 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10366 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10367 ) {
10368 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10369 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10370 }
10371 }
10372
10373 /**
10374 * @return array[number]FS_User
10375 */
10376 static function get_all_users() {
10377 $users = self::maybe_get_entities_account_option( 'users', array() );
10378
10379 if ( ! is_array( $users ) ) {
10380 $users = array();
10381 }
10382
10383 return $users;
10384 }
10385
10386 /**
10387 * @param string $module_type
10388 * @param null|int $blog_id Since 2.0.0
10389 *
10390 * @return array[string]FS_Site
10391 */
10392 public static function get_all_sites(
10393 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10394 $blog_id = null,
10395 $is_backup = false
10396 ) {
10397 $sites = self::get_account_option(
10398 ( $is_backup ? 'prev_' : '' ) . 'sites',
10399 $module_type,
10400 $blog_id
10401 );
10402
10403 if ( ! is_array( $sites ) ) {
10404 $sites = array();
10405 }
10406
10407 return $sites;
10408 }
10409
10410 /**
10411 * @author Leo Fajardo (@leorw)
10412 *
10413 * @since 1.2.2
10414 *
10415 * @param string $option_name
10416 * @param string $module_type
10417 * @param null|int $network_level_or_blog_id Since 2.0.0
10418 *
10419 * @return mixed
10420 */
10421 public static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10422 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10423 $option_name = $module_type . '_' . $option_name;
10424 }
10425
10426 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10427 }
10428
10429 /**
10430 * @author Leo Fajardo (@leorw)
10431 *
10432 * @since 1.2.2
10433 *
10434 * @param string $option_name
10435 * @param mixed $option_value
10436 * @param bool $store
10437 * @param null|int $network_level_or_blog_id Since 2.0.0
10438 */
10439 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10440 self::set_account_option_by_module(
10441 $this->_module_type,
10442 $option_name,
10443 $option_value,
10444 $store,
10445 $network_level_or_blog_id
10446 );
10447 }
10448
10449 /**
10450 * @author Vova Feldman (@svovaf)
10451 *
10452 * @since 1.2.2.7
10453 *
10454 * @param string $module_type
10455 * @param string $option_name
10456 * @param mixed $option_value
10457 * @param bool $store
10458 * @param null|int $network_level_or_blog_id Since 2.0.0
10459 */
10460 private static function set_account_option_by_module(
10461 $module_type,
10462 $option_name,
10463 $option_value,
10464 $store,
10465 $network_level_or_blog_id = null
10466 ) {
10467 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10468 $option_name = $module_type . '_' . $option_name;
10469 }
10470
10471 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10472 }
10473
10474 /**
10475 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10476 *
10477 * @author Leo Fajardo (@leorw)
10478 * @since 2.3.1
10479 *
10480 * @param string $option_name
10481 * @param mixed $default
10482 * @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).
10483 *
10484 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10485 */
10486 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10487 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10488
10489 $class_name = '';
10490
10491 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10492 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10493 }
10494
10495 switch ( $option_name ) {
10496 case 'plugins':
10497 case 'themes':
10498 case 'addons':
10499 $class_name = FS_Plugin::get_class_name();
10500 break;
10501 case 'users':
10502 $class_name = FS_User::get_class_name();
10503 break;
10504 case 'sites':
10505 $class_name = FS_Site::get_class_name();
10506 break;
10507 case 'licenses':
10508 case 'all_licenses':
10509 $class_name = FS_Plugin_License::get_class_name();
10510 break;
10511 case 'plans':
10512 $class_name = FS_Plugin_Plan::get_class_name();
10513 break;
10514 case 'updates':
10515 $class_name = FS_Plugin_Tag::get_class_name();
10516 break;
10517 }
10518
10519 if ( empty( $class_name ) ) {
10520 return $option;
10521 }
10522
10523 return fs_get_entities( $option, $class_name );
10524 }
10525
10526 /**
10527 * @author Vova Feldman (@svovaf)
10528 * @since 1.0.6
10529 *
10530 * @param number|null $module_id
10531 *
10532 * @return FS_Plugin_License[]
10533 */
10534 private static function get_all_licenses( $module_id = null ) {
10535 $licenses = self::get_account_option( 'all_licenses' );
10536
10537 if ( ! is_array( $licenses ) ) {
10538 $licenses = array();
10539 }
10540
10541 if ( is_null( $module_id ) ) {
10542 return $licenses;
10543 }
10544
10545 $licenses = isset( $licenses[ $module_id ] ) ?
10546 $licenses[ $module_id ] :
10547 array();
10548
10549 return $licenses;
10550 }
10551
10552 /**
10553 * @author Leo Fajardo (@leorw)
10554 * @since 2.0.0
10555 *
10556 * @param number $module_id
10557 * @param number|null $user_id
10558 *
10559 * @return array
10560 */
10561 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10562 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10563
10564 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10565 $all_modules_user_id_license_ids_map = array();
10566 }
10567
10568 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10569 $all_modules_user_id_license_ids_map[ $module_id ] :
10570 array();
10571
10572 if ( FS_User::is_valid_id( $user_id ) ) {
10573 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10574 $user_id_license_ids_map[ $user_id ] :
10575 array();
10576 }
10577
10578 return $user_id_license_ids_map;
10579 }
10580
10581 /**
10582 * @author Leo Fajardo (@leorw)
10583 * @since 2.0.0
10584 *
10585 * @param array $new_user_id_license_ids_map
10586 * @param number $module_id
10587 * @param number|null $user_id
10588 */
10589 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10590 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10591 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10592 $all_modules_user_id_license_ids_map = array();
10593 }
10594
10595 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10596 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10597 }
10598
10599 if ( FS_User::is_valid_id( $user_id ) ) {
10600 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10601 } else {
10602 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10603 }
10604
10605 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10606 }
10607
10608 /**
10609 * Get a collection of the user's linked license IDs.
10610 *
10611 * @author Vova Feldman (@svovaf)
10612 * @since 2.0.0
10613 *
10614 * @param number $user_id
10615 *
10616 * @return number[]
10617 */
10618 private function get_user_linked_license_ids( $user_id ) {
10619 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10620 }
10621
10622 /**
10623 * Override the user's linked license IDs with a new IDs collection.
10624 *
10625 * @author Vova Feldman (@svovaf)
10626 * @since 2.0.0
10627 *
10628 * @param number $user_id
10629 * @param number[] $license_ids
10630 */
10631 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10632 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10633 }
10634
10635 /**
10636 * Link a specified license ID to a given user.
10637 *
10638 * @author Vova Feldman (@svovaf)
10639 * @since 2.0.0
10640 *
10641 * @param number $license_id
10642 * @param number $user_id
10643 */
10644 private function link_license_2_user( $license_id, $user_id ) {
10645 $license_ids = $this->get_user_linked_license_ids( $user_id );
10646
10647 if ( in_array( $license_id, $license_ids ) ) {
10648 // License already linked.
10649 return;
10650 }
10651
10652 $license_ids[] = $license_id;
10653
10654 $this->set_user_linked_license_ids( $user_id, $license_ids );
10655 }
10656
10657 /**
10658 * @param string|bool $module_type
10659 *
10660 * @return FS_Plugin_Plan[]
10661 */
10662 private static function get_all_plans( $module_type = false ) {
10663 $plans = self::get_account_option( 'plans', $module_type );
10664
10665 if ( ! is_array( $plans ) ) {
10666 $plans = array();
10667 }
10668
10669 return $plans;
10670 }
10671
10672 /**
10673 * @author Vova Feldman (@svovaf)
10674 * @since 1.0.4
10675 *
10676 * @return FS_Plugin_Tag[]
10677 */
10678 private static function get_all_updates() {
10679 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10680
10681 if ( ! is_array( $updates ) ) {
10682 $updates = array();
10683 }
10684
10685 return $updates;
10686 }
10687
10688 /**
10689 * @author Vova Feldman (@svovaf)
10690 * @since 1.0.6
10691 *
10692 * @return array<number,FS_Plugin[]>|false
10693 */
10694 public static function get_all_addons() {
10695 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10696
10697 if ( ! is_array( $addons ) ) {
10698 $addons = array();
10699 }
10700
10701 return $addons;
10702 }
10703
10704 /**
10705 * @author Vova Feldman (@svovaf)
10706 * @since 1.0.6
10707 *
10708 * @return number[]|false
10709 */
10710 public static function get_all_account_addons() {
10711 $addons = self::$_accounts->get_option( 'account_addons', array() );
10712
10713 if ( ! is_array( $addons ) ) {
10714 $addons = array();
10715 }
10716
10717 return $addons;
10718 }
10719
10720 /**
10721 * Check if user has connected his account (opted-in).
10722 *
10723 * Note:
10724 * If the user opted-in and opted-out on a later stage,
10725 * this will still return true. If you want to check if the
10726 * user is currently opted-in, use:
10727 * `$fs->is_registered() && $fs->is_tracking_allowed()`
10728 *
10729 * @author Vova Feldman (@svovaf)
10730 * @since 1.0.1
10731 *
10732 * @param bool $ignore_anonymous_state Since 2.5.1
10733 *
10734 * @return bool
10735 */
10736 function is_registered( $ignore_anonymous_state = false ) {
10737 return (
10738 is_object( $this->_user ) &&
10739 (
10740 $this->is_premium() ||
10741 $ignore_anonymous_state ||
10742 ! $this->is_anonymous()
10743 )
10744 );
10745 }
10746
10747 /**
10748 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
10749 *
10750 * @author Leo Fajardo (@leorw)
10751 * @since 1.2.1.5
10752 *
10753 * @return bool
10754 */
10755 function is_tracking_allowed( $blog_id = null, $install = null ) {
10756 if ( is_null( $install ) ) {
10757 $install = is_null( $blog_id ) ?
10758 $this->_site :
10759 $this->get_install_by_blog_id( $blog_id );
10760 }
10761
10762 return (
10763 is_object( $install ) &&
10764 FS_Permission_Manager::instance( $this )->is_homepage_url_tracking_allowed( $blog_id )
10765 );
10766 }
10767
10768 /**
10769 * Returns TRUE if the user never opted-in or manually opted-out.
10770 *
10771 * @author Vova Feldman (@svovaf)
10772 * @since 1.2.1.5
10773 *
10774 * @param int|null $blog_id
10775 *
10776 * @return bool
10777 */
10778 function is_tracking_prohibited( $blog_id = null ) {
10779 return (
10780 ! $this->is_registered( true ) ||
10781 ! $this->is_tracking_allowed( $blog_id )
10782 );
10783 }
10784
10785 /**
10786 * @author Leo Fajardo (@leorw)
10787 * @since 2.4.0
10788 *
10789 * @return bool
10790 */
10791 function is_bundle_license_auto_activation_enabled() {
10792 return $this->is_addon() ?
10793 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
10794 $this->_is_bundle_license_auto_activation_enabled;
10795 }
10796
10797 /**
10798 * @author Vova Feldman (@svovaf)
10799 * @since 1.0.4
10800 *
10801 * @return FS_Plugin
10802 */
10803 function get_plugin() {
10804 return $this->_plugin;
10805 }
10806
10807 /**
10808 * @author Vova Feldman (@svovaf)
10809 * @since 1.0.3
10810 *
10811 * @return FS_User
10812 */
10813 function get_user() {
10814 return $this->_user;
10815 }
10816
10817 /**
10818 * @author Vova Feldman (@svovaf)
10819 * @since 1.0.3
10820 *
10821 * @return FS_Site
10822 */
10823 function get_site() {
10824 return $this->_site;
10825 }
10826
10827 /**
10828 * @author Daniele Alessandra (@danielealessandra)
10829 * @return FS_Storage
10830 * @since 2.6.2
10831 *
10832 */
10833 public function get_storage() {
10834 return $this->_storage;
10835 }
10836
10837 /**
10838 * @author Leo Fajardo (@leorw)
10839 * @since 2.5.0
10840 */
10841 function store_site( $site ) {
10842 $this->_site = $site;
10843 $this->_store_site( true );
10844 }
10845
10846 /**
10847 * 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).
10848 *
10849 * @author Leo Fajardo (@leorw)
10850 * @since 2.5.0
10851 */
10852 function delete_current_install( $back_up ) {
10853 // Back up and delete the unique ID.
10854 if ( $back_up ) {
10855 self::$_accounts->set_option( 'prev_unique_id', $this->get_anonymous_id() );
10856 }
10857
10858 self::$_accounts->set_option( 'unique_id', null );
10859
10860 if ( $back_up ) {
10861 // 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).
10862 $this->back_up_site();
10863 }
10864
10865 $this->_delete_site();
10866 $this->_site = null;
10867 }
10868
10869 /**
10870 * @author Leo Fajardo (@leorw)
10871 * @since 2.5.0
10872 */
10873 function restore_backup_site() {
10874 self::$_accounts->set_option(
10875 'unique_id',
10876 self::$_accounts->get_option( 'prev_unique_id' )
10877 );
10878
10879 $sites = self::get_all_sites( $this->_module_type, null, true );
10880 $this->store_site( clone $sites[ $this->_slug ] );
10881 }
10882
10883 /**
10884 * Get plugin add-ons.
10885 *
10886 * @author Vova Feldman (@svovaf)
10887 * @since 1.0.6
10888 *
10889 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
10890 *
10891 * @param bool $flush
10892 *
10893 * @return FS_Plugin[]|false
10894 */
10895 function get_addons( $flush = false ) {
10896 $this->_logger->entrance();
10897
10898 if ( ! $this->_has_addons ) {
10899 return false;
10900 }
10901
10902 $addons = $this->sync_addons( $flush );
10903
10904 return ( ! is_array( $addons ) || empty( $addons ) ) ?
10905 false :
10906 $addons;
10907 }
10908
10909 /**
10910 * @author Vova Feldman (@svovaf)
10911 * @since 1.0.6
10912 *
10913 * @return number[]|false
10914 */
10915 function get_account_addons() {
10916 $this->_logger->entrance();
10917
10918 $addons = self::get_all_account_addons();
10919
10920 if ( ! is_array( $addons ) ||
10921 ! isset( $addons[ $this->_plugin->id ] ) ||
10922 ! is_array( $addons[ $this->_plugin->id ] ) ||
10923 0 === count( $addons[ $this->_plugin->id ] )
10924 ) {
10925 return false;
10926 }
10927
10928 return $addons[ $this->_plugin->id ];
10929 }
10930
10931 /**
10932 * Check if user has any
10933 *
10934 * @author Vova Feldman (@svovaf)
10935 * @since 1.1.6
10936 *
10937 * @return bool
10938 */
10939 function has_account_addons() {
10940 $addons = $this->get_account_addons();
10941
10942 return is_array( $addons ) && ( 0 < count( $addons ) );
10943 }
10944
10945
10946 /**
10947 * Get add-on by ID (from local data).
10948 *
10949 * @author Vova Feldman (@svovaf)
10950 * @since 1.0.6
10951 *
10952 * @param number $id
10953 *
10954 * @return FS_Plugin|false
10955 */
10956 function get_addon( $id ) {
10957 $this->_logger->entrance();
10958
10959 $addons = $this->get_addons();
10960
10961 if ( is_array( $addons ) ) {
10962 foreach ( $addons as $addon ) {
10963 if ( $id == $addon->id ) {
10964 return $addon;
10965 }
10966 }
10967 }
10968
10969 return false;
10970 }
10971
10972 /**
10973 * Get add-on by slug (from local data).
10974 *
10975 * @author Vova Feldman (@svovaf)
10976 * @since 1.0.6
10977 *
10978 * @param string $slug
10979 *
10980 * @param bool $flush
10981 *
10982 * @return FS_Plugin|false
10983 */
10984 function get_addon_by_slug( $slug, $flush = false ) {
10985 $this->_logger->entrance();
10986
10987 $addons = $this->get_addons( $flush );
10988
10989 if ( is_array( $addons ) ) {
10990 foreach ( $addons as $addon ) {
10991 if ( $slug === $addon->slug ) {
10992 return $addon;
10993 }
10994 }
10995 }
10996
10997 return false;
10998 }
10999
11000 /**
11001 * @var array<number,object[]> {
11002 * @key number Add-on ID.
11003 * @val object[] The add-on's plans and prices object.
11004 * }
11005 */
11006 private $plans_and_pricing_by_addon_id;
11007
11008 /**
11009 * @author Leo Fajardo (@leorw)
11010 * @since 2.3.0
11011 *
11012 * @return array<number,object[]> {
11013 * @key number Add-on ID.
11014 * @val object[] The add-on's plans and prices object.
11015 * }
11016 */
11017 function _get_addons_plans_and_pricing_map_by_id() {
11018 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
11019 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
11020
11021 $plans_and_pricing_by_addon_id = array();
11022 if ( $this->is_api_result_object( $result, 'addons' ) ) {
11023 foreach ( $result->addons as $addon ) {
11024 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
11025 }
11026 }
11027
11028 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
11029 }
11030
11031 return $this->plans_and_pricing_by_addon_id;
11032 }
11033
11034 /**
11035 * @author Leo Fajardo (@leorw)
11036 * @since 2.3.0
11037 *
11038 * @param number $addon_id
11039 * @param bool $is_installed
11040 *
11041 * @return array
11042 */
11043 function _get_addon_info( $addon_id, $is_installed ) {
11044 $addon = $this->get_addon( $addon_id );
11045
11046 if ( ! is_object( $addon ) ) {
11047 // Unexpected call.
11048 return array();
11049 }
11050
11051 $slug = $addon->slug;
11052
11053 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
11054
11055 if ( ! fs_is_network_admin() ) {
11056 // Get blog-level activated installations.
11057 $sites = self::maybe_get_entities_account_option( 'sites', array() );
11058 } else {
11059 $sites = null;
11060
11061 if ( $this->is_addon_activated( $addon_id ) &&
11062 $this->get_addon_instance( $addon_id )->is_network_active()
11063 ) {
11064 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
11065 // Get network-level activated installations.
11066 $sites = self::maybe_get_entities_account_option(
11067 'sites',
11068 array(),
11069 $addon_storage->network_install_blog_id
11070 );
11071 }
11072 }
11073 }
11074
11075 $addon_info = array(
11076 'is_connected' => false,
11077 'slug' => $slug,
11078 'title' => $addon->title,
11079 'is_whitelabeled' => $addon_storage->is_whitelabeled
11080 );
11081
11082 if ( ! $is_installed ) {
11083 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
11084
11085 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
11086 $has_paid_plan = false;
11087 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
11088
11089 if ( is_array( $plans ) && count( $plans ) > 0 ) {
11090 foreach ( $plans as $plan ) {
11091 if ( isset( $plan->pricing ) &&
11092 is_array( $plan->pricing ) &&
11093 count( $plan->pricing ) > 0
11094 ) {
11095 $has_paid_plan = true;
11096 break;
11097 }
11098 }
11099 }
11100
11101 $addon_info['has_paid_plan'] = $has_paid_plan;
11102 }
11103 }
11104
11105 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
11106 return $addon_info;
11107 }
11108
11109 $site = $sites[ $slug ];
11110
11111 $addon_info['is_connected'] = (
11112 ( $addon->parent_plugin_id == $this->get_id() ) &&
11113 is_object( $site ) &&
11114 FS_Site::is_valid_id( $site->id ) &&
11115 FS_User::is_valid_id( $site->user_id ) &&
11116 FS_Plugin_Plan::is_valid_id( $site->plan_id )
11117 );
11118
11119 if ( $addon_info['is_connected'] && $is_installed ) {
11120 return $addon_info;
11121 }
11122
11123 $addon_info['site'] = $site;
11124
11125 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
11126 if ( isset( $plugins_data[ $slug ] ) ) {
11127 $plugin_data = $plugins_data[ $slug ];
11128
11129 $addon_info['version'] = $plugin_data->version;
11130 }
11131
11132 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
11133 if ( isset( $all_plans[ $slug ] ) ) {
11134 $plans = $all_plans[ $slug ];
11135
11136 foreach ( $plans as $plan ) {
11137 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
11138 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
11139 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
11140 break;
11141 }
11142 }
11143 }
11144
11145 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
11146 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
11147 foreach ( $licenses[ $addon_id ] as $license ) {
11148 if ( $license->id == $site->license_id ) {
11149 $addon_info['license'] = $license;
11150 break;
11151 }
11152 }
11153 }
11154
11155 if ( isset( $addon_info['license'] ) ) {
11156 if ( isset( $addon_storage->subscriptions ) &&
11157 ! empty( $addon_storage->subscriptions )
11158 ) {
11159 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11160
11161 foreach ( $addon_subscriptions as $subscription ) {
11162 if ( $subscription->license_id == $site->license_id ) {
11163 $addon_info['subscription'] = $subscription;
11164 break;
11165 }
11166 }
11167 }
11168 }
11169
11170 return $addon_info;
11171 }
11172
11173 /**
11174 * @author Vova Feldman (@svovaf)
11175 * @since 2.0.0
11176 *
11177 * @param number $user_id
11178 *
11179 * @return FS_User
11180 */
11181 static function _get_user_by_id( $user_id ) {
11182 self::$_static_logger->entrance( "user_id = {$user_id}" );
11183
11184 $users = self::get_all_users();
11185
11186 if ( is_array( $users ) ) {
11187 if ( isset( $users[ $user_id ] ) &&
11188 $users[ $user_id ] instanceof FS_User &&
11189 $user_id == $users[ $user_id ]->id
11190 ) {
11191 return $users[ $user_id ];
11192 }
11193
11194 // If user wasn't found by the key, iterate over all the users collection.
11195 foreach ( $users as $user ) {
11196 /**
11197 * @var FS_User $user
11198 */
11199 if ( $user_id == $user->id ) {
11200 return $user;
11201 }
11202 }
11203 }
11204
11205 return null;
11206 }
11207
11208 /**
11209 * Checks if a Freemius user_id is associated with a super-admin.
11210 *
11211 * @author Vova Feldman (@svovaf)
11212 * @since 2.0.0
11213 *
11214 * @param number $user_id
11215 *
11216 * @return bool
11217 */
11218 private static function is_super_admin( $user_id ) {
11219 $is_super_admin = false;
11220
11221 $user = self::_get_user_by_id( $user_id );
11222
11223 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11224 self::require_pluggable_essentials();
11225
11226 $wp_user = get_user_by( 'email', $user->email );
11227
11228 if ( $wp_user instanceof WP_User ) {
11229 $super_admins = get_super_admins();
11230 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11231 }
11232 }
11233
11234 return $is_super_admin;
11235 }
11236
11237 #----------------------------------------------------------------------------------
11238 #region Plans & Licensing
11239 #----------------------------------------------------------------------------------
11240
11241 /**
11242 * Check if running premium plugin code.
11243 *
11244 * @author Vova Feldman (@svovaf)
11245 * @since 1.0.5
11246 *
11247 * @return bool
11248 */
11249 function is_premium() {
11250 /**
11251 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11252 * `register_constructor_hooks` method.
11253 *
11254 * @author Leo Fajardo (@leorw)
11255 * @since 2.2.3
11256 */
11257 return is_object( $this->_plugin ) ?
11258 $this->_plugin->is_premium :
11259 false;
11260 }
11261
11262 /**
11263 * Get site's plan ID.
11264 *
11265 * @author Vova Feldman (@svovaf)
11266 * @since 1.0.2
11267 *
11268 * @return number
11269 */
11270 function get_plan_id() {
11271 return $this->_site->plan_id;
11272 }
11273
11274 /**
11275 * Get site's plan title.
11276 *
11277 * @author Vova Feldman (@svovaf)
11278 * @since 1.0.2
11279 *
11280 * @return string
11281 */
11282 function get_plan_title() {
11283 $plan = $this->get_plan();
11284
11285 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11286 }
11287
11288 /**
11289 * Get site's plan name.
11290 *
11291 * @author Vova Feldman (@svovaf)
11292 * @since 2.0.0
11293 *
11294 * @return string
11295 */
11296 function get_plan_name() {
11297 $plan = $this->get_plan();
11298
11299 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11300 }
11301
11302 /**
11303 * @author Vova Feldman (@svovaf)
11304 * @since 1.0.9
11305 *
11306 * @return FS_Plugin_Plan|false
11307 */
11308 function get_plan() {
11309 if ( ! is_object( $this->_site ) ) {
11310 return false;
11311 }
11312
11313 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11314 $this->_get_plan_by_id( $this->_site->plan_id ) :
11315 false;
11316 }
11317
11318 /**
11319 * @author Vova Feldman (@svovaf)
11320 * @since 1.0.3
11321 *
11322 * @return bool
11323 */
11324 function is_trial() {
11325 $this->_logger->entrance();
11326
11327 if ( ! $this->is_registered( true ) || ! is_object( $this->_site ) ) {
11328 return false;
11329 }
11330
11331 return $this->_site->is_trial();
11332 }
11333
11334 /**
11335 * Check if currently in a trial with payment method (credit card or paypal).
11336 *
11337 * @author Vova Feldman (@svovaf)
11338 * @since 1.1.7
11339 *
11340 * @return bool
11341 */
11342 function is_paid_trial() {
11343 $this->_logger->entrance();
11344
11345 if ( ! $this->is_trial() ) {
11346 return false;
11347 }
11348
11349 if ( ! $this->has_active_valid_license() ) {
11350 return false;
11351 }
11352
11353 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11354 return false;
11355 }
11356
11357 /**
11358 * @var FS_Subscription $subscription
11359 */
11360 $subscription = $this->_get_subscription( $this->_license->id );
11361
11362 return ( is_object( $subscription ) && $subscription->is_active() );
11363 }
11364
11365 /**
11366 * Check if trial already utilized.
11367 *
11368 * @since 1.0.9
11369 *
11370 * @return bool
11371 */
11372 function is_trial_utilized() {
11373 $this->_logger->entrance();
11374
11375 if ( ! $this->is_registered() ) {
11376 return false;
11377 }
11378
11379 return $this->_site->is_trial_utilized();
11380 }
11381
11382 /**
11383 * Get trial plan information (if in trial).
11384 *
11385 * @author Vova Feldman (@svovaf)
11386 * @since 1.0.9
11387 *
11388 * @return bool|FS_Plugin_Plan
11389 */
11390 function get_trial_plan() {
11391 $this->_logger->entrance();
11392
11393 if ( ! $this->is_trial() ) {
11394 return false;
11395 }
11396
11397 // Try to load plan from local cache.
11398 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11399
11400 if ( ! is_object( $trial_plan ) ) {
11401 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11402
11403 /**
11404 * If managed to fetch the plan, add it to the plans collection.
11405 */
11406 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11407 if ( ! is_array( $this->_plans ) ) {
11408 $this->_plans = array();
11409 }
11410
11411 $this->_plans[] = $trial_plan;
11412 $this->_store_plans();
11413 }
11414 }
11415
11416 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11417 return $trial_plan;
11418 }
11419
11420 /**
11421 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11422 */
11423 $trial_plan = new FS_Plugin_Plan();
11424 $trial_plan->id = $this->_site->trial_plan_id;
11425 $trial_plan->name = 'pro';
11426 $trial_plan->title = 'Pro';
11427
11428 return $trial_plan;
11429 }
11430
11431 /**
11432 * Check if the user has an activate, non-expired license on current plugin's install.
11433 *
11434 * @since 1.0.9
11435 *
11436 * @return bool
11437 */
11438 function is_paying() {
11439 $this->_logger->entrance();
11440
11441 if ( ! $this->is_registered( true ) ) {
11442 return false;
11443 }
11444
11445 if ( ! $this->has_paid_plan() ) {
11446 return false;
11447 }
11448
11449 return (
11450 ! $this->is_trial() &&
11451 'free' !== $this->get_plan_name() &&
11452 $this->has_active_valid_license()
11453 );
11454 }
11455
11456 /**
11457 * @author Vova Feldman (@svovaf)
11458 * @since 1.0.4
11459 *
11460 * @return bool
11461 */
11462 function is_free_plan() {
11463 if ( ! $this->is_registered() ) {
11464 return true;
11465 }
11466
11467 if ( ! $this->has_paid_plan() ) {
11468 return true;
11469 }
11470
11471 return (
11472 'free' === $this->get_plan_name() ||
11473 ! $this->has_features_enabled_license()
11474 );
11475 }
11476
11477 /**
11478 * @author Vova Feldman (@svovaf)
11479 * @since 1.0.5
11480 *
11481 * @return bool
11482 */
11483 function _has_premium_license() {
11484 $this->_logger->entrance();
11485
11486 $premium_license = $this->_get_available_premium_license();
11487
11488 return ( false !== $premium_license );
11489 }
11490
11491 /**
11492 * Check if user has any licenses associated with the plugin (including expired or blocking).
11493 *
11494 * @author Vova Feldman (@svovaf)
11495 * @since 1.1.7.3
11496 *
11497 * @param bool $including_foreign
11498 *
11499 * @return bool
11500 */
11501 function has_any_license( $including_foreign = true ) {
11502 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11503 return false;
11504 }
11505
11506 if ( $including_foreign ) {
11507 return true;
11508 }
11509
11510 foreach ( $this->_licenses as $license ) {
11511 if ( $this->_user->id == $license->user_id ) {
11512 return true;
11513 }
11514 }
11515
11516 return false;
11517 }
11518
11519 /**
11520 * @author Vova Feldman (@svovaf)
11521 * @since 1.0.5
11522 *
11523 * @param bool|null $is_localhost
11524 *
11525 * @return FS_Plugin_License|false
11526 */
11527 function _get_available_premium_license( $is_localhost = null ) {
11528 $this->_logger->entrance();
11529
11530 $licenses = $this->get_available_premium_licenses( $is_localhost );
11531 if ( ! empty( $licenses ) ) {
11532 return $licenses[0];
11533 }
11534
11535 return false;
11536 }
11537
11538 /**
11539 * @author Vova Feldman (@svovaf)
11540 * @since 1.0.5
11541 *
11542 * @param bool|null $is_localhost
11543 *
11544 * @return FS_Plugin_License[]
11545 */
11546 function get_available_premium_licenses( $is_localhost = null ) {
11547 $this->_logger->entrance();
11548
11549 $licenses = array();
11550 if ( ! $this->has_paid_plan() ) {
11551 return $licenses;
11552 }
11553
11554 if ( is_array( $this->_licenses ) ) {
11555 foreach ( $this->_licenses as $license ) {
11556 if ( ! $license->can_activate( $is_localhost ) ) {
11557 continue;
11558 }
11559
11560 $licenses[] = $license;
11561 }
11562 }
11563
11564 return $licenses;
11565 }
11566
11567 /**
11568 * Sync local plugin plans with remote server.
11569 *
11570 * 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).
11571 *
11572 * @author Vova Feldman (@svovaf)
11573 * @since 1.0.5
11574 *
11575 * @return FS_Plugin_Plan[]|object
11576 */
11577 function _sync_plans() {
11578 $plans = $this->_fetch_plugin_plans();
11579
11580 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11581 $plans_map = array();
11582 foreach ( $plans as $plan ) {
11583 $plans_map[ $plan->id ] = true;
11584 }
11585
11586 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11587
11588 foreach ( $plans_ids_to_keep as $plan_id ) {
11589 if ( isset( $plans_map[ $plan_id ] ) ) {
11590 continue;
11591 }
11592
11593 $missing_plan = self::_get_plan_by_id( $plan_id, false );
11594
11595 if ( is_object( $missing_plan ) ) {
11596 $plans[] = $missing_plan;
11597 }
11598 }
11599
11600 $this->_plans = $plans;
11601 $this->_store_plans();
11602 }
11603
11604 $this->do_action( 'after_plans_sync', $plans );
11605
11606 return $this->_plans;
11607 }
11608
11609 /**
11610 * Check if specified plan exists locally. If not, fetch it and store it.
11611 *
11612 * @author Vova Feldman (@svovaf)
11613 * @since 2.0.0
11614 *
11615 * @param number $plan_id
11616 *
11617 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11618 */
11619 private function sync_plan_if_not_exist( $plan_id ) {
11620 $plan = self::_get_plan_by_id( $plan_id );
11621
11622 if ( is_object( $plan ) ) {
11623 // Plan already exists.
11624 return $plan;
11625 }
11626
11627 $plan = $this->fetch_plan_by_id( $plan_id );
11628
11629 if ( $plan instanceof FS_Plugin_Plan ) {
11630 $this->_plans[] = $plan;
11631 $this->_store_plans();
11632
11633 return $plan;
11634 }
11635
11636 return $plan;
11637 }
11638
11639 /**
11640 * Check if specified license exists locally. If not, fetch it and store it.
11641 *
11642 * @author Vova Feldman (@svovaf)
11643 * @since 2.0.0
11644 *
11645 * @param number $license_id
11646 * @param string $license_key
11647 *
11648 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11649 */
11650 private function sync_license_if_not_exist( $license_id, $license_key ) {
11651 $license = $this->_get_license_by_id( $license_id );
11652
11653 if ( is_object( $license ) ) {
11654 // License already exists.
11655 return $license;
11656 }
11657
11658 $license = $this->fetch_license_by_key( $license_id, $license_key );
11659
11660 if ( $license instanceof FS_Plugin_License ) {
11661 $this->_licenses[] = $license;
11662
11663 $this->set_license( $license );
11664
11665 $this->_store_licenses();
11666
11667 return $license;
11668 }
11669
11670 return $license;
11671 }
11672
11673 /**
11674 * Get a collection of unique plan IDs that are associated with any installs in the network.
11675 *
11676 * @author Leo Fajardo (@leorw)
11677 * @since 2.0.0
11678 *
11679 * @return number[]
11680 */
11681 private function get_plans_ids_associated_with_installs() {
11682 if ( ! is_multisite() ) {
11683 if ( ! is_object( $this->_site ) ||
11684 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11685 ) {
11686 return array();
11687 }
11688
11689 return array( $this->_site->plan_id );
11690 }
11691
11692 $plan_ids = array();
11693 $sites = self::get_sites();
11694 foreach ( $sites as $site ) {
11695 $blog_id = self::get_site_blog_id( $site );
11696 $install = $this->get_install_by_blog_id( $blog_id );
11697
11698 if ( ! is_object( $install ) ||
11699 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11700 ) {
11701 continue;
11702 }
11703
11704 $plan_ids[ $install->plan_id ] = true;
11705 }
11706
11707 return array_keys( $plan_ids );
11708 }
11709
11710 /**
11711 * Get a collection of unique license IDs that are associated with any installs in the network.
11712 *
11713 * @author Leo Fajardo (@leorw)
11714 * @since 2.0.0
11715 *
11716 * @return number[]
11717 */
11718 private function get_license_ids_associated_with_installs() {
11719 if ( ! $this->_is_network_active ) {
11720 if ( ! is_object( $this->_site ) ||
11721 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11722 ) {
11723 return array();
11724 }
11725
11726 return array( $this->_site->license_id );
11727 }
11728
11729 $license_ids = array();
11730 $sites = self::get_sites();
11731 foreach ( $sites as $site ) {
11732 $blog_id = self::get_site_blog_id( $site );
11733 $install = $this->get_install_by_blog_id( $blog_id );
11734
11735 if ( ! is_object( $install ) ||
11736 ! FS_Plugin_License::is_valid_id( $install->license_id )
11737 ) {
11738 continue;
11739 }
11740
11741 $license_ids[ $install->license_id ] = true;
11742 }
11743
11744 return array_keys( $license_ids );
11745 }
11746
11747 /**
11748 * @author Vova Feldman (@svovaf)
11749 * @since 1.0.5
11750 *
11751 * @param number $id
11752 *
11753 * @return FS_Plugin_Plan|false
11754 */
11755 function _get_plan_by_id( $id, $allow_sync = true ) {
11756 $this->_logger->entrance();
11757
11758 if ( $allow_sync && ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) ) {
11759 $this->_sync_plans();
11760 }
11761
11762 foreach ( $this->_plans as $plan ) {
11763 if ( $id == $plan->id ) {
11764 return $plan;
11765 }
11766 }
11767
11768 return false;
11769 }
11770
11771 /**
11772 * @author Vova Feldman (@svovaf)
11773 * @since 1.1.8.1
11774 *
11775 * @param string $name
11776 *
11777 * @return FS_Plugin_Plan|false
11778 */
11779 private function get_plan_by_name( $name ) {
11780 $this->_logger->entrance();
11781
11782 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11783 $this->_sync_plans();
11784 }
11785
11786 foreach ( $this->_plans as $plan ) {
11787 if ( $name == $plan->name ) {
11788 return $plan;
11789 }
11790 }
11791
11792 return false;
11793 }
11794
11795 /**
11796 * Sync local licenses with remote server.
11797 *
11798 * @author Vova Feldman (@svovaf)
11799 * @since 1.0.6
11800 *
11801 * @param number|bool $site_license_id
11802 * @param number|null $blog_id
11803 *
11804 * @return FS_Plugin_License[]|object
11805 */
11806 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
11807 $this->_logger->entrance();
11808
11809 $is_network_admin = fs_is_network_admin();
11810
11811 if ( $is_network_admin && is_null( $blog_id ) ) {
11812 $all_licenses = self::get_all_licenses( $this->_module_id );
11813 } else {
11814 $all_licenses = $this->get_user_licenses( $this->_user->id );
11815 }
11816
11817 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
11818
11819 $all_licenses_map = array();
11820 foreach ( $all_licenses as $license ) {
11821 $all_licenses_map[ $license->id ] = true;
11822 }
11823
11824 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
11825
11826 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
11827 $licenses_map = array();
11828 foreach ( $licenses as $license ) {
11829 $licenses_map[ $license->id ] = true;
11830 }
11831
11832 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
11833 // foreach ( $license_ids_to_keep as $license_id ) {
11834 // if ( isset( $licenses_map[ $license_id ] ) ) {
11835 // continue;
11836 // }
11837 //
11838 // $missing_license = self::_get_license_by_id( $license_id, false );
11839 // if ( is_object( $missing_license ) ) {
11840 // $licenses[] = $missing_license;
11841 // $licenses_map[ $missing_license->id ] = true;
11842 // }
11843 // }
11844
11845 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
11846
11847 foreach ( $user_license_ids as $key => $license_id ) {
11848 if ( ! isset( $licenses_map[ $license_id ] ) ) {
11849 // Remove access to licenses that no longer exist.
11850 unset( $user_license_ids[ $key ] );
11851 }
11852 }
11853
11854 if ( ! empty( $user_license_ids ) ) {
11855 foreach ( $licenses_map as $license_id => $value ) {
11856 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
11857 // Associate new licenses with the user who triggered the license syncing.
11858 $user_license_ids[] = $license_id;
11859 }
11860 }
11861
11862 $user_license_ids = array_unique( $user_license_ids );
11863 } else {
11864 $user_license_ids = array_keys( $licenses_map );
11865 }
11866
11867 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
11868 $user_licenses = array();
11869 foreach ( $licenses as $license ) {
11870 if ( ! in_array( $license->id, $user_license_ids ) ) {
11871 continue;
11872 }
11873
11874 $user_licenses[] = $license;
11875 }
11876
11877 $this->_licenses = $user_licenses;
11878 } else {
11879 $this->_licenses = $licenses;
11880 }
11881
11882 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
11883
11884 $this->_store_licenses( true, $this->_module_id, $licenses );
11885 }
11886
11887 // Update current license.
11888 if ( is_object( $this->_license ) ) {
11889 $license = $this->_get_license_by_id( $this->_license->id );
11890
11891 if ( is_object( $license ) ) {
11892 /**
11893 * `$license` can be `false` in case a user change action has just been completed and this method
11894 * has synced the `$this->_licenses` collection for the new user. In this case, the
11895 * `$this->_licenses` collection may have only the newly activated license that is associated with
11896 * the new user. `set_license` will eventually be called in the same request by the logic that
11897 * follows outside this method which will detect that the install's license has been updated, and
11898 * then `_update_site_license` will be called which in turn will call `set_license`.
11899 *
11900 * @author Leo Fajardo (@leorw)
11901 * @since 2.3.2
11902 */
11903 $this->set_license( $license );
11904 }
11905 }
11906
11907 return $this->_licenses;
11908 }
11909
11910 /**
11911 * @author Vova Feldman (@svovaf)
11912 * @since 1.0.5
11913 *
11914 * @param number $id
11915 * @param bool $sync_licenses
11916 *
11917 * @return FS_Plugin_License|false
11918 */
11919 function _get_license_by_id( $id, $sync_licenses = true ) {
11920 $this->_logger->entrance();
11921
11922 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
11923 return false;
11924 }
11925
11926 /**
11927 * When running from the network level admin and opted-in from the network,
11928 * check if the license exists in the network user licenses collection.
11929 *
11930 * @author Vova Feldman (@svovaf)
11931 * @since 2.0.0
11932 */
11933 if ( fs_is_network_admin() &&
11934 $this->is_network_registered() &&
11935 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
11936 ) {
11937 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
11938
11939 foreach ( $licenses as $license ) {
11940 if ( $id == $license->id ) {
11941 return $license;
11942 }
11943 }
11944 }
11945
11946 if ( ! $this->has_any_license() && $sync_licenses ) {
11947 $this->_sync_licenses( $id );
11948 }
11949
11950 if ( is_array( $this->_licenses ) ) {
11951 foreach ( $this->_licenses as $license ) {
11952 if ( $id == $license->id ) {
11953 return $license;
11954 }
11955 }
11956 }
11957
11958 return false;
11959 }
11960
11961 /**
11962 * 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.
11963 *
11964 * @author Vova Feldman (@svovaf)
11965 * @since 2.0.0
11966 *
11967 * @param number $id
11968 *
11969 * @return FS_Plugin_License
11970 */
11971 private function get_license_by_id( $id ) {
11972 $licenses = self::get_all_licenses( $this->_module_id );
11973
11974 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
11975 foreach ( $licenses as $license ) {
11976 if ( $id == $license->id ) {
11977 return $license;
11978 }
11979 }
11980 }
11981
11982 return null;
11983 }
11984
11985 /**
11986 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
11987 *
11988 * @author Vova Feldman (@svovaf)
11989 * @since 2.0.0
11990 *
11991 * @return \FS_Plugin_License|mixed
11992 */
11993 private function sync_site_license() {
11994 $api = $this->get_api_user_scope();
11995
11996 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
11997
11998 if ( ! $this->is_api_result_entity( $result ) ) {
11999 return $result;
12000 }
12001
12002 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
12003 $this->_store_licenses();
12004
12005 return $license;
12006 }
12007
12008 /**
12009 * Get all user's available licenses for the current module.
12010 *
12011 * @author Vova Feldman (@svovaf)
12012 * @since 2.0.0
12013 *
12014 * @param number $user_id
12015 *
12016 * @return FS_Plugin_License[]
12017 */
12018 private function get_user_licenses( $user_id ) {
12019 $all_licenses = self::get_all_licenses( $this->_module_id );
12020 if ( empty( $all_licenses ) ) {
12021 return array();
12022 }
12023
12024 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
12025 if ( empty( $user_license_ids ) ) {
12026 return array();
12027 }
12028
12029 $licenses = array();
12030 foreach ( $all_licenses as $license ) {
12031 if ( in_array( $license->id, $user_license_ids ) ) {
12032 $licenses[] = $license;
12033 }
12034 }
12035
12036 return $licenses;
12037 }
12038
12039 /**
12040 * Checks if the context license is network activated except on the given blog ID.
12041 *
12042 * @author Vova Feldman (@svovaf)
12043 * @since 2.0.0
12044 *
12045 * @param int $except_blog_id
12046 *
12047 * @return bool
12048 */
12049 private function is_license_network_active( $except_blog_id = 0 ) {
12050 $this->_logger->entrance();
12051
12052 if ( ! is_object( $this->_license ) ) {
12053 return false;
12054 }
12055
12056 $sites = self::get_sites();
12057
12058 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
12059 // There are more sites than the number of activations, so license cannot be network activated.
12060 return false;
12061 }
12062
12063 foreach ( $sites as $site ) {
12064 $blog_id = self::get_site_blog_id( $site );
12065
12066 if ( $except_blog_id == $blog_id ) {
12067 // Skip excluded blog.
12068 continue;
12069 }
12070
12071 $install = $this->get_install_by_blog_id( $blog_id );
12072
12073 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
12074 return false;
12075 }
12076 }
12077
12078 return true;
12079 }
12080
12081 /**
12082 * 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.
12083 *
12084 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
12085 *
12086 * @author Vova Feldman (@svovaf)
12087 * @since 2.0.0
12088 *
12089 * @param \FS_User $user
12090 * @param \FS_Plugin_License $license
12091 *
12092 * @return bool
12093 */
12094 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
12095 $this->_logger->entrance();
12096
12097 $result = $this->can_activate_license_on_network( $license );
12098
12099 if ( false === $result ) {
12100 return false;
12101 }
12102
12103 $installs_without_license = $result['installs'];
12104 if ( ! empty( $installs_without_license ) ) {
12105 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
12106 }
12107
12108 $disconnected_site_ids = $result['sites'];
12109 if ( ! empty( $disconnected_site_ids ) ) {
12110 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
12111 }
12112
12113 $this->link_license_2_user( $license->id, $user->id );
12114
12115 // Sync license after activations.
12116 $license->activated += $result['production_count'];
12117 $license->activated_local += $result['localhost_count'];
12118
12119 // $this->_store_licenses()
12120
12121 return true;
12122 }
12123
12124 /**
12125 * Checks if the given license can be activated on the whole network.
12126 *
12127 * @author Vova Feldman (@svovaf)
12128 * @since 2.0.0
12129 *
12130 * @param \FS_Plugin_License $license
12131 *
12132 * @return false|array {
12133 * @type array[int]FS_Site $installs Blog ID to install map.
12134 * @type int[] $sites Non-connected blog IDs.
12135 * @type int $production_count Production sites count.
12136 * @type int $localhost_count Production sites count.
12137 * }
12138 */
12139 private function can_activate_license_on_network( FS_Plugin_License $license ) {
12140 $sites = self::get_sites();
12141
12142 $production_count = 0;
12143 $localhost_count = 0;
12144
12145 $installs_without_license = array();
12146 $disconnected_site_ids = array();
12147
12148 foreach ( $sites as $site ) {
12149 $blog_id = self::get_site_blog_id( $site );
12150 $install = $this->get_install_by_blog_id( $blog_id );
12151
12152 if ( is_object( $install ) ) {
12153 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
12154 // License already activated on the install.
12155 continue;
12156 }
12157
12158 $url = $install->url;
12159
12160 $installs_without_license[ $blog_id ] = $install;
12161 } else {
12162 $url = is_object( $site ) ?
12163 $site->siteurl :
12164 self::get_unfiltered_site_url( $blog_id );
12165
12166 $disconnected_site_ids[] = $blog_id;
12167 }
12168
12169 if ( FS_Site::is_localhost_by_address( $url ) ) {
12170 $localhost_count ++;
12171 } else {
12172 $production_count ++;
12173 }
12174 }
12175
12176 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12177 return false;
12178 }
12179
12180 return array(
12181 'installs' => $installs_without_license,
12182 'sites' => $disconnected_site_ids,
12183 'production_count' => $production_count,
12184 'localhost_count' => $localhost_count,
12185 );
12186 }
12187
12188 /**
12189 * Activate a given license on a collection of installs.
12190 *
12191 * @author Vova Feldman (@svovaf)
12192 * @since 2.0.0
12193 *
12194 * @param \FS_User $user
12195 * @param string $license_key
12196 * @param array $blog_2_install_map {
12197 * @key int Blog ID.
12198 * @value FS_Site Blog's associated install.
12199 * }
12200 *
12201 * @return mixed|true
12202 */
12203 private function activate_license_on_many_installs(
12204 FS_User $user,
12205 $license_key,
12206 array $blog_2_install_map
12207 ) {
12208 $params = array(
12209 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12210 );
12211
12212 $install_2_blog_map = array();
12213 foreach ( $blog_2_install_map as $blog_id => $install ) {
12214 $params[] = array( 'id' => $install->id, 'url' => $install->url );
12215
12216 $install_2_blog_map[ $install->id ] = $blog_id;
12217 }
12218
12219 $result = $this->get_api_user_scope_by_user( $user )->call(
12220 "plugins/{$this->_plugin->id}/installs.json",
12221 'PUT',
12222 $params
12223 );
12224
12225 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12226 return $result;
12227 }
12228
12229 foreach ( $result->installs as $r_install ) {
12230 $install = new FS_Site( $r_install );
12231 $install->is_disconnected = false;
12232
12233 // Update install.
12234 $this->_store_site(
12235 true,
12236 $install_2_blog_map[ $r_install->id ],
12237 $install
12238 );
12239 }
12240
12241 return true;
12242 }
12243
12244 /**
12245 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12246 *
12247 * @author Vova Feldman (@svovaf)
12248 * @since 2.3.1
12249 *
12250 * @param \FS_User $user
12251 * @param string $license_key
12252 *
12253 * @return true|mixed True if successful, otherwise, the API result.
12254 */
12255 private function activate_license_on_site( FS_User $user, $license_key ) {
12256 return $this->activate_license_on_many_sites( $user, $license_key );
12257 }
12258
12259 /**
12260 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12261 *
12262 * @author Vova Feldman (@svovaf)
12263 * @since 2.0.0
12264 *
12265 * @param \FS_User $user
12266 * @param string $license_key
12267 * @param int[] $site_ids
12268 *
12269 * @return true|mixed True if successful, otherwise, the API result.
12270 */
12271 private function activate_license_on_many_sites(
12272 FS_User $user,
12273 $license_key,
12274 array $site_ids = array()
12275 ) {
12276 $sites = array();
12277 foreach ( $site_ids as $site_id ) {
12278 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12279 }
12280
12281 // Install the plugin.
12282 $result = $this->create_installs_with_user(
12283 $user,
12284 $license_key,
12285 false,
12286 $sites,
12287 false,
12288 true
12289 );
12290
12291 if ( ! $this->is_api_result_entity( $result ) &&
12292 ! $this->is_api_result_object( $result, 'installs' )
12293 ) {
12294 return $result;
12295 }
12296
12297 $installs = array();
12298
12299 if ( $this->is_api_result_entity( $result ) ) {
12300 $install = new FS_Site( $result );
12301
12302 $this->_user = $user;
12303
12304 $this->_store_site( true, null, $install );
12305
12306 $this->_site = $install;
12307
12308 $this->reset_anonymous_mode();
12309 } else {
12310 foreach ( $result->installs as $install ) {
12311 $installs[] = new FS_Site( $install );
12312 }
12313
12314 // Map site addresses to their blog IDs.
12315 $address_to_blog_map = $this->get_address_to_blog_map();
12316
12317 $first_blog_id = null;
12318
12319 foreach ( $installs as $install ) {
12320 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12321 $blog_id = $address_to_blog_map[ $address ];
12322
12323 $this->_store_site( true, $blog_id, $install );
12324
12325 $this->reset_anonymous_mode( $blog_id );
12326
12327 if ( is_null( $first_blog_id ) ) {
12328 $first_blog_id = $blog_id;
12329 }
12330 }
12331
12332 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12333 $this->_storage->network_install_blog_id = $first_blog_id;
12334 }
12335 }
12336
12337 return true;
12338 }
12339
12340 /**
12341 * Sync site's license with user licenses.
12342 *
12343 * @author Vova Feldman (@svovaf)
12344 * @since 1.0.6
12345 *
12346 * @param FS_Plugin_License|null $new_license
12347 *
12348 * @return FS_Plugin_License|null
12349 */
12350 function _update_site_license( $new_license ) {
12351 $this->_logger->entrance();
12352
12353 /**
12354 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12355 * accordingly so that it will also handle the case when an ownership change is done via license
12356 * activation.
12357 *
12358 * @author Leo Fajardo (@leorw)
12359 * @since 2.3.2
12360 */
12361 $this->set_license( $new_license );
12362
12363 if ( ! is_object( $new_license ) ) {
12364 $this->_site->license_id = null;
12365 $this->_sync_site_subscription( null );
12366
12367 return $this->_license;
12368 }
12369
12370 $this->_site->license_id = $this->_license->id;
12371
12372 if ( ! is_array( $this->_licenses ) ) {
12373 $this->_licenses = array();
12374 }
12375
12376 $is_license_found = false;
12377 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12378 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12379 $this->_licenses[ $i ] = $new_license;
12380
12381 $is_license_found = true;
12382 break;
12383 }
12384 }
12385
12386 // If new license just append.
12387 if ( ! $is_license_found ) {
12388 $this->_licenses[] = $new_license;
12389 }
12390
12391 $this->_sync_site_subscription( $new_license );
12392
12393 return $this->_license;
12394 }
12395
12396 /**
12397 * @author Vova Feldman (@svovaf)
12398 * @since 2.3.1
12399 *
12400 * @param \FS_Plugin_License $license
12401 */
12402 private function set_license( $license = null ) {
12403 $this->_license = $license;
12404
12405 $this->maybe_update_whitelabel_flag( $license );
12406 }
12407
12408 /**
12409 * @author Leo Fajardo (@leorw)
12410 * @since 2.3.1
12411 *
12412 * @param FS_Plugin_License $license
12413 */
12414 private function maybe_update_whitelabel_flag( $license ) {
12415 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12416 $this->_storage->is_whitelabeled :
12417 false;
12418
12419 if ( is_object( $license ) ) {
12420 $license_user = self::_get_user_by_id( $license->user_id );
12421
12422 if ( ! is_object( $license_user ) ) {
12423 // If foreign license, do not update the `is_whitelabeled` flag.
12424 return;
12425 }
12426
12427 if ( $this->is_addon() ) {
12428 /**
12429 * Store the last license data to the parent's storage since it's needed only when showing the
12430 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12431 * iterate over the add-ons just to get the last license data.
12432 */
12433 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12434 } else {
12435 $this->store_last_activated_license_data( $license );
12436 }
12437
12438 if ( $license->is_whitelabeled ) {
12439 // Activated a developer license, data should be hidden.
12440 $is_whitelabeled = true;
12441 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12442 // The account owner activated a regular license key, no need to hide the data.
12443 $is_whitelabeled = false;
12444 }
12445 }
12446
12447 $this->_storage->is_whitelabeled = $is_whitelabeled;
12448
12449 // Reset the whitelabeled status after update.
12450 $this->is_whitelabeled = null;
12451 if ( $this->is_addon() ) {
12452 $parent_fs = $this->get_parent_instance();
12453
12454 if ( is_object( $parent_fs ) ) {
12455 $parent_fs->is_whitelabeled = null;
12456 }
12457 }
12458 }
12459
12460 /**
12461 * @author Leo Fajardo (@leorw)
12462 * @since 2.3.1
12463 *
12464 * @param FS_Plugin_License $license
12465 * @param FS_User $license_user
12466 */
12467 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12468 if ( ! is_object( $license_user ) ) {
12469 $this->_storage->last_license_key = md5( $license->secret_key );
12470 $this->_storage->last_license_user_id = null;
12471 } else {
12472 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12473 $this->_storage->last_license_user_id = $license_user->id;
12474 }
12475 }
12476
12477 /**
12478 * @author Leo Fajardo (@leorw)
12479 * @since 2.3.1
12480 *
12481 * @param bool $ignore_data_debug_mode
12482 *
12483 * @return bool
12484 */
12485 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12486 if ( true !== $this->_storage->is_whitelabeled ) {
12487 return false;
12488 } else if ( $ignore_data_debug_mode ) {
12489 return true;
12490 }
12491
12492 $fs = $this->is_addon() ?
12493 $this->get_parent_instance() :
12494 $this;
12495
12496 return ! $fs->is_data_debug_mode();
12497 }
12498
12499 /**
12500 * @author Leo Fajardo (@leorw)
12501 * @since 2.3.1
12502 *
12503 * @return number
12504 */
12505 function get_last_license_user_id() {
12506 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12507 $this->_storage->last_license_user_id :
12508 null;
12509 }
12510
12511 /**
12512 * @author Leo Fajardo (@leorw)
12513 * @since 2.3.1
12514 *
12515 * @param int $blog_id
12516 * @param bool $ignore_data_debug_mode
12517 *
12518 * @return bool
12519 */
12520 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12521 if ( ! is_null( $blog_id ) ) {
12522 $this->switch_to_blog( $blog_id );
12523 }
12524
12525 if ( ! is_null( $this->is_whitelabeled ) ) {
12526 $is_whitelabeled = $this->is_whitelabeled;
12527 } else {
12528 $is_whitelabeled = false;
12529
12530 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12531
12532 if ( ! $this->has_addons() ) {
12533 $is_whitelabeled = $is_whitelabeled_flag;
12534 } else if ( $is_whitelabeled_flag ) {
12535 $is_whitelabeled = true;
12536 } else {
12537 if ( $this->is_registered() || $this->is_premium() ) {
12538 $addon_ids = $this->get_updated_account_addons();
12539 } else {
12540 $addons = self::get_all_addons();
12541
12542 $plugin_addons = isset( $addons[ $this->_plugin->id ] ) ?
12543 $addons[ $this->_plugin->id ] :
12544 array();
12545
12546 $addon_ids = array();
12547 foreach ( $plugin_addons as $addon ) {
12548 $addon_ids[] = $addon->id;
12549 }
12550 }
12551
12552 $installed_addons = $this->get_installed_addons();
12553 foreach ( $installed_addons as $fs_addon ) {
12554 $addon_ids[] = $fs_addon->get_id();
12555 }
12556
12557 if ( ! empty( $addon_ids ) ) {
12558 $addon_ids = array_unique( $addon_ids );
12559
12560 $is_network_level = (
12561 fs_is_network_admin() &&
12562 $this->is_network_active()
12563 );
12564
12565 foreach ( $addon_ids as $addon_id ) {
12566 $addon = $this->get_addon( $addon_id );
12567
12568 if ( ! is_object( $addon ) ) {
12569 continue;
12570 }
12571
12572 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12573 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12574 self::get_addon_instance( $addon_id ) :
12575 null;
12576
12577 $was_addon_network_activated = false;
12578
12579 if ( is_object( $fs_addon ) ) {
12580 $was_addon_network_activated = $fs_addon->is_network_active();
12581 } else if ( $is_network_level ) {
12582 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12583 }
12584
12585 $network_delegated_connection = (
12586 $was_addon_network_activated &&
12587 $addon_storage->get( 'is_delegated_connection', false, true )
12588 );
12589
12590 if (
12591 $is_network_level &&
12592 ( ! $was_addon_network_activated || $network_delegated_connection )
12593 ) {
12594 $sites = self::get_sites();
12595
12596 /**
12597 * If in network admin area and the add-on was not network-activated or network-activated
12598 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12599 */
12600 foreach ( $sites as $site ) {
12601 $site_info = $this->get_site_info( $site );
12602
12603 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12604 $is_whitelabeled = true;
12605 break;
12606 }
12607 }
12608
12609 if ( $is_whitelabeled ) {
12610 break;
12611 }
12612 } else {
12613 /**
12614 * This will be executed when any of the following is met:
12615 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12616 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12617 * 3. Add-on was not network-activated and in site admin area.
12618 */
12619 if ( true === $addon_storage->is_whitelabeled ) {
12620 $is_whitelabeled = true;
12621 break;
12622 }
12623 }
12624 }
12625 }
12626 }
12627
12628 $this->is_whitelabeled = $is_whitelabeled;
12629
12630 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12631 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12632 }
12633
12634 if ( ! is_null( $blog_id ) ) {
12635 $this->restore_current_blog();
12636 }
12637 }
12638
12639 return (
12640 $is_whitelabeled &&
12641 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12642 );
12643 }
12644
12645 /**
12646 * Sync site's subscription.
12647 *
12648 * @author Vova Feldman (@svovaf)
12649 * @since 1.0.9
12650 *
12651 * @param FS_Plugin_License|null $license
12652 *
12653 * @return bool|\FS_Subscription
12654 */
12655 private function _sync_site_subscription( $license ) {
12656 if ( ! is_object( $license ) ) {
12657 $this->delete_unused_subscriptions();
12658
12659 return false;
12660 }
12661
12662 // Load subscription details if not lifetime.
12663 $subscription = $license->is_lifetime() ?
12664 false :
12665 $this->_fetch_site_license_subscription();
12666
12667 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12668 $this->store_subscription( $subscription );
12669 } else {
12670 $this->delete_unused_subscriptions();
12671 }
12672
12673 return $subscription;
12674 }
12675
12676 /**
12677 * @author Vova Feldman (@svovaf)
12678 * @since 1.0.6
12679 *
12680 * @return bool|\FS_Plugin_License
12681 */
12682 function _get_license() {
12683 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12684 return $this->_license;
12685 }
12686
12687 return $this->_get_available_premium_license();
12688 }
12689
12690 /**
12691 * @param number $license_id
12692 *
12693 * @return null|\FS_Subscription
12694 */
12695 function _get_subscription( $license_id ) {
12696 if ( ! isset( $this->_storage->subscriptions ) ||
12697 empty( $this->_storage->subscriptions )
12698 ) {
12699 return null;
12700 }
12701
12702 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12703 if ( $subscription->license_id == $license_id ) {
12704 return $subscription;
12705 }
12706 }
12707
12708 return null;
12709 }
12710
12711 /**
12712 * @author Leo Fajardo (@leorw)
12713 * @since 2.0.0
12714 *
12715 * @param FS_Subscription $subscription
12716 */
12717 function store_subscription( FS_Subscription $subscription ) {
12718 if ( ! isset( $this->_storage->subscriptions ) ) {
12719 $this->_storage->subscriptions = array();
12720 }
12721
12722 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12723 $this->_storage->subscriptions = array( $subscription );
12724
12725 return;
12726 }
12727
12728 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
12729
12730 $updated_subscription = false;
12731 foreach ( $subscriptions as $key => $existing_subscription ) {
12732 if ( $existing_subscription->id == $subscription->id ) {
12733 $subscriptions[ $key ] = $subscription;
12734 $updated_subscription = true;
12735 break;
12736 }
12737 }
12738
12739 if ( ! $updated_subscription ) {
12740 $subscriptions[] = $subscription;
12741 }
12742
12743 $this->_storage->subscriptions = $subscriptions;
12744 }
12745
12746 /**
12747 * @author Leo Fajardo (@leorw)
12748 * @since 2.0.0
12749 */
12750 function delete_unused_subscriptions() {
12751 if ( ! isset( $this->_storage->subscriptions ) ||
12752 empty( $this->_storage->subscriptions ) ||
12753 // Clean up only if there are already at least 3 subscriptions.
12754 ( count( $this->_storage->subscriptions ) < 3 )
12755 ) {
12756 return;
12757 }
12758
12759 if ( ! is_multisite() ) {
12760 // If not multisite, there should only be 1 subscription, so just clear the array.
12761 $this->_storage->subscriptions = array();
12762
12763 return;
12764 }
12765
12766 $subscriptions_to_keep_by_license_id_map = array();
12767 $sites = self::get_sites();
12768 foreach ( $sites as $site ) {
12769 $blog_id = self::get_site_blog_id( $site );
12770 $install = $this->get_install_by_blog_id( $blog_id );
12771
12772 if ( ! is_object( $install ) ||
12773 ! FS_Plugin_License::is_valid_id( $install->license_id )
12774 ) {
12775 continue;
12776 }
12777
12778 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
12779 }
12780
12781 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
12782 $this->_storage->subscriptions = array();
12783
12784 return;
12785 }
12786
12787 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
12788 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
12789 unset( $this->_storage->subscriptions[ $key ] );
12790 }
12791 }
12792 }
12793
12794 /**
12795 * @author Vova Feldman (@svovaf)
12796 * @since 1.0.2
12797 *
12798 * @param string $plan Plan name
12799 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12800 *
12801 * @return bool
12802 */
12803 function is_plan( $plan, $exact = false ) {
12804 $this->_logger->entrance();
12805
12806 if ( ! $this->is_registered() ) {
12807 return false;
12808 }
12809
12810 $plan = strtolower( $plan );
12811
12812 $current_plan_name = $this->get_plan_name();
12813
12814 if ( $current_plan_name === $plan ) {
12815 // Exact plan.
12816 return true;
12817 } else if ( $exact ) {
12818 // Required exact, but plans are different.
12819 return false;
12820 }
12821
12822 $current_plan_order = - 1;
12823 $required_plan_order = PHP_INT_MAX;
12824 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12825 if ( $plan === $this->_plans[ $i ]->name ) {
12826 $required_plan_order = $i;
12827 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
12828 $current_plan_order = $i;
12829 }
12830 }
12831
12832 return ( $current_plan_order > $required_plan_order );
12833 }
12834
12835 /**
12836 * Check if module has only one plan.
12837 *
12838 * @author Vova Feldman (@svovaf)
12839 * @since 1.2.1.7
12840 *
12841 * @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.
12842 *
12843 * @return bool
12844 */
12845 function is_single_plan( $double_check = false ) {
12846 $this->_logger->entrance();
12847
12848 if ( ! $this->is_registered() ||
12849 ! is_array( $this->_plans ) ||
12850 0 === count( $this->_plans )
12851 ) {
12852 return true;
12853 }
12854
12855 $has_free_plan = $this->has_free_plan();
12856
12857 if ( ! $has_free_plan && $double_check ) {
12858 foreach ( $this->_plans as $plan ) {
12859 if ( $plan->is_free() ) {
12860 $has_free_plan = true;
12861 break;
12862 }
12863 }
12864 }
12865
12866 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
12867 }
12868
12869 /**
12870 * Check if plan based on trial. If not in trial mode, should return false.
12871 *
12872 * @since 1.0.9
12873 *
12874 * @param string $plan Plan name
12875 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12876 *
12877 * @return bool
12878 */
12879 function is_trial_plan( $plan, $exact = false ) {
12880 $this->_logger->entrance();
12881
12882 if ( ! $this->is_registered() ) {
12883 return false;
12884 }
12885
12886 if ( ! $this->is_trial() ) {
12887 return false;
12888 }
12889
12890 $trial_plan = $this->get_trial_plan();
12891
12892 if ( $trial_plan->name === $plan ) {
12893 // Exact plan.
12894 return true;
12895 } else if ( $exact ) {
12896 // Required exact, but plans are different.
12897 return false;
12898 }
12899
12900 $current_plan_order = - 1;
12901 $required_plan_order = - 1;
12902 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12903 if ( $plan === $this->_plans[ $i ]->name ) {
12904 $required_plan_order = $i;
12905 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
12906 $current_plan_order = $i;
12907 }
12908 }
12909
12910 return ( $current_plan_order > $required_plan_order );
12911 }
12912
12913 /**
12914 * Check if plugin has any paid plans.
12915 *
12916 * @author Vova Feldman (@svovaf)
12917 * @since 1.0.7
12918 *
12919 * @return bool
12920 */
12921 function has_paid_plan() {
12922 return $this->_has_paid_plans ||
12923 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
12924 }
12925
12926 /**
12927 * Check if plugin has any plan with a trail.
12928 *
12929 * @author Vova Feldman (@svovaf)
12930 * @since 1.0.9
12931 *
12932 * @return bool
12933 */
12934 function has_trial_plan() {
12935 /**
12936 * @author Vova Feldman(@svovaf)
12937 * @since 1.2.1.5
12938 *
12939 * Allow setting a trial from the SDK without calling the API.
12940 * But, if the user did opt-in, continue using the real data from the API.
12941 */
12942 if ( $this->_trial_days >= 0 ) {
12943 return true;
12944 }
12945
12946 return $this->_storage->get( 'has_trial_plan', false );
12947 }
12948
12949 /**
12950 * Check if plugin has any free plan, or is it premium only.
12951 *
12952 * Note: If no plans configured, assume plugin is free.
12953 *
12954 * @author Vova Feldman (@svovaf)
12955 * @since 1.0.7
12956 *
12957 * @return bool
12958 */
12959 function has_free_plan() {
12960 return ! $this->is_only_premium();
12961 }
12962
12963 /**
12964 * Displays a license activation dialog box when the user clicks on the "Activate License"
12965 * or "Change License" link on the plugins
12966 * page.
12967 *
12968 * @author Leo Fajardo (@leorw)
12969 * @since 1.1.9
12970 */
12971 function _add_license_activation_dialog_box() {
12972 $vars = array(
12973 'id' => $this->_module_id,
12974 );
12975
12976 fs_require_template( 'forms/license-activation.php', $vars );
12977 fs_require_template( 'forms/resend-key.php', $vars );
12978 }
12979
12980 /**
12981 * Displays an email address update dialog box when the user clicks on the email address "Edit" button on the "Account" page.
12982 *
12983 * @author Leo Fajardo (@leorw)
12984 * @since 2.5.0
12985 */
12986 function _add_email_address_update_dialog_box() {
12987 $vars = array( 'id' => $this->_module_id );
12988
12989 fs_require_template( 'forms/email-address-update.php', $vars );
12990 }
12991
12992 /**
12993 * @author Leo Fajardo (@leorw)
12994 * @since 2.5.0
12995 */
12996 function _add_email_address_update_option() {
12997 if ( ! $this->should_handle_user_change() ) {
12998 return;
12999 }
13000
13001 // Add email address update AJAX handler.
13002 $this->add_ajax_action( 'update_email_address', array( &$this, '_email_address_update_ajax_handler' ) );
13003 }
13004
13005 /**
13006 * @author Leo Fajardo (@leorw)
13007 * @since 2.5.0
13008 */
13009 function _email_address_update_ajax_handler() {
13010 $this->check_ajax_referer( 'update_email_address' );
13011
13012 $new_email_address = fs_request_get( 'email_address' );
13013 $transfer_type = fs_request_get( 'transfer_type' );
13014
13015 $result = $this->update_email( $new_email_address );
13016
13017 if ( ! FS_Api::is_api_error( $result ) ) {
13018 self::shoot_ajax_success();
13019 }
13020
13021 $error = '';
13022
13023 if ( FS_Api::is_api_error_object( $result ) ) {
13024 switch ( $result->error->code ) {
13025 case 'user_exist':
13026 case 'account_verification_required':
13027 $error = array(
13028 'code' => 'change_ownership',
13029 'url' => $this->get_account_url( 'change_owner', array(
13030 'state' => 'init',
13031 'candidate_email' => $new_email_address,
13032 'transfer_type' => $transfer_type,
13033 ) ),
13034 );
13035
13036 break;
13037 }
13038 }
13039
13040 if ( empty( $error ) ) {
13041 $error = is_object( $result ) ?
13042 var_export( $result->error, true ) :
13043 $result;
13044 }
13045
13046 self::shoot_ajax_failure( $error );
13047 }
13048
13049 /**
13050 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
13051 *
13052 * @author Leo Fajardo (@leorw)
13053 * @since 2.3.2
13054 *
13055 * @return number[]
13056 */
13057 function get_installs_ids_with_foreign_licenses() {
13058 $installs = array();
13059
13060 if (
13061 is_object( $this->_license ) &&
13062 $this->_site->user_id != $this->_license->user_id
13063 ) {
13064 $installs[] = $this->_site->id;
13065 }
13066
13067 /**
13068 * Also try to get foreign licenses for the context product's add-ons.
13069 */
13070 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
13071
13072 foreach ( $installs_by_slug_map as $slug => $install_info ) {
13073 if ( $slug == $this->get_slug() ) {
13074 continue;
13075 }
13076
13077 $install = $install_info['install'];
13078 $license = $install_info['license'];
13079
13080 if (
13081 is_object( $license ) &&
13082 $install->user_id != $license->user_id
13083 ) {
13084 $installs[] = $install->id;
13085 }
13086 }
13087
13088 return $installs;
13089 }
13090
13091 /**
13092 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
13093 *
13094 * @author Leo Fajardo (@leorw)
13095 * @since 2.3.2
13096 *
13097 * @param number[] $install_ids
13098 */
13099 function _add_user_change_dialog_box( $install_ids ) {
13100 $vars = array(
13101 'id' => $this->_module_id,
13102 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
13103 );
13104
13105 fs_require_template( 'forms/user-change.php', $vars );
13106 }
13107
13108 /**
13109 * @author Leo Fajardo (@leorw)
13110 * @since 2.3.1
13111 */
13112 function _add_data_debug_mode_dialog_box() {
13113 $vars = array(
13114 'id' => $this->_module_id,
13115 );
13116
13117 fs_require_template( 'forms/data-debug-mode.php', $vars );
13118 }
13119
13120 /**
13121 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
13122 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
13123 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
13124 * is only activated on a single production site.
13125 *
13126 * @author Leo Fajardo (@leorw)
13127 * @since 2.2.1
13128 *
13129 * @param bool $is_license_deactivation
13130 *
13131 * @return array
13132 */
13133 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
13134 if ( fs_is_network_admin() ) {
13135 // Subscription cancellation dialog box is currently not supported for multisite networks.
13136 return array();
13137 }
13138
13139 if ( $this->is_whitelabeled() ) {
13140 return array();
13141 }
13142
13143 $license = $this->_get_license();
13144
13145 /**
13146 * 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.
13147 *
13148 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
13149 * @since 2.2.1
13150 */
13151 if ( ! is_object( $license ) ||
13152 $license->is_lifetime() ||
13153 ( ! $license->is_single_site() && $license->activated > 1 )
13154 ) {
13155 return array();
13156 }
13157
13158 /**
13159 * @var FS_Subscription $subscription
13160 */
13161 $subscription = $this->_get_subscription( $license->id );
13162 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
13163 return array();
13164 }
13165
13166 return array(
13167 'id' => $this->_module_id,
13168 'license' => $license,
13169 'has_trial' => $this->is_paid_trial(),
13170 'is_license_deactivation' => $is_license_deactivation,
13171 );
13172 }
13173
13174 /**
13175 * @author Leo Fajardo (@leorw)
13176 * @since 2.0.2
13177 */
13178 function _add_premium_version_upgrade_selection_dialog_box() {
13179 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
13180 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
13181 return;
13182 }
13183
13184 $vars = array(
13185 'id' => $this->_module_id,
13186 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
13187 $modules_update->response[ $this->_plugin_basename ]->new_version :
13188 $modules_update->response[ $this->_plugin_basename ]['new_version']
13189 );
13190
13191 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13192 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13193 }
13194
13195 /**
13196 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13197 * page.
13198 *
13199 * @author Leo Fajardo (@leorw)
13200 * @since 1.2.1.5
13201 */
13202 function _add_optout_dialog() {
13203 if ( $this->is_theme() ) {
13204 $vars = null;
13205 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13206 }
13207
13208 $vars = array( 'id' => $this->_module_id );
13209 fs_require_template( 'forms/optout.php', $vars );
13210 }
13211
13212 /**
13213 * Prepare page to include all required UI and logic for the license activation dialog.
13214 *
13215 * @author Vova Feldman (@svovaf)
13216 * @since 1.2.0
13217 */
13218 function _add_license_activation() {
13219 if ( $this->is_migration() ) {
13220 return;
13221 }
13222
13223 if ( ! $this->is_user_admin() ) {
13224 // Only admins can activate a license.
13225 return;
13226 }
13227
13228 if ( ! $this->has_paid_plan() ) {
13229 // Module doesn't have any paid plans.
13230 return;
13231 }
13232
13233 if (
13234 $this->has_premium_version() &&
13235 ! $this->is_premium() &&
13236 /**
13237 * Also handle the case when an upgrade was made using the free version.
13238 *
13239 * @author Leo Fajardo (@leorw)
13240 * @since 2.3.2
13241 */
13242 ! is_object( $this->_get_license() )
13243 ) {
13244 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13245 return;
13246 }
13247
13248 // Add license activation link and AJAX request handler.
13249 if ( self::is_plugins_page() ) {
13250 $is_network_admin = fs_is_network_admin();
13251
13252 if (
13253 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13254 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13255 ) {
13256 if (
13257 $this->is_premium() ||
13258 ( $this->has_paid_plan() && ! $this->has_premium_version() )
13259 ) {
13260 /**
13261 * @since 1.2.0 Add license action link only on plugins page.
13262 */
13263 $this->_add_license_action_link();
13264 }
13265 }
13266 }
13267
13268 // Add license activation AJAX callback.
13269 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13270
13271 // Add resend license AJAX callback.
13272 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13273 }
13274
13275 /**
13276 * Prepares page to include all required UI and logic for the "Change User" dialog.
13277 *
13278 * @author Leo Fajardo (@leorw)
13279 * @since 2.3.2
13280 */
13281 function _add_user_change_option() {
13282 if ( ! $this->should_handle_user_change() ) {
13283 return;
13284 }
13285
13286 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13287
13288 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13289 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13290 return;
13291 }
13292
13293 // Add user change AJAX handler.
13294 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13295 }
13296
13297 /**
13298 * @author Leo Fajardo (@leorw)
13299 * @since 2.3.2
13300 */
13301 function should_handle_user_change() {
13302 if ( ! $this->is_user_admin() ) {
13303 // Only admins can change user.
13304 return false;
13305 }
13306
13307 if ( $this->is_addon() ) {
13308 return false;
13309 }
13310
13311 if ( ! $this->is_registered() ) {
13312 return false;
13313 }
13314
13315 if (
13316 $this->is_network_active() &&
13317 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13318 ) {
13319 // Handle only on site-level "Account" section for now.
13320 return false;
13321 }
13322
13323 return true;
13324 }
13325
13326 /**
13327 * @author Leo Fajardo (@leorw)
13328 * @since 2.0.2
13329 */
13330 function _add_premium_version_upgrade_selection() {
13331 if ( ! $this->is_user_admin() ) {
13332 return;
13333 }
13334
13335 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13336 // This is relevant only to the free versions and premium versions without an active license.
13337 return;
13338 }
13339
13340 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13341 $this->_add_premium_version_upgrade_selection_action();
13342 }
13343 }
13344
13345 /**
13346 * @author Edgar Melkonyan
13347 * @since 2.4.1
13348 *
13349 * @throws Freemius_Exception
13350 */
13351 function _toggle_whitelabel_mode_ajax_handler() {
13352 $this->_logger->entrance();
13353
13354 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13355
13356 if ( ! $this->is_user_admin() ) {
13357 // Only for admins.
13358 self::shoot_ajax_failure();
13359 }
13360
13361 $license = $this->get_api_user_scope()->call(
13362 "/licenses/{$this->_site->license_id}.json",
13363 'put',
13364 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13365 );
13366
13367 if ( ! $this->is_api_result_entity( $license ) ) {
13368 self::shoot_ajax_failure(
13369 FS_Api::is_api_error_object( $license ) ?
13370 $license->error->message :
13371 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13372 );
13373 }
13374
13375 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13376 $this->_store_licenses();
13377
13378 $this->_sync_license();
13379
13380 if ( ! $license->is_whitelabeled ) {
13381 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13382 } else {
13383 $this->_admin_notices->add_sticky(
13384 sprintf(
13385 $this->get_text_inline(
13386 '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.',
13387 'license_whitelabeled'
13388 ),
13389 "<strong>{$this->get_plugin_title()}</strong>",
13390 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13391 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13392 ),
13393 'license_whitelabeled'
13394 );
13395 }
13396
13397 self::shoot_ajax_response( array( 'success' => true ) );
13398 }
13399
13400 /**
13401 * @author Leo Fajardo (@leorw)
13402 * @since 2.3.0
13403 */
13404 function _add_beta_mode_update_handler() {
13405 if ( ! $this->is_user_admin() ) {
13406 return;
13407 }
13408
13409 if ( ! $this->is_premium() ) {
13410 return;
13411 }
13412
13413 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13414 }
13415
13416 /**
13417 * @author Leo Fajardo (@leorw)
13418 * @since 2.3.0
13419 */
13420 function _set_beta_mode_ajax_handler() {
13421 $this->_logger->entrance();
13422
13423 $this->check_ajax_referer( 'set_beta_mode' );
13424
13425 if ( ! $this->is_user_admin() ) {
13426 // Only for admins.
13427 self::shoot_ajax_failure();
13428 }
13429
13430 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13431
13432 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13433 self::shoot_ajax_failure();
13434 }
13435
13436 $site = $this->api_site_call(
13437 '',
13438 'put',
13439 array(
13440 'is_beta' => ( 'true' == $is_beta ),
13441 'fields' => 'is_beta'
13442 )
13443 );
13444
13445 if ( ! $this->is_api_result_entity( $site ) ) {
13446 self::shoot_ajax_failure(
13447 FS_Api::is_api_error_object( $site ) ?
13448 $site->error->message :
13449 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13450 );
13451 }
13452
13453 $this->_site->is_beta = $site->is_beta;
13454 $this->_store_site();
13455
13456 self::shoot_ajax_response( array( 'success' => true ) );
13457 }
13458
13459 /**
13460 * License activation WP AJAX handler.
13461 *
13462 * @author Leo Fajardo (@leorw)
13463 * @since 1.1.9
13464 *
13465 * @uses Freemius::activate_license()
13466 */
13467 function _activate_license_ajax_action() {
13468 $this->_logger->entrance();
13469
13470 $this->check_ajax_referer( 'activate_license' );
13471
13472 $license_key = trim( fs_request_get_raw( 'license_key' ) );
13473
13474 if ( empty( $license_key ) ) {
13475 $license_id = trim( fs_request_get_raw( 'license_id' ) );
13476
13477 if ( FS_Plugin_License::is_valid_id( $license_id ) ) {
13478 $license = $this->_get_license_by_id( $license_id, false );
13479
13480 if ( is_object( $license ) ) {
13481 $license_key = $license->secret_key;
13482 }
13483 }
13484 }
13485
13486 if ( empty( $license_key ) ) {
13487 exit;
13488 }
13489
13490 $sites = fs_is_network_admin() ?
13491 fs_request_get( 'sites', array(), 'post' ) :
13492 array();
13493
13494 $result = $this->activate_license(
13495 $license_key,
13496 $sites,
13497 fs_request_get_bool( 'is_marketing_allowed', null ),
13498 fs_request_get( 'blog_id', null ),
13499 fs_request_get( 'module_id', null, 'post' ),
13500 fs_request_get( 'user_id', null ),
13501 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
13502 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
13503 fs_request_get( 'license_owner_id', null )
13504 );
13505
13506 if (
13507 $result['success'] &&
13508 $this->is_bundle_license_auto_activation_enabled()
13509 ) {
13510 $license = new FS_Plugin_License();
13511 $license->secret_key = $license_key;
13512
13513 $this->maybe_activate_bundle_license( $license, $sites );
13514 }
13515
13516 echo json_encode( $result );
13517
13518 exit;
13519 }
13520
13521 /**
13522 * User change WP AJAX handler.
13523 *
13524 * @author Leo Fajardo (@leorw)
13525 * @since 2.3.2
13526 */
13527 function _user_change_ajax_action() {
13528 $this->_logger->entrance();
13529
13530 $this->check_ajax_referer( 'change_user' );
13531
13532 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13533 $new_user_id = fs_request_get( 'user_id' );
13534
13535 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13536 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13537 }
13538
13539 $params = array();
13540
13541 if ( ! empty( $new_email_address ) ) {
13542 $params['user_email'] = $new_email_address;
13543 } else {
13544 $params['user_id'] = $new_user_id;
13545 }
13546
13547 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13548 $install_ids = array();
13549
13550 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13551 $install_ids[ $slug ] = $install_info['install']->id;
13552 }
13553
13554 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13555
13556 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13557
13558 if ( FS_Api::is_api_error( $install ) ) {
13559 $error = '';
13560
13561 if ( is_object( $install ) ) {
13562 switch ( $install->error->code ) {
13563 case 'user_exist':
13564 $error = (
13565 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13566 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
13567 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>' ) .
13568 sprintf(
13569 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13570 $this->get_account_url( 'change_owner', array(
13571 'state' => 'init',
13572 'candidate_email' => $new_email_address
13573 ) ),
13574 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
13575 )
13576 );
13577 break;
13578 }
13579 }
13580
13581 if ( empty( $error ) ) {
13582 $error = FS_Api::is_api_error_object( $install ) ?
13583 $install->error->message :
13584 var_export( $install->error, true );
13585 }
13586
13587 self::shoot_ajax_failure( $error );
13588 } else {
13589 if (
13590 // If successful ownership change.
13591 $this->get_user()->id != $install->user_id ||
13592 ! empty( $new_email_address )
13593 ) {
13594 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
13595 }
13596 }
13597
13598 self::shoot_ajax_success();
13599 }
13600
13601 /**
13602 * @author Leo Fajardo (@leorw)
13603 * @since 2.3.2.14
13604 */
13605 function starting_migration() {
13606 if ( ! empty( $this->_storage->license_migration ) ) {
13607 // Do not overwrite the data if already set.
13608 return;
13609 }
13610
13611 $this->_storage->license_migration = array(
13612 'is_migrating' => true,
13613 'start_timestamp' => time()
13614 );
13615 }
13616
13617 /**
13618 * @author Leo Fajardo (@leorw)
13619 * @since 2.3.2.14
13620 */
13621 function is_migration() {
13622 if ( $this->is_addon() ) {
13623 return $this->get_parent_instance()->is_migration();
13624 }
13625
13626 if ( empty( $this->_storage->license_migration ) ) {
13627 return false;
13628 }
13629
13630 if ( ! $this->_storage->license_migration['is_migrating'] ) {
13631 return false;
13632 }
13633
13634 return (
13635 // Return `true` if the migration is within 5 minutes from the starting time.
13636 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
13637 );
13638 }
13639
13640 /**
13641 *
13642 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
13643 *
13644 * @author Vova Feldman (@svovaf)
13645 * @since 2.3.0
13646 *
13647 * @param string $license_key
13648 * @param null|bool $is_marketing_allowed
13649 * @param null|number $plugin_id
13650 * @param array $sites
13651 * @param int $blog_id
13652 * @param null|number $license_owner_id
13653 *
13654 * @return array {
13655 * @var bool $success
13656 * @var string $error
13657 * @var string $next_page
13658 * }
13659 *
13660 * @uses Freemius::activate_license()
13661 */
13662 function activate_migrated_license(
13663 $license_key,
13664 $is_marketing_allowed = null,
13665 $plugin_id = null,
13666 $sites = array(),
13667 $blog_id = null,
13668 $license_owner_id = null
13669 ) {
13670 $this->_logger->entrance();
13671
13672 $result = $this->activate_license(
13673 $license_key,
13674 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
13675 $this->get_sites_for_network_level_optin() :
13676 $sites,
13677 $is_marketing_allowed,
13678 $blog_id,
13679 $plugin_id,
13680 null,
13681 null,
13682 null,
13683 $license_owner_id
13684 );
13685
13686 // No need to show the sticky after license activation notice after migrating a license.
13687 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13688
13689 return $result;
13690 }
13691
13692 /**
13693 * @author Leo Fajardo (@leorw)
13694 * @since 2.3.1
13695 *
13696 * @return string
13697 */
13698 function get_pricing_js_path() {
13699 if ( ! isset( $this->_pricing_js_path ) ) {
13700 $default_path = WP_FS__DIR_JS . '/pricing/freemius-pricing.js';
13701
13702 $pricing_js_path = $this->apply_filters( 'freemius_pricing_js_path', $default_path );
13703
13704 // Backward compatibility for people who placed the freemius-pricing inside `includes` directory. Let it take more preference than the default path.
13705 if ( empty( $pricing_js_path ) ) {
13706 global $fs_active_plugins;
13707
13708 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
13709 if ( $data->plugin_path == $this->get_plugin_basename() ) {
13710 $plugin_or_theme_root_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
13711
13712 $pricing_js_path = $plugin_or_theme_root_dir
13713 . '/'
13714 // The basename will be `plugins`, `themes`, or the basename of a custom plugins or themes directory.
13715 . str_replace( '../' . basename( $plugin_or_theme_root_dir ) . '/', '', $sdk_path )
13716 . '/includes/freemius-pricing/freemius-pricing.js';
13717
13718 break;
13719 }
13720 }
13721 }
13722
13723 // If it is still empty, load the default pricing JS.
13724 if ( ! file_exists( $pricing_js_path ) ) {
13725 $pricing_js_path = $default_path;
13726 }
13727
13728 $this->_pricing_js_path = $pricing_js_path;
13729 }
13730
13731 return $this->_pricing_js_path;
13732 }
13733
13734 /**
13735 * @author Leo Fajardo (@leorw)
13736 * @since 2.3.1
13737 *
13738 * @deprecated Since v2.9.0 we have removed the iFrame based pricing. This will always return `false`.
13739 *
13740 * @return bool
13741 */
13742 function should_use_external_pricing() {
13743 return false;
13744 }
13745
13746 /**
13747 * The implementation of this method was previously in `_activate_license_ajax_action()`.
13748 *
13749 * @author Vova Feldman (@svovaf)
13750 * @since 2.2.4
13751 * @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).
13752 * @param string $license_key
13753 * @param array $sites
13754 * @param null|bool $is_marketing_allowed
13755 * @param null|int $blog_id
13756 * @param null|number $plugin_id
13757 * @param null|number $user_id
13758 * @param bool|null $is_extensions_tracking_allowed
13759 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint.
13760 * @param null|number $license_owner_id
13761 *
13762 *
13763 * @return array {
13764 * @var bool $success
13765 * @var string $error
13766 * @var string $next_page
13767 * }
13768 */
13769 private function activate_license(
13770 $license_key,
13771 $sites = array(),
13772 $is_marketing_allowed = null,
13773 $blog_id = null,
13774 $plugin_id = null,
13775 $user_id = null,
13776 $is_extensions_tracking_allowed = null,
13777 $is_diagnostic_tracking_allowed = null,
13778 $license_owner_id = null
13779 ) {
13780 $this->_logger->entrance();
13781
13782 $license_key = trim( $license_key );
13783
13784 $is_network_activation_or_migration = (
13785 fs_is_network_admin() ||
13786 ( ! empty( $sites ) && $this->is_migration() )
13787 );
13788
13789 if ( ! $is_network_activation_or_migration ) {
13790 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
13791 $sites = array();
13792 }
13793
13794 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
13795 $this :
13796 $this->get_addon_instance( $plugin_id );
13797
13798 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
13799 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
13800 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
13801 ) );
13802
13803 $error = false;
13804 $next_page = false;
13805
13806 $has_valid_blog_id = is_numeric( $blog_id );
13807
13808 $user = null;
13809
13810 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
13811 /**
13812 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
13813 *
13814 * @author Vova Feldman (@svovaf)
13815 */
13816 $user = $fs->get_parent_instance()->get_current_or_network_user();
13817 } else if ( $fs->is_registered() ) {
13818 $user = $fs->get_current_or_network_user();
13819 }
13820
13821 if ( $has_valid_blog_id ) {
13822 /**
13823 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
13824 *
13825 * @author Leo Fajardo (@leorw)
13826 */
13827 $fs->switch_to_blog( $blog_id );
13828 }
13829
13830 if ( is_object( $user ) ) {
13831 $result = true;
13832
13833 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
13834 // If no specific blog ID was provided, activate the license for all sites in the network.
13835 $blog_2_install_map = array();
13836 $site_ids = array();
13837
13838 foreach ( $sites as $site ) {
13839 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
13840 continue;
13841 }
13842
13843 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
13844
13845 if ( is_object( $install ) ) {
13846 $blog_2_install_map[ $site['blog_id'] ] = $install;
13847 } else {
13848 $site_ids[] = $site['blog_id'];
13849 }
13850 }
13851
13852 if ( ! empty( $blog_2_install_map ) ) {
13853 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
13854 }
13855
13856 if ( true === $result && ! empty( $site_ids ) ) {
13857 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
13858 }
13859 } else {
13860 if ( $fs->is_registered() ) {
13861 $params = array(
13862 'license_key' => $fs->apply_filters( 'license_key', $license_key )
13863 );
13864
13865 $install_ids = array();
13866
13867 $change_owner = FS_User::is_valid_id( $user_id );
13868
13869 if ( $change_owner ) {
13870 $params['user_id'] = $user_id;
13871
13872 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
13873
13874 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13875 $install_ids[ $slug ] = $install_info['install']->id;
13876 }
13877
13878 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13879 }
13880
13881 $api = $fs->get_api_site_scope();
13882
13883 $result = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
13884
13885 if ( ! FS_Api::is_api_error( $result ) ) {
13886 $install = $result;
13887
13888 $fs->reconnect_locally( $has_valid_blog_id );
13889
13890 if (
13891 $change_owner &&
13892 // If successful ownership change.
13893 $fs->get_user()->id != $install->user_id
13894 ) {
13895 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
13896 }
13897 }
13898 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
13899 $result = $fs->activate_license_on_site( $user, $license_key );
13900 }
13901 }
13902
13903 $is_connected = null;
13904
13905 if ( true !== $result && ! FS_Api::is_api_result_entity( $result ) ) {
13906 if ( FS_Api::is_blocked( $result ) ) {
13907 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
13908
13909 $is_connected = false;
13910 }
13911
13912 $error = FS_Api::is_api_error_object( $result ) ?
13913 $result->error->message :
13914 var_export( $result, true );
13915 } else {
13916 $is_connected = true;
13917
13918 $fs->network_upgrade_mode_completed();
13919
13920 $fs->_user = $user;
13921
13922 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
13923 $fs->_site = $fs->get_network_install();
13924 }
13925
13926 $fs->_sync_license( true, $has_valid_blog_id );
13927
13928 $this->maybe_sync_install_user();
13929
13930 $next_page = $fs->is_addon() ?
13931 $fs->get_parent_instance()->get_account_url() :
13932 $fs->get_after_activation_url( 'after_connect_url' );
13933 }
13934
13935 $fs->update_connectivity_info( $is_connected );
13936 } else {
13937 $next_page = $fs->opt_in(
13938 false,
13939 false,
13940 false,
13941 $license_key,
13942 false,
13943 false,
13944 false,
13945 $is_marketing_allowed,
13946 $sites,
13947 true,
13948 $license_owner_id
13949 );
13950
13951 if ( isset( $next_page->error ) ) {
13952 $error = $next_page->error;
13953 } else {
13954 if ( $is_network_activation_or_migration ) {
13955 /**
13956 * Get the list of sites that were just opted-in (and license activated).
13957 * This is an optimization for the next part below saving some DB queries.
13958 */
13959 $connected_sites = array();
13960 foreach ( $sites as $site ) {
13961 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
13962 $connected_sites[ $site['blog_id'] ] = true;
13963 }
13964 }
13965
13966 $all_sites = self::get_sites();
13967 $pending_blog_ids = array();
13968
13969 /**
13970 * 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.
13971 *
13972 * @author Vova Feldman (@svovaf)
13973 */
13974 foreach ( $all_sites as $site ) {
13975 $blog_id = self::get_site_blog_id( $site );
13976
13977 if ( isset( $connected_sites[ $blog_id ] ) ) {
13978 // Site was just connected.
13979 continue;
13980 }
13981
13982 if ( $fs->is_installed_on_site( $blog_id ) ) {
13983 // Site was already connected before.
13984 continue;
13985 }
13986
13987 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
13988 // Site's connection was delegated.
13989 continue;
13990 }
13991
13992 if ( $fs->is_anonymous_site( $blog_id ) ) {
13993 // Site connection was already skipped.
13994 continue;
13995 }
13996
13997 $pending_blog_ids[] = $blog_id;
13998 }
13999
14000 if ( ! empty( $pending_blog_ids ) ) {
14001 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
14002 $fs->skip_connection( $pending_blog_ids );
14003 } else {
14004 $fs->delegate_connection( $pending_blog_ids );
14005 }
14006 }
14007 }
14008 }
14009 }
14010
14011 if ( false === $error && true === $fs->_storage->require_license_activation ) {
14012 $fs->_storage->require_license_activation = false;
14013 }
14014
14015 $result = array(
14016 'success' => ( false === $error )
14017 );
14018
14019 if ( false !== $error ) {
14020 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
14021 } else {
14022 if ( $fs->is_addon() || $fs->has_addons() ) {
14023 /**
14024 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
14025 * an updated valid user licenses collection will be fetched from the server which is used to also
14026 * update the account add-ons (add-ons the user has licenses for).
14027 *
14028 * @author Leo Fajardo (@leorw)
14029 * @since 2.2.4
14030 */
14031 $fs->purge_valid_user_licenses_cache();
14032 }
14033
14034 $result['next_page'] = $next_page;
14035 }
14036
14037 if ( $result['success'] ) {
14038 $this->do_action( 'after_license_activation' );
14039 }
14040
14041 return $result;
14042 }
14043
14044 /**
14045 * @author Leo Fajardo (@leorw)
14046 * @since 2.3.2
14047 *
14048 * @return array {
14049 * @key string Product slug.
14050 * @value array {
14051 * @property FS_Site $site
14052 * @property FS_Plugin_License $license
14053 * }
14054 * }
14055 */
14056 private function get_parent_and_addons_installs_info() {
14057 $fs = $this->is_addon() ?
14058 $this->get_parent_instance() :
14059 $this;
14060
14061 $installed_addons_ids = array();
14062
14063 $installed_addons_instances = $fs->get_installed_addons();
14064 foreach ( $installed_addons_instances as $instance ) {
14065 $installed_addons_ids[] = $instance->get_id();
14066 }
14067
14068 $addons_ids = array_unique( array_merge(
14069 $installed_addons_ids,
14070 $fs->get_updated_account_addons()
14071 ) );
14072
14073 // Add parent product info.
14074 $installs_info_by_slug_map = array(
14075 $fs->get_slug() => array(
14076 'install' => $fs->get_site(),
14077 'license' => $fs->_get_license()
14078 )
14079 );
14080
14081 foreach ( $addons_ids as $addon_id ) {
14082 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
14083
14084 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
14085
14086 if ( ! isset( $addon_info['is_connected'] ) || ! $addon_info['is_connected'] ) {
14087 // Add-on is not associated with an install entity.
14088 continue;
14089 }
14090
14091 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
14092 'install' => $addon_info['site'],
14093 'license' => isset( $addon_info['license'] ) ?
14094 $addon_info['license'] :
14095 null
14096 );
14097 }
14098
14099 return $installs_info_by_slug_map;
14100 }
14101
14102 /**
14103 * @author Leo Fajardo (@leorw)
14104 * @since 1.2.3.1
14105 */
14106 function _network_activate_ajax_action() {
14107 $this->_logger->entrance();
14108
14109 $this->check_ajax_referer( 'network_activate' );
14110
14111 $plugin_id = fs_request_get( 'module_id', '', 'post' );
14112 $fs = ( $plugin_id == $this->_module_id ) ?
14113 $this :
14114 $this->get_addon_instance( $plugin_id );
14115
14116 $error = false;
14117
14118 $sites = fs_request_get( 'sites', array(), 'post' );
14119 if ( is_array( $sites ) && ! empty( $sites ) ) {
14120 $sites_by_action = array(
14121 'allow' => array(),
14122 'delegate' => array(),
14123 'skip' => array()
14124 );
14125
14126 foreach ( $sites as $site ) {
14127 $sites_by_action[ $site['action'] ][] = $site;
14128 }
14129
14130 $total_sites = count( $sites );
14131 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
14132
14133 $next_page = '';
14134
14135 $has_any_install = fs_request_get_bool( 'has_any_install' );
14136
14137 if ( $total_sites === $total_sites_to_delegate &&
14138 ! $this->is_network_upgrade_mode() &&
14139 ! $has_any_install
14140 ) {
14141 $this->delegate_connection();
14142 } else {
14143 if ( ! empty( $sites_by_action['delegate'] ) ) {
14144 $this->delegate_connection( self::get_sites_blog_ids( $sites_by_action['delegate'] ) );
14145 }
14146
14147 if ( ! empty( $sites_by_action['skip'] ) ) {
14148 $this->skip_connection( self::get_sites_blog_ids( $sites_by_action['skip'] ) );
14149 }
14150
14151 if ( empty( $sites_by_action['allow'] ) ) {
14152 if ( $has_any_install ) {
14153 $first_install = $fs->find_first_install();
14154
14155 if ( ! is_null( $first_install ) ) {
14156 $fs->_site = $first_install['install'];
14157 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
14158
14159 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
14160 $fs->_storage->network_user_id = $fs->_user->id;
14161 }
14162 }
14163 } else {
14164 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
14165 $next_page = $fs->opt_in(
14166 false,
14167 false,
14168 false,
14169 false,
14170 false,
14171 false,
14172 false,
14173 fs_request_get_bool( 'is_marketing_allowed', null ),
14174 $sites_by_action['allow']
14175 );
14176 } else {
14177 $next_page = $fs->install_with_user(
14178 $this->get_network_user(),
14179 false,
14180 false,
14181 false,
14182 true,
14183 $sites_by_action['allow']
14184 );
14185 }
14186
14187 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
14188 $error = $next_page->error;
14189 }
14190 }
14191 }
14192
14193 if ( empty( $next_page ) ) {
14194 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
14195 }
14196 } else {
14197 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
14198 }
14199
14200 $result = array(
14201 'success' => ( false === $error )
14202 );
14203
14204 if ( false !== $error ) {
14205 $result['error'] = $error;
14206 } else {
14207 $result['next_page'] = $next_page;
14208 }
14209
14210 echo json_encode( $result );
14211
14212 exit;
14213 }
14214
14215 /**
14216 * Billing update AJAX callback.
14217 *
14218 * @author Vova Feldman (@svovaf)
14219 * @since 1.2.1.5
14220 */
14221 function _update_billing_ajax_action() {
14222 $this->_logger->entrance();
14223
14224 $this->check_ajax_referer( 'update_billing' );
14225
14226 if ( ! $this->is_user_admin() ) {
14227 // Only for admins.
14228 self::shoot_ajax_failure();
14229 }
14230
14231 $billing = fs_request_get( 'billing' );
14232
14233 $api = $this->get_api_user_scope();
14234 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
14235 'plugin_id' => $this->get_parent_id(),
14236 ) ) );
14237
14238 if ( ! $this->is_api_result_entity( $result ) ) {
14239 self::shoot_ajax_failure();
14240 }
14241
14242 // Purge cached billing.
14243 $this->get_api_user_scope()->purge_cache( 'billing.json' );
14244
14245 self::shoot_ajax_success();
14246 }
14247
14248 /**
14249 * Trial start for anonymous users (AJAX callback).
14250 *
14251 * @author Vova Feldman (@svovaf)
14252 * @since 1.2.1.5
14253 */
14254 function _start_trial_ajax_action() {
14255 $this->_logger->entrance();
14256
14257 $this->check_ajax_referer( 'start_trial' );
14258
14259 if ( ! $this->is_user_admin() ) {
14260 // Only for admins.
14261 self::shoot_ajax_failure();
14262 }
14263
14264 $trial_data = fs_request_get( 'trial' );
14265
14266 $next_page = $this->opt_in(
14267 false,
14268 false,
14269 false,
14270 false,
14271 false,
14272 $trial_data['plan_id']
14273 );
14274
14275 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14276 self::shoot_ajax_failure(
14277 isset( $next_page->error ) ?
14278 $next_page->error->message :
14279 var_export( $next_page, true )
14280 );
14281 }
14282
14283 $this->shoot_ajax_success( array(
14284 'next_page' => $next_page,
14285 ) );
14286 }
14287
14288 /**
14289 * @author Leo Fajardo (@leorw)
14290 * @since 1.2.0
14291 */
14292 function _resend_license_key_ajax_action() {
14293 $this->_logger->entrance();
14294
14295 $this->check_ajax_referer( 'resend_license_key' );
14296
14297 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14298
14299 if ( empty( $email_address ) ) {
14300 exit;
14301 }
14302
14303 $error = false;
14304
14305 $api = $this->get_api_plugin_scope();
14306 $result = $api->call( '/licenses/resend.json', 'post',
14307 array(
14308 'email' => $email_address,
14309 'url' => home_url(),
14310 )
14311 );
14312
14313 if ( is_object( $result ) && isset( $result->error ) ) {
14314 $error = $result->error;
14315
14316 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14317 $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' );
14318 } else if ( 'no_license' === $error->code ) {
14319 $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' );
14320 } else {
14321 $error = $error->message;
14322 }
14323 }
14324
14325 $licenses = array(
14326 'success' => ( false === $error )
14327 );
14328
14329 if ( false !== $error ) {
14330 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14331 }
14332
14333 echo json_encode( $licenses );
14334
14335 exit;
14336 }
14337
14338 /**
14339 * @author Vova Feldman (@svovaf)
14340 * @since 1.2.1.8
14341 *
14342 * @var string
14343 */
14344 private static $_pagenow;
14345
14346 /**
14347 * Get current page or the referer if executing a WP AJAX request.
14348 *
14349 * @author Vova Feldman (@svovaf)
14350 * @since 1.2.1.8
14351 *
14352 * @return string
14353 */
14354 static function get_current_page() {
14355 if ( ! isset( self::$_pagenow ) ) {
14356 global $pagenow;
14357 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14358 /**
14359 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14360 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14361 *
14362 * @author Leo Fajardo (@leorw)
14363 * @since 2.2.3
14364 */
14365 if ( is_network_admin() ) {
14366 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14367 } else if ( is_user_admin() ) {
14368 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14369 } else {
14370 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14371 }
14372
14373 $pagenow = $self_matches[1];
14374 $pagenow = trim( $pagenow, '/' );
14375 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14376 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14377 $pagenow = 'index.php';
14378 } else {
14379 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14380 $pagenow = strtolower( $self_matches[1] );
14381 if ( '.php' !== substr($pagenow, -4, 4) )
14382 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14383 }
14384 }
14385
14386 self::$_pagenow = $pagenow;
14387
14388 if ( self::is_ajax() &&
14389 'admin-ajax.php' === $pagenow
14390 ) {
14391 $referer = fs_get_raw_referer();
14392
14393 if ( is_string( $referer ) ) {
14394 $parts = explode( '?', $referer );
14395
14396 self::$_pagenow = basename( $parts[0] );
14397 }
14398 }
14399 }
14400
14401 return self::$_pagenow;
14402 }
14403
14404 /**
14405 * Helper method to check if user in the plugins page.
14406 *
14407 * @author Vova Feldman (@svovaf)
14408 * @since 1.2.1.5
14409 *
14410 * @return bool
14411 */
14412 static function is_plugins_page() {
14413 return ( 'plugins.php' === self::get_current_page() );
14414 }
14415
14416 /**
14417 * @author Leo Fajardo (@leorw)
14418 * @since 2.2.3
14419 *
14420 * @return bool
14421 */
14422 static function is_plugin_install_page() {
14423 return ( 'plugin-install.php' === self::get_current_page() );
14424 }
14425
14426 /**
14427 * @author Leo Fajardo (@leorw)
14428 * @since 2.0.2
14429 *
14430 * @return bool
14431 */
14432 static function is_updates_page() {
14433 return ( 'update-core.php' === self::get_current_page() );
14434 }
14435
14436 /**
14437 * Helper method to check if user in the themes page.
14438 *
14439 * @author Vova Feldman (@svovaf)
14440 * @since 1.2.2.6
14441 *
14442 * @return bool
14443 */
14444 static function is_themes_page() {
14445 return ( 'themes.php' === self::get_current_page() );
14446 }
14447
14448 #----------------------------------------------------------------------------------
14449 #region Affiliation
14450 #----------------------------------------------------------------------------------
14451
14452 /**
14453 * @author Leo Fajardo (@leorw)
14454 * @since 1.2.3
14455 *
14456 * @return bool
14457 */
14458 function has_affiliate_program() {
14459 if ( ! is_object( $this->_plugin ) ) {
14460 return false;
14461 }
14462
14463 return $this->_plugin->has_affiliate_program();
14464 }
14465
14466 /**
14467 * Get Plugin ID under which we will track affiliate application.
14468 *
14469 * This could either be the Bundle ID or the main plugin ID.
14470 *
14471 * @return number Bundle ID if developer has provided one, else the main plugin ID.
14472 */
14473 private function get_plugin_id_for_affiliate_terms() {
14474 return $this->has_bundle_context() ?
14475 $this->get_bundle_id() :
14476 $this->_plugin->id;
14477 }
14478
14479 /**
14480 * @author Leo Fajardo (@leorw)
14481 * @since 1.2.4
14482 */
14483 private function fetch_affiliate_terms() {
14484 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14485 /**
14486 * In case we have a bundle set in SDK configuration, we would like to use that for affiliates, not the main plugin.
14487 */
14488 $plugins_api = $this->has_bundle_context() ?
14489 $this->get_api_bundle_scope() :
14490 $this->get_api_plugin_scope();
14491
14492 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14493
14494 /**
14495 * 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.
14496 */
14497 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14498 return;
14499 }
14500
14501 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14502 }
14503 }
14504
14505 /**
14506 * @author Leo Fajardo (@leorw)
14507 * @since 1.2.4
14508 */
14509 private function fetch_affiliate_and_custom_terms() {
14510 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14511 $application_data = $this->_storage->affiliate_application_data;
14512 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14513
14514 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14515
14516 $users_api = $this->get_api_user_scope();
14517 $result = $users_api->get( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14518 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14519 if ( ! empty( $result->affiliates ) ) {
14520 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14521
14522 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14523 $application_data['status'] = $affiliate->status;
14524 $this->_storage->affiliate_application_data = $application_data;
14525 }
14526
14527 if ( $affiliate->is_using_custom_terms ) {
14528 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14529 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14530 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14531 }
14532 }
14533
14534 $this->affiliate = $affiliate;
14535 }
14536 }
14537 }
14538 }
14539
14540 /**
14541 * @author Leo Fajardo (@leorw)
14542 * @since 1.2.3
14543 */
14544 private function fetch_affiliate_and_terms() {
14545 $this->_logger->entrance();
14546
14547 $this->fetch_affiliate_terms();
14548 $this->fetch_affiliate_and_custom_terms();
14549 }
14550
14551 /**
14552 * @author Leo Fajardo (@leorw)
14553 * @since 1.2.3
14554 *
14555 * @return FS_Affiliate
14556 */
14557 function get_affiliate() {
14558 return $this->affiliate;
14559 }
14560
14561
14562 /**
14563 * @author Leo Fajardo (@leorw)
14564 * @since 1.2.3
14565 *
14566 * @return FS_AffiliateTerms
14567 */
14568 function get_affiliate_terms() {
14569 return is_object( $this->custom_affiliate_terms ) ?
14570 $this->custom_affiliate_terms :
14571 $this->plugin_affiliate_terms;
14572 }
14573
14574 /**
14575 * @author Leo Fajardo (@leorw)
14576 * @since 1.2.3
14577 */
14578 function _submit_affiliate_application() {
14579 $this->_logger->entrance();
14580
14581 $this->check_ajax_referer( 'submit_affiliate_application' );
14582
14583 if ( ! $this->is_user_admin() ) {
14584 // Only for admins.
14585 self::shoot_ajax_failure();
14586 }
14587
14588 $affiliate = fs_request_get( 'affiliate' );
14589
14590 if ( empty( $affiliate['promotion_methods'] ) ) {
14591 unset( $affiliate['promotion_methods'] );
14592 }
14593
14594 if ( ! empty( $affiliate['additional_domains'] ) ) {
14595 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
14596 }
14597
14598 if ( ! $this->is_registered() ) {
14599 $email_address = isset( $affiliate['email'] ) ? $affiliate['email'] : '';
14600
14601 if ( ! is_email( $email_address ) ) {
14602 self::shoot_ajax_failure('Invalid email address.');
14603 }
14604
14605 // Opt in but don't track usage.
14606 $next_page = $this->opt_in(
14607 $email_address,
14608 false,
14609 false,
14610 false,
14611 false,
14612 false,
14613 true
14614 );
14615
14616 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14617 self::shoot_ajax_failure(
14618 isset( $next_page->error ) ?
14619 $next_page->error->message :
14620 var_export( $next_page, true )
14621 );
14622 } else if ( $this->is_pending_activation() ) {
14623 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' ) );
14624 }
14625 }
14626
14627 $this->fetch_affiliate_terms();
14628
14629 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14630
14631 $api = $this->get_api_user_scope();
14632 $result = $api->call(
14633 ( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
14634 'post',
14635 $affiliate
14636 );
14637
14638 if ( $this->is_api_error( $result ) ) {
14639 self::shoot_ajax_failure(
14640 isset( $result->error ) ?
14641 $result->error->message :
14642 var_export( $result, true )
14643 );
14644 } else {
14645 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
14646 $this->_admin_notices->remove_sticky( 'affiliate_program' );
14647 }
14648
14649 $affiliate_application_data = array(
14650 'status' => 'pending',
14651 'stats_description' => $affiliate['stats_description'],
14652 'promotion_method_description' => $affiliate['promotion_method_description'],
14653 );
14654
14655 if ( ! empty( $affiliate['promotion_methods'] ) ) {
14656 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
14657 }
14658
14659 if ( ! empty( $affiliate['domain'] ) ) {
14660 $affiliate_application_data['domain'] = $affiliate['domain'];
14661 }
14662
14663 if ( ! empty( $affiliate['additional_domains'] ) ) {
14664 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
14665 }
14666
14667 $this->_storage->affiliate_application_data = $affiliate_application_data;
14668 }
14669
14670 // Purge cached affiliate.
14671 $api->purge_cache( 'affiliate.json' );
14672
14673 self::shoot_ajax_success( $result );
14674 }
14675
14676 /**
14677 * @author Leo Fajardo (@leorw)
14678 * @since 1.2.3
14679 *
14680 * @return array|null
14681 */
14682 function get_affiliate_application_data() {
14683 if ( empty( $this->_storage->affiliate_application_data ) ) {
14684 return null;
14685 }
14686
14687 return $this->_storage->affiliate_application_data;
14688 }
14689
14690 #endregion Affiliation ------------------------------------------------------------
14691
14692 #----------------------------------------------------------------------------------
14693 #region URL Generators
14694 #----------------------------------------------------------------------------------
14695
14696 /**
14697 * Alias to pricing_url().
14698 *
14699 * @author Vova Feldman (@svovaf)
14700 * @since 1.0.2
14701 *
14702 * @uses pricing_url()
14703 *
14704 * @param string $period Billing cycle
14705 * @param bool $is_trial
14706 *
14707 * @return string
14708 */
14709 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14710 return $this->pricing_url( $period, $is_trial );
14711 }
14712
14713 /**
14714 * @author Vova Feldman (@svovaf)
14715 * @since 1.0.9
14716 *
14717 * @uses get_upgrade_url()
14718 *
14719 * @return string
14720 */
14721 function get_trial_url() {
14722 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
14723 }
14724
14725 /**
14726 * @author Leo Fajardo (@leorw)
14727 * @since 2.1.4
14728 *
14729 * @param string $new_version
14730 *
14731 * @return string
14732 */
14733 function version_upgrade_checkout_link( $new_version ) {
14734 if ( ! is_object( $this->_license ) ) {
14735 $url = $this->pricing_url();
14736
14737 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
14738 } else {
14739 $subscription = $this->_get_subscription( $this->_license->id );
14740
14741 $url = $this->checkout_url(
14742 is_object( $subscription ) ?
14743 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
14744 WP_FS__PERIOD_LIFETIME,
14745 false,
14746 array( 'licenses' => $this->_license->quota )
14747 );
14748
14749 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
14750 }
14751
14752 return sprintf(
14753 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
14754 sprintf(
14755 '<a href="%s">%s</a>',
14756 $this->apply_filters( 'update_notice_checkout_url', $url ),
14757 $purchase_license_text
14758 ),
14759 $new_version
14760 );
14761 }
14762
14763 /**
14764 * Plugin's pricing URL.
14765 *
14766 * @author Vova Feldman (@svovaf)
14767 * @since 1.0.4
14768 *
14769 * @param string $billing_cycle Billing cycle
14770 *
14771 * @param bool $is_trial
14772 *
14773 * @return string
14774 */
14775 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14776 $this->_logger->entrance();
14777
14778 $params = array(
14779 'billing_cycle' => $billing_cycle
14780 );
14781
14782 if ( $is_trial ) {
14783 $params['trial'] = 'true';
14784 }
14785
14786 $url = $this->is_addon() ?
14787 $this->_parent->addon_url( $this->_slug ) :
14788 $this->_get_admin_page_url( 'pricing', $params );
14789
14790 return $this->get_pricing_url_with_filter( $url );
14791 }
14792
14793 /**
14794 * Retrieves the filtered pricing URL.
14795 *
14796 * @author Leo Fajardo (@leorw)
14797 * @since 2.7.4
14798 *
14799 * @param string $url
14800 *
14801 * @return string
14802 */
14803 private function get_pricing_url_with_filter( $url ) {
14804 return $this->apply_filters( 'pricing_url', $url );
14805 }
14806
14807 /**
14808 * Checkout page URL.
14809 *
14810 * @author Vova Feldman (@svovaf)
14811 * @since 1.0.6
14812 *
14813 * @param string $billing_cycle Billing cycle
14814 * @param bool $is_trial
14815 * @param array $extra (optional) Extra parameters, override other query params.
14816 * @param bool|null $network
14817 *
14818 * @return string
14819 */
14820 function checkout_url(
14821 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14822 $is_trial = false,
14823 $extra = array(),
14824 $network = null
14825 ) {
14826 $this->_logger->entrance();
14827
14828 $params = array(
14829 'checkout' => 'true',
14830 'billing_cycle' => $billing_cycle,
14831 );
14832
14833 if ( $is_trial ) {
14834 $params['trial'] = 'true';
14835 }
14836
14837 /**
14838 * Params in extra override other params.
14839 */
14840 $params = array_merge( $params, $extra );
14841
14842 return $this->apply_filters( 'checkout_url', $this->_get_admin_page_url( 'pricing', $params, $network ) );
14843 }
14844
14845 /**
14846 * Add-on checkout URL.
14847 *
14848 * @author Vova Feldman (@svovaf)
14849 * @since 1.1.7
14850 *
14851 * @param number $addon_id
14852 * @param number $pricing_id
14853 * @param string $billing_cycle
14854 * @param bool $is_trial
14855 * @param bool|null $network
14856 *
14857 * @return string
14858 */
14859 function addon_checkout_url(
14860 $addon_id,
14861 $pricing_id,
14862 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14863 $is_trial = false,
14864 $network = null
14865 ) {
14866 return $this->checkout_url( $billing_cycle, $is_trial, array(
14867 'plugin_id' => $addon_id,
14868 'pricing_id' => $pricing_id,
14869 ), $network );
14870 }
14871
14872 #endregion
14873
14874 #endregion ------------------------------------------------------------------
14875
14876 /**
14877 * Check if plugin has any add-ons.
14878 *
14879 * @author Vova Feldman (@svovaf)
14880 * @since 1.0.5
14881 *
14882 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
14883 *
14884 * @return bool
14885 */
14886 function has_addons() {
14887 $this->_logger->entrance();
14888
14889 return $this->_has_addons;
14890 }
14891
14892 /**
14893 * Check if plugin can work in anonymous mode.
14894 *
14895 * @author Vova Feldman (@svovaf)
14896 * @since 1.0.9
14897 *
14898 * @return bool
14899 *
14900 * @deprecated Please use is_enable_anonymous() instead.
14901 */
14902 function enable_anonymous() {
14903 return $this->_enable_anonymous;
14904 }
14905
14906 /**
14907 * Check if plugin can work in anonymous mode.
14908 *
14909 * @author Vova Feldman (@svovaf)
14910 * @since 1.1.9
14911 *
14912 * @return bool
14913 */
14914 function is_enable_anonymous() {
14915 return $this->_enable_anonymous;
14916 }
14917
14918 /**
14919 * Check if plugin is premium only (no free plans).
14920 *
14921 * @author Vova Feldman (@svovaf)
14922 * @since 1.1.9
14923 *
14924 * @return bool
14925 */
14926 function is_only_premium() {
14927 return $this->_is_premium_only;
14928 }
14929
14930 /**
14931 * Checks if the plugin's type is "plugin". The other type is "theme".
14932 *
14933 * @author Leo Fajardo (@leorw)
14934 * @since 1.2.2
14935 *
14936 * @return bool
14937 */
14938 function is_plugin() {
14939 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
14940 }
14941
14942 /**
14943 * @author Leo Fajardo (@leorw)
14944 * @since 1.2.2
14945 *
14946 * @return string
14947 */
14948 function get_module_type() {
14949 if ( ! isset( $this->_module_type ) ) {
14950 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
14951 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
14952 }
14953
14954 return $this->_module_type;
14955 }
14956
14957 /**
14958 * @author Leo Fajardo (@leorw)
14959 * @since 1.2.2
14960 *
14961 * @return string
14962 */
14963 function get_plugin_main_file_path() {
14964 return $this->_plugin_main_file_path;
14965 }
14966
14967 /**
14968 * Check if module has a premium code version.
14969 *
14970 * Serviceware module might be freemium without any
14971 * premium code version, where the paid features
14972 * are all part of the service.
14973 *
14974 * @author Vova Feldman (@svovaf)
14975 * @since 1.2.1.6
14976 *
14977 * @return bool
14978 */
14979 function has_premium_version() {
14980 return $this->_has_premium_version;
14981 }
14982
14983 /**
14984 * Check if feature supported with current site's plan.
14985 *
14986 * @author Vova Feldman (@svovaf)
14987 * @since 1.0.1
14988 *
14989 * @todo IMPLEMENT
14990 *
14991 * @param number $feature_id
14992 *
14993 * @throws Exception
14994 */
14995 function is_feature_supported( $feature_id ) {
14996 throw new Exception( 'not implemented' );
14997 }
14998
14999 /**
15000 * @author Vova Feldman (@svovaf)
15001 * @since 1.0.1
15002 *
15003 * @return bool Is running in SSL/HTTPS
15004 */
15005 function is_ssl() {
15006 return WP_FS__IS_HTTPS;
15007 }
15008
15009 /**
15010 * @author Vova Feldman (@svovaf)
15011 * @since 1.0.9
15012 *
15013 * @return bool Is running in AJAX call.
15014 *
15015 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
15016 */
15017 static function is_ajax() {
15018 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
15019 }
15020
15021 /**
15022 * Check if it's an AJAX call targeted for the current module.
15023 *
15024 * @author Vova Feldman (@svovaf)
15025 * @since 1.2.0
15026 *
15027 * @param array|string $actions Collection of AJAX actions.
15028 *
15029 * @return bool
15030 */
15031 function is_ajax_action( $actions ) {
15032 // Verify it's an ajax call.
15033 if ( ! self::is_ajax() ) {
15034 return false;
15035 }
15036
15037 // Verify the call is relevant for the plugin.
15038 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
15039 return false;
15040 }
15041
15042 // Verify it's one of the specified actions.
15043 if ( is_string( $actions ) ) {
15044 $actions = explode( ',', $actions );
15045 }
15046
15047 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15048 $ajax_action = fs_request_get( 'action' );
15049
15050 foreach ( $actions as $action ) {
15051 if ( $ajax_action === $this->get_action_tag( $action ) ) {
15052 return true;
15053 }
15054 }
15055 }
15056
15057 return false;
15058 }
15059
15060 /**
15061 * Check if it's an AJAX call targeted for current request.
15062 *
15063 * @author Vova Feldman (@svovaf)
15064 * @since 1.2.0
15065 *
15066 * @param array|string $actions Collection of AJAX actions.
15067 * @param number|null $module_id
15068 *
15069 * @return bool
15070 */
15071 static function is_ajax_action_static( $actions, $module_id = null ) {
15072 // Verify it's an ajax call.
15073 if ( ! self::is_ajax() ) {
15074 return false;
15075 }
15076
15077
15078 if ( ! empty( $module_id ) ) {
15079 // Verify the call is relevant for the plugin.
15080 if ( $module_id != fs_request_get( 'module_id' ) ) {
15081 return false;
15082 }
15083 }
15084
15085 // Verify it's one of the specified actions.
15086 if ( is_string( $actions ) ) {
15087 $actions = explode( ',', $actions );
15088 }
15089
15090 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15091 $ajax_action = fs_request_get( 'action' );
15092
15093 foreach ( $actions as $action ) {
15094 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
15095 return true;
15096 }
15097 }
15098 }
15099
15100 return false;
15101 }
15102
15103 /**
15104 * @author Vova Feldman (@svovaf)
15105 * @since 1.1.7
15106 *
15107 * @return bool
15108 */
15109 static function is_cron() {
15110 return ( defined( 'DOING_CRON' ) && DOING_CRON );
15111 }
15112
15113 /**
15114 * @author Leo Fajardo (@leorw)
15115 * @since 2.5.0
15116 *
15117 * @return bool
15118 */
15119 static function is_admin_post() {
15120 return ( 'admin-post.php' === self::get_current_page() );
15121 }
15122
15123 /**
15124 * Check if a real user is visiting the admin dashboard.
15125 *
15126 * @author Vova Feldman (@svovaf)
15127 * @since 1.1.7
15128 *
15129 * @return bool
15130 */
15131 function is_user_in_admin() {
15132 return (
15133 is_admin() &&
15134 ! self::is_ajax() &&
15135 ! self::is_cron() &&
15136 ! self::is_admin_post()
15137 );
15138 }
15139
15140 /**
15141 * Check if a real user is in the customizer view.
15142 *
15143 * @author Vova Feldman (@svovaf)
15144 * @since 1.2.2.7
15145 *
15146 * @return bool
15147 */
15148 static function is_customizer() {
15149 return is_customize_preview();
15150 }
15151
15152 /**
15153 * Check if running in HTTPS and if site's plan matching the specified plan.
15154 *
15155 * @param string $plan
15156 * @param bool $exact
15157 *
15158 * @return bool
15159 */
15160 function is_ssl_and_plan( $plan, $exact = false ) {
15161 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
15162 }
15163
15164 /**
15165 * Construct plugin's settings page URL.
15166 *
15167 * @author Vova Feldman (@svovaf)
15168 * @since 1.0.4
15169 *
15170 * @param string $page
15171 * @param array $params
15172 * @param bool|null $network
15173 *
15174 * @return string
15175 */
15176 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
15177 if ( is_null( $network ) ) {
15178 $network = (
15179 $this->_is_network_active &&
15180 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
15181 );
15182 }
15183
15184 if ( 0 < count( $params ) ) {
15185 foreach ( $params as $k => $v ) {
15186 $params[ $k ] = urlencode( $v );
15187 }
15188 }
15189
15190 $page_param = $this->_menu->get_slug( $page );
15191
15192 if ( empty( $page ) &&
15193 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
15194 $this->show_opt_in_on_themes_page()
15195 ) {
15196 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
15197
15198 return add_query_arg(
15199 $params,
15200 $this->admin_url( 'themes.php', 'admin', $network )
15201 );
15202 }
15203
15204 if ( ! $this->has_settings_menu() ) {
15205 if ( ! empty( $page ) ) {
15206 // Module doesn't have a setting page, but since the request is for
15207 // a specific Freemius page, use the admin.php path.
15208 return add_query_arg( array_merge( $params, array(
15209 'page' => $page_param,
15210 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15211 } else {
15212 if ( $this->is_activation_mode() ) {
15213 /**
15214 * @author Vova Feldman
15215 * @since 1.2.1.6
15216 *
15217 * If plugin doesn't have a settings page, create one for the opt-in screen.
15218 */
15219 return add_query_arg( array_merge( $params, array(
15220 'page' => $this->_slug,
15221 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15222 } else {
15223 // Plugin without a settings page.
15224 return add_query_arg(
15225 $params,
15226 $this->admin_url( 'plugins.php', 'admin', $network )
15227 );
15228 }
15229 }
15230 }
15231
15232 // Module has a submenu settings page.
15233 if ( ! $this->_menu->is_top_level() ) {
15234 $parent_slug = $this->_menu->get_parent_slug();
15235 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
15236 $parent_slug :
15237 'admin.php';
15238
15239 return add_query_arg( array_merge( $params, array(
15240 'page' => $page_param,
15241 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
15242 }
15243
15244 // Module has a top level CPT settings page.
15245 if ( $this->_menu->is_cpt() ) {
15246 if ( empty( $page ) && $this->is_activation_mode() ) {
15247 return add_query_arg( array_merge( $params, array(
15248 'page' => $page_param
15249 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15250 } else {
15251 if ( ! empty( $page ) ) {
15252 $params['page'] = $page_param;
15253 }
15254
15255 return add_query_arg(
15256 $params,
15257 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
15258 );
15259 }
15260 }
15261
15262 // Module has a custom top level settings page.
15263 return add_query_arg( array_merge( $params, array(
15264 'page' => $page_param,
15265 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15266 }
15267
15268 #--------------------------------------------------------------------------------
15269 #region Multisite
15270 #--------------------------------------------------------------------------------
15271
15272 /**
15273 * @author Leo Fajardo (@leorw)
15274 * @since 2.0.0
15275 *
15276 * @return bool
15277 */
15278 function is_network_active() {
15279 return $this->_is_network_active;
15280 }
15281
15282 /**
15283 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
15284 *
15285 * @author Leo Fajardo (@leorw)
15286 * @since 2.0.0
15287 *
15288 * @param bool|int[] $all_or_blog_ids
15289 */
15290 private function delegate_connection( $all_or_blog_ids = true ) {
15291 $this->_logger->entrance();
15292
15293 $this->_admin_notices->remove_sticky( 'connect_account' );
15294
15295 if ( true === $all_or_blog_ids ) {
15296 // All sites delegation.
15297 $this->_storage->store( 'is_delegated_connection', true, true );
15298 } else {
15299 // Specified sites delegation.
15300 foreach ( $all_or_blog_ids as $blog_id ) {
15301 $this->delegate_site_connection( $blog_id );
15302 }
15303 }
15304
15305 $this->network_upgrade_mode_completed();
15306 }
15307
15308 /**
15309 * Delegate specific network site conncetion to the site admin.
15310 *
15311 * @author Vova Feldman (@svovaf)
15312 * @since 2.0.0
15313 *
15314 * @param int $blog_id
15315 */
15316 private function delegate_site_connection( $blog_id ) {
15317 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
15318 }
15319
15320 /**
15321 * Check if super-admin delegated the connection of ALL sites to the site admins.
15322 *
15323 * @author Vova Feldman (@svovaf)
15324 * @since 2.0.0
15325 *
15326 * @return bool
15327 */
15328 function is_network_delegated_connection() {
15329 if ( ! $this->_is_network_active ) {
15330 return false;
15331 }
15332
15333 return $this->_storage->get( 'is_delegated_connection', false, true );
15334 }
15335
15336 /**
15337 * @author Leo Fajardo (@leorw)
15338 * @since 2.0.0
15339 *
15340 * @param int $blog_id
15341 *
15342 * @return bool
15343 */
15344 function is_site_delegated_connection( $blog_id = 0 ) {
15345 if ( ! $this->_is_network_active ) {
15346 return false;
15347 }
15348
15349 if ( 0 == $blog_id ) {
15350 $blog_id = get_current_blog_id();
15351 }
15352
15353 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15354 }
15355
15356 /**
15357 * Check if delegated the connection. When running within the network admin,
15358 * and haven't specified the blog ID, checks if network level delegated. If running
15359 * within a site admin or specified a blog ID, check if delegated the connection for
15360 * the current context site.
15361 *
15362 * If executed outside the the admin, check if delegated the connection
15363 * for the current context site OR the whole network.
15364 *
15365 * @author Vova Feldman (@svovaf)
15366 * @since 2.0.0
15367 *
15368 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15369 *
15370 * @return bool
15371 */
15372 function is_delegated_connection( $blog_id = 0 ) {
15373 if ( ! $this->_is_network_active ) {
15374 return false;
15375 }
15376
15377 if ( fs_is_network_admin() && 0 == $blog_id ) {
15378 return $this->is_network_delegated_connection();
15379 }
15380
15381 return (
15382 $this->is_network_delegated_connection() ||
15383 $this->is_site_delegated_connection( $blog_id )
15384 );
15385 }
15386
15387 /**
15388 * Check if the current module is active for the site.
15389 *
15390 * @author Vova Feldman (@svovaf)
15391 * @since 2.0.0
15392 *
15393 * @param int $blog_id
15394 *
15395 * @return bool
15396 */
15397 function is_active_for_site( $blog_id ) {
15398 if ( ! is_multisite() ) {
15399 // Not a multisite and this code is executed, means that the plugin is active.
15400 return true;
15401 }
15402
15403 if ( $this->is_theme() ) {
15404 // All themes are site level activated.
15405 return true;
15406 }
15407
15408 if ( $this->_is_network_active ) {
15409 // Plugin was network activated so it's active.
15410 return true;
15411 }
15412
15413 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15414 }
15415
15416 /**
15417 * @todo Implement pagination when accessing the subsites collection.
15418 *
15419 * @author Leo Fajardo (@leorw)
15420 * @since 2.0.0
15421 *
15422 * @param int $limit Default to 1,000
15423 * @param int $offset Default to 0
15424 *
15425 * @return array Active & public sites collection.
15426 */
15427 static function get_sites( $limit = 1000, $offset = 0 ) {
15428 if ( ! is_multisite() ) {
15429 return array();
15430 }
15431
15432 /**
15433 * For consistency with get_blog_list() which only return active public sites.
15434 *
15435 * @author Vova Feldman (@svovaf)
15436 */
15437 $args = array(
15438 /**
15439 * Commented out in order to handle the migration of site options whether the site is public or not.
15440 *
15441 * @author Leo Fajardo (@leorw)
15442 * @since 2.2.1
15443 */
15444 // 'public' => 1,
15445 'archived' => 0,
15446 'mature' => 0,
15447 'spam' => 0,
15448 'deleted' => 0,
15449 'number' => $limit,
15450 'offset' => $offset,
15451 );
15452
15453 return get_sites( $args );
15454 }
15455
15456 /**
15457 * Checks if a given blog is active.
15458 *
15459 * @author Vova Feldman (@svovaf)
15460 * @since 2.0.0
15461 *
15462 * @param $blog_id
15463 *
15464 * @return bool
15465 */
15466 private static function is_site_active( $blog_id ) {
15467 global $wpdb;
15468
15469 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15470
15471 if ( ! is_object( $blog_info ) ) {
15472 return false;
15473 }
15474
15475 return (
15476 true == $blog_info->public &&
15477 false == $blog_info->archived &&
15478 false == $blog_info->mature &&
15479 false == $blog_info->spam &&
15480 false == $blog_info->deleted
15481 );
15482 }
15483
15484 /**
15485 * Get a mapping between the site addresses to their blog IDs.
15486 *
15487 * @author Vova Feldman (@svovaf)
15488 * @since 2.0.0
15489 *
15490 * @return array {
15491 * @key string Site address without protocol with a trailing slash.
15492 * @value int Site's blog ID.
15493 * }
15494 */
15495 private function get_address_to_blog_map() {
15496 $sites = self::get_sites();
15497
15498 // Map site addresses to their blog IDs.
15499 $address_to_blog_map = array();
15500 foreach ( $sites as $site ) {
15501 $blog_id = self::get_site_blog_id( $site );
15502 $address = self::get_unfiltered_site_url( $blog_id, true, true );
15503 $address_to_blog_map[ $address ] = $blog_id;
15504 }
15505
15506 return $address_to_blog_map;
15507 }
15508
15509 /**
15510 * Get a mapping between the site addresses to their blog IDs.
15511 *
15512 * @author Vova Feldman (@svovaf)
15513 * @since 2.0.0
15514 *
15515 * @return array {
15516 * @key int Site's blog ID.
15517 * @value FS_Site Associated install.
15518 * }
15519 */
15520 function get_blog_install_map() {
15521 $sites = self::get_sites();
15522
15523 // Map site blog ID to its install.
15524 $install_map = array();
15525
15526 foreach ( $sites as $site ) {
15527 $blog_id = self::get_site_blog_id( $site );
15528 $install = $this->get_install_by_blog_id( $blog_id );
15529
15530 if ( is_object( $install ) ) {
15531 $install_map[ $blog_id ] = $install;
15532 }
15533 }
15534
15535 return $install_map;
15536 }
15537
15538 /**
15539 * @author Vova Feldman (@svovaf)
15540 * @since 2.5.1
15541 *
15542 * @param bool|null $is_delegated When `true`, returns only connection delegated blog IDs. When `false`, only non-delegated blog IDs.
15543 *
15544 * @return int[]
15545 */
15546 private function get_blog_ids( $is_delegated = null ) {
15547 $blog_ids = array();
15548
15549 $sites = self::get_sites();
15550 foreach ( $sites as $site ) {
15551 $blog_id = self::get_site_blog_id( $site );
15552
15553 if (
15554 is_null( $is_delegated ) ||
15555 $is_delegated === $this->is_site_delegated_connection( $blog_id )
15556 ) {
15557 $blog_ids[] = $blog_id;
15558 }
15559 }
15560
15561 return $blog_ids;
15562 }
15563
15564 /**
15565 * @author Vova Feldman (@svovaf)
15566 * @since 2.5.1
15567 *
15568 * @return int[]
15569 */
15570 private function get_non_delegated_blog_ids() {
15571 return $this->get_blog_ids( false );
15572 }
15573
15574 /**
15575 * Gets a map of module IDs that the given user has opted-in to.
15576 *
15577 * @author Leo Fajardo (@leorw)
15578 * @since 2.1.0
15579 *
15580 * @param number $fs_user_id
15581 *
15582 * @return array {
15583 * @key number $plugin_id
15584 * @value bool Always true.
15585 * }
15586 */
15587 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15588 self::$_static_logger->entrance();
15589
15590 if ( ! is_multisite() ) {
15591 $installs = array_merge(
15592 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15593 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15594 );
15595 } else {
15596 $sites = self::get_sites();
15597
15598 $installs = array();
15599 foreach ( $sites as $site ) {
15600 $blog_id = self::get_site_blog_id( $site );
15601
15602 $installs = array_merge(
15603 $installs,
15604 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15605 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15606 );
15607 }
15608 }
15609
15610 $module_ids_map = array();
15611 foreach ( $installs as $install ) {
15612 if ( is_object( $install ) &&
15613 FS_Site::is_valid_id( $install->id ) &&
15614 FS_User::is_valid_id( $install->user_id ) &&
15615 ( $install->user_id == $fs_user_id )
15616 ) {
15617 $module_ids_map[ $install->plugin_id ] = true;
15618 }
15619 }
15620
15621 return $module_ids_map;
15622 }
15623
15624 /**
15625 * @author Leo Fajardo (@leorw)
15626 *
15627 * @return null|array {
15628 * 'install' => FS_Site Module's install,
15629 * 'blog_id' => string The associated blog ID.
15630 * }
15631 */
15632 function find_first_install() {
15633 $sites = self::get_sites();
15634
15635 foreach ( $sites as $site ) {
15636 $blog_id = self::get_site_blog_id( $site );
15637 $install = $this->get_install_by_blog_id( $blog_id );
15638
15639 if ( is_object( $install ) ) {
15640 return array(
15641 'install' => $install,
15642 'blog_id' => $blog_id
15643 );
15644 }
15645 }
15646
15647 return null;
15648 }
15649
15650 /**
15651 * Switches the Freemius site level context to a specified blog.
15652 *
15653 * @author Vova Feldman (@svovaf)
15654 * @since 2.0.0
15655 *
15656 * @param int $blog_id
15657 * @param FS_Site $install
15658 * @param bool $flush
15659 *
15660 * @return bool Since 2.3.1 returns if a switch was made.
15661 */
15662 function switch_to_blog( $blog_id, $install = null, $flush = false ) {
15663 if ( ! is_numeric( $blog_id ) ) {
15664 return false;
15665 }
15666
15667 if ( ! $flush && $blog_id == $this->_context_is_network_or_blog_id ) {
15668 return false;
15669 }
15670
15671 switch_to_blog( $blog_id );
15672 $this->_context_is_network_or_blog_id = $blog_id;
15673
15674 self::$_accounts->set_site_blog_context( $blog_id );
15675 $this->_storage->set_site_blog_context( $blog_id );
15676 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
15677
15678 $this->_site = is_object( $install ) ?
15679 $install :
15680 $this->get_install_by_blog_id( $blog_id );
15681
15682 $this->_user = false;
15683 $this->_licenses = false;
15684 $this->_license = null;
15685 $this->is_whitelabeled = null;
15686
15687 if ( is_object( $this->_site ) ) {
15688 // Try to fetch user from install.
15689 $this->_user = self::_get_user_by_id( $this->_site->user_id );
15690
15691 if ( ! is_object( $this->_user ) &&
15692 FS_User::is_valid_id( $this->_storage->prev_user_id )
15693 ) {
15694 // Try to fetch previously saved user.
15695 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
15696
15697 if ( ! is_object( $this->_user ) ) {
15698 // Fallback to network's user.
15699 $this->_user = $this->get_network_user();
15700 }
15701 }
15702
15703 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
15704
15705 if ( ! empty( $all_plugin_licenses ) ) {
15706 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
15707 $this->_license = null;
15708 } else {
15709 $license_found = false;
15710 foreach ( $all_plugin_licenses as $license ) {
15711 if ( $license->id == $this->_site->license_id ) {
15712 // License found.
15713 $this->_license = $license;
15714 $license_found = true;
15715 break;
15716 }
15717 }
15718
15719 if ( $license_found ) {
15720 $this->link_license_2_user( $this->_license->id, $this->_user->id );
15721 }
15722 }
15723
15724 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15725 }
15726 }
15727
15728 unset( $this->_site_api );
15729 unset( $this->_user_api );
15730
15731 return true;
15732 }
15733
15734 /**
15735 * Restore the blog context to the blog that originally loaded the module.
15736 *
15737 * @author Vova Feldman (@svovaf)
15738 * @since 2.0.0
15739 */
15740 function restore_current_blog() {
15741 $this->switch_to_blog( $this->_blog_id );
15742 }
15743
15744 /**
15745 * @author Vova Feldman (@svovaf)
15746 * @since 2.0.0
15747 *
15748 * @param array|WP_Site $site
15749 *
15750 * @return int
15751 */
15752 static function get_site_blog_id( &$site ) {
15753 return ( $site instanceof WP_Site ) ?
15754 $site->blog_id :
15755 ( is_object( $site ) && isset( $site->userblog_id ) ?
15756 $site->userblog_id :
15757 $site['blog_id'] );
15758 }
15759
15760 /**
15761 * @author Vova Feldman (@svovaf)
15762 * @since 2.5.1
15763 *
15764 * @param WP_Site[]|array[] $sites
15765 *
15766 * @return int[]
15767 */
15768 static function get_sites_blog_ids( $sites ) {
15769 $blog_ids = array();
15770 foreach ( $sites as $site ) {
15771 $blog_ids[] = self::get_site_blog_id( $site );
15772 }
15773
15774 return $blog_ids;
15775 }
15776
15777 /**
15778 * @author Leo Fajardo (@leorw)
15779 * @since 2.0.0
15780 *
15781 * @param array|WP_Site|null $site
15782 * @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.
15783 *
15784 * @return array
15785 */
15786 function get_site_info( $site = null, $load_registration = false ) {
15787 $this->_logger->entrance();
15788
15789 $fs_hook_snapshot = new FS_Hook_Snapshot();
15790 // Remove all filters from `switch_blog`.
15791 $fs_hook_snapshot->remove( 'switch_blog' );
15792
15793 $switched = false;
15794
15795 $registration_date = null;
15796
15797 if ( is_null( $site ) ) {
15798 $url = self::get_unfiltered_site_url();
15799 $name = get_bloginfo( 'name' );
15800 $blog_id = null;
15801 } else {
15802 $blog_id = self::get_site_blog_id( $site );
15803
15804 if ( get_current_blog_id() != $blog_id ) {
15805 switch_to_blog( $blog_id );
15806 $switched = true;
15807 }
15808
15809 if ( $site instanceof WP_Site ) {
15810 $url = $site->siteurl;
15811 $name = $site->blogname;
15812 $registration_date = $site->registered;
15813 } else {
15814 $url = self::get_unfiltered_site_url( $blog_id );
15815 $name = get_bloginfo( 'name' );
15816 }
15817 }
15818
15819 if ( empty( $registration_date ) && $load_registration ) {
15820 $blog_details = get_blog_details( $blog_id, false );
15821
15822 if ( is_object( $blog_details ) && isset( $blog_details->registered ) ) {
15823 $registration_date = $blog_details->registered;
15824 }
15825 }
15826
15827 $info = array(
15828 'uid' => $this->get_anonymous_id( $blog_id ),
15829 'url' => $url,
15830 );
15831
15832 // Add these diagnostic information only if user allowed to track.
15833 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
15834 $info = array_merge( $info, array(
15835 'title' => $name,
15836 'language' => self::get_sanitized_language(),
15837 ) );
15838 }
15839
15840 if ( is_numeric( $blog_id ) ) {
15841 $info['blog_id'] = $blog_id;
15842 }
15843
15844 if ( ! empty( $registration_date ) ) {
15845 $info[ 'registration_date' ] = $registration_date;
15846 }
15847
15848 if ( $switched ) {
15849 restore_current_blog();
15850 }
15851
15852 // Add the filters back to `switch_blog`.
15853 $fs_hook_snapshot->restore( 'switch_blog' );
15854
15855 return $info;
15856 }
15857
15858 /**
15859 * Load the module's install based on the blog ID.
15860 *
15861 * @author Vova Feldman (@svovaf)
15862 * @since 2.0.0
15863 *
15864 * @param int|null $blog_id
15865 *
15866 * @return FS_Site
15867 */
15868 function get_install_by_blog_id( $blog_id = null ) {
15869 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15870 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15871
15872 if ( is_object( $install ) &&
15873 is_numeric( $install->id ) &&
15874 is_numeric( $install->user_id ) &&
15875 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15876 ) {
15877 // Load site.
15878 $install = clone $install;
15879 }
15880
15881 return $install;
15882 }
15883
15884 /**
15885 * Check if module is installed on a specified site.
15886 *
15887 * @author Vova Feldman (@svovaf)
15888 * @since 2.0.0
15889 *
15890 * @param int|null $blog_id
15891 *
15892 * @return bool
15893 */
15894 function is_installed_on_site( $blog_id = null ) {
15895 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15896 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15897
15898 return (
15899 is_object( $install ) &&
15900 is_numeric( $install->id ) &&
15901 is_numeric( $install->user_id ) &&
15902 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15903 );
15904 }
15905
15906 /**
15907 * Check if super-admin connected at least one site via the network opt-in.
15908 *
15909 * @author Vova Feldman (@svovaf)
15910 * @since 2.0.0
15911 *
15912 * @return bool
15913 */
15914 function is_network_registered() {
15915 if ( ! $this->_is_network_active ) {
15916 return false;
15917 }
15918
15919 return FS_User::is_valid_id( $this->_storage->network_user_id );
15920 }
15921
15922 /**
15923 * Returns the main user associated with the network.
15924 *
15925 * @author Vova Feldman (@svovaf)
15926 * @since 2.0.0
15927 *
15928 * @return FS_User
15929 */
15930 function get_network_user() {
15931 if ( ! $this->_is_network_active ) {
15932 return null;
15933 }
15934
15935 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
15936 self::_get_user_by_id( $this->_storage->network_user_id ) :
15937 null;
15938 }
15939
15940 /**
15941 * Returns the current context user or the network's main user.
15942 *
15943 * @author Vova Feldman (@svovaf)
15944 * @since 2.0.0
15945 *
15946 * @return FS_User
15947 */
15948 function get_current_or_network_user() {
15949 return ( $this->_user instanceof FS_User ) ?
15950 $this->_user :
15951 $this->get_network_user();
15952 }
15953
15954 /**
15955 * Returns the main install associated with the network.
15956 *
15957 * @author Vova Feldman (@svovaf)
15958 * @since 2.0.0
15959 *
15960 * @return FS_Site
15961 */
15962 function get_network_install() {
15963 if ( ! $this->_is_network_active ) {
15964 return null;
15965 }
15966
15967 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15968 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
15969 null;
15970 }
15971
15972 /**
15973 * Returns the blog ID that is associated with the main install.
15974 *
15975 * @author Leo Fajardo (@leorw)
15976 * @since 2.0.0
15977 *
15978 * @return int|null
15979 */
15980 function get_network_install_blog_id() {
15981 if ( ! $this->_is_network_active ) {
15982 return null;
15983 }
15984
15985 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15986 $this->_storage->network_install_blog_id :
15987 null;
15988 }
15989
15990 /**
15991 * Returns the current context install or the network's main install.
15992 *
15993 * @author Vova Feldman (@svovaf)
15994 * @since 2.0.0
15995 *
15996 * @return FS_Site
15997 */
15998 function get_current_or_network_install() {
15999 return ( $this->_site instanceof FS_Site ) ?
16000 $this->_site :
16001 $this->get_network_install();
16002 }
16003
16004 /**
16005 * Check if executing a site level action from the network level admin.
16006 *
16007 * @author Vova Feldman (@svovaf)
16008 * @since 2.0.0
16009 *
16010 * @return false|int If yes, return the requested blog ID.
16011 */
16012 private function is_network_level_site_specific_action() {
16013 if ( ! $this->_is_network_active ) {
16014 return false;
16015 }
16016
16017 if ( ! fs_is_network_admin() ) {
16018 return false;
16019 }
16020
16021 $blog_id = fs_request_get( 'blog_id', '' );
16022
16023 return is_numeric( $blog_id ) ? $blog_id : false;
16024 }
16025
16026 /**
16027 * Check if executing an action from the network level admin.
16028 *
16029 * @author Vova Feldman (@svovaf)
16030 * @since 2.0.0
16031 *
16032 * @return bool
16033 */
16034 private function is_network_level_action() {
16035 return ( $this->_is_network_active && fs_is_network_admin() );
16036 }
16037
16038 /**
16039 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
16040 * 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.
16041 *
16042 * @author Vova Feldman (@svovaf)
16043 * @since 2.0.0
16044 *
16045 * @param int $context_blog_id
16046 */
16047 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
16048 $this->_logger->entrance();
16049
16050 if ( $this->_is_network_active ) {
16051 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
16052 $installs_map = $this->get_blog_install_map();
16053
16054 foreach ( $installs_map as $blog_id => $install ) {
16055 /**
16056 * @var FS_Site $install
16057 */
16058 if ( $context_blog_id == $blog_id ) {
16059 continue;
16060 }
16061
16062 if ( $install->user_id != $this->_storage->network_user_id ) {
16063 continue;
16064 }
16065
16066 // Switch reference to a blog that is opted-in and belong to the same super-admin.
16067 $this->_storage->network_install_blog_id = $blog_id;
16068 break;
16069 }
16070 }
16071 }
16072
16073 if ( ! $this->is_registered() ) {
16074 return;
16075 }
16076
16077 if ( $this->is_sync_cron_scheduled() &&
16078 $context_blog_id == $this->get_sync_cron_blog_id()
16079 ) {
16080 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
16081 }
16082
16083 if ( $this->is_install_sync_scheduled() &&
16084 $context_blog_id == $this->get_install_sync_cron_blog_id()
16085 ) {
16086 $this->maybe_schedule_install_sync_cron( $context_blog_id );
16087 }
16088 }
16089
16090 /**
16091 * Executed after site deactivation, archive, or flag as spam.
16092 *
16093 * @author Vova Feldman (@svovaf)
16094 * @since 2.0.0
16095 *
16096 * @param int $context_blog_id
16097 */
16098 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
16099 $this->_logger->entrance();
16100
16101 $install = $this->get_install_by_blog_id( $context_blog_id );
16102
16103 if ( ! is_object( $install ) ) {
16104 // Site not connected.
16105 return;
16106 }
16107
16108 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16109
16110 if ( ! $this->is_registered() ) {
16111 return;
16112 }
16113
16114 $current_blog_id = get_current_blog_id();
16115
16116 $this->switch_to_blog( $context_blog_id );
16117
16118 // Send deactivation event.
16119 $this->sync_install( array(
16120 'is_active' => false,
16121 ) );
16122
16123 $this->switch_to_blog( $current_blog_id );
16124 }
16125
16126 /**
16127 * Executed after site deletion.
16128 *
16129 * @author Vova Feldman (@svovaf)
16130 * @since 2.0.0
16131 *
16132 * @param int $context_blog_id
16133 * @param bool $drop True if site's database tables should be dropped. Default is false.
16134 */
16135 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
16136 $this->_logger->entrance();
16137
16138 $install = $this->get_install_by_blog_id( $context_blog_id );
16139
16140 if ( ! is_object( $install ) ) {
16141 // Site not connected.
16142 return;
16143 }
16144
16145 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16146
16147 if ( ! $this->is_registered() ) {
16148 return;
16149 }
16150
16151 $current_blog_id = get_current_blog_id();
16152
16153 $this->switch_to_blog( $context_blog_id );
16154
16155 if ( $drop ) {
16156 // Delete install if dropping site DB.
16157 $this->delete_account_event();
16158 } else {
16159 // Send deactivation event.
16160 $this->sync_install( array(
16161 'is_active' => false,
16162 ) );
16163 }
16164
16165 $this->switch_to_blog( $current_blog_id );
16166 }
16167
16168 /**
16169 * Executed after site deletion, called from wp_delete_site
16170 *
16171 * @author Dario Curvino (@dudo)
16172 * @since 2.5.0
16173 *
16174 * @param WP_Site $old_site
16175 */
16176 public function _after_wpsite_deleted_callback( WP_Site $old_site ) {
16177 $this->_logger->entrance();
16178
16179 $this->_after_site_deleted_callback( $old_site->blog_id, true );
16180 }
16181
16182 /**
16183 * Executed after site re-activation.
16184 *
16185 * @author Vova Feldman (@svovaf)
16186 * @since 2.0.0
16187 *
16188 * @param int $context_blog_id
16189 */
16190 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
16191 $this->_logger->entrance();
16192
16193 $install = $this->get_install_by_blog_id( $context_blog_id );
16194
16195 if ( ! is_object( $install ) ) {
16196 // Site not connected.
16197 return;
16198 }
16199
16200 if ( ! self::is_site_active( $context_blog_id ) ) {
16201 // Site not yet active (can be in spam mode, archived, deleted...).
16202 return;
16203 }
16204
16205 $current_blog_id = get_current_blog_id();
16206
16207 $this->switch_to_blog( $context_blog_id );
16208
16209 // Send re-activation event.
16210 $this->sync_install( array(
16211 'is_active' => true,
16212 ) );
16213
16214 $this->switch_to_blog( $current_blog_id );
16215 }
16216
16217 #endregion Multisite
16218
16219 /**
16220 * @author Leo Fajardo (@leorw)
16221 *
16222 * @param string $path
16223 * @param string $scheme
16224 * @param bool $network
16225 *
16226 * @return string
16227 */
16228 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
16229 return ( $this->_is_network_active && $network ) ?
16230 network_admin_url( $path, $scheme ) :
16231 admin_url( $path, $scheme );
16232 }
16233
16234 /**
16235 * Check if currently in a specified admin page.
16236 *
16237 * @author Vova Feldman (@svovaf)
16238 * @since 1.2.2.7
16239 *
16240 * @param string $page
16241 *
16242 * @return bool
16243 */
16244 function is_admin_page( $page ) {
16245 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
16246 }
16247
16248 /**
16249 * Check if currently in the product's main admin page.
16250 *
16251 * @author Vova Feldman (@svovaf)
16252 * @since 2.3.1
16253 *
16254 * @return bool
16255 */
16256 function is_main_admin_page() {
16257 return $this->is_admin_page( '' );
16258 }
16259
16260 /**
16261 * Get module's main admin setting page URL.
16262 *
16263 * @author Vova Feldman (@svovaf)
16264 * @since 1.2.2.7
16265 *
16266 * @return string
16267 */
16268 function main_menu_url() {
16269 return $this->_menu->main_menu_url();
16270 }
16271
16272 /**
16273 * Check if currently on the theme's setting page or
16274 * on any of the Freemius added pages (via tabs).
16275 *
16276 * @author Vova Feldman (@svovaf)
16277 * @since 1.2.2.7
16278 *
16279 * @return bool
16280 *
16281 * @deprecated Please use is_product_settings_page() instead;
16282 */
16283 function is_theme_settings_page() {
16284 return $this->is_product_settings_page();
16285 }
16286
16287 /**
16288 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
16289 *
16290 * @author Vova Feldman (@svovaf)
16291 * @since 1.2.2.7
16292 *
16293 * @return bool
16294 */
16295 function is_product_settings_page() {
16296 $page = fs_request_get( 'page', '', 'get' );
16297 $menu_slug = $this->_menu->get_slug();
16298
16299 if ( $page === $menu_slug ) {
16300 return true;
16301 }
16302
16303 return fs_starts_with(
16304 // e.g., {$menu_slug}-account, {$menu_slug}-affiliation, etc.
16305 $page,
16306 ( $menu_slug . '-' )
16307 );
16308 }
16309
16310 /**
16311 * Plugin's account page + sync license URL.
16312 *
16313 * @author Vova Feldman (@svovaf)
16314 * @since 1.1.9.1
16315 *
16316 * @param bool|number $plugin_id
16317 * @param bool $add_action_nonce
16318 * @param array $params
16319 *
16320 * @return string
16321 */
16322 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
16323 if ( is_numeric( $plugin_id ) ) {
16324 $params['plugin_id'] = $plugin_id;
16325 }
16326
16327 return $this->get_account_url(
16328 $this->get_unique_affix() . '_sync_license',
16329 $params,
16330 $add_action_nonce
16331 );
16332 }
16333
16334 /**
16335 * Plugin's account URL.
16336 *
16337 * @author Vova Feldman (@svovaf)
16338 * @since 1.0.4
16339 *
16340 * @param bool|string $action
16341 * @param array $params
16342 *
16343 * @param bool $add_action_nonce
16344 *
16345 * @return string
16346 */
16347 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
16348 if ( is_string( $action ) ) {
16349 $params['fs_action'] = $action;
16350 }
16351
16352 self::require_pluggable_essentials();
16353
16354 return ( $add_action_nonce && is_string( $action ) ) ?
16355 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
16356 $this->_get_admin_page_url( 'account', $params );
16357 }
16358
16359 /**
16360 * @author Vova Feldman (@svovaf)
16361 * @since 1.2.0
16362 *
16363 * @param string $tab
16364 * @param bool $action
16365 * @param array $params
16366 * @param bool $add_action_nonce
16367 *
16368 * @return string
16369 *
16370 * @uses get_account_url()
16371 */
16372 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
16373 $params['tab'] = $tab;
16374
16375 return $this->get_account_url( $action, $params, $add_action_nonce );
16376 }
16377
16378 /**
16379 * Plugin's account URL.
16380 *
16381 * @author Vova Feldman (@svovaf)
16382 * @since 1.0.4
16383 *
16384 * @param bool|string $topic
16385 * @param bool|string $message
16386 * @param bool|string $summary Since 2.5.1.
16387 *
16388 * @return string
16389 */
16390 function contact_url( $topic = false, $message = false, $summary = false ) {
16391 $params = array();
16392 if ( is_string( $topic ) ) {
16393 $params['topic'] = $topic;
16394 }
16395 if ( is_string( $message ) ) {
16396 $params['message'] = $message;
16397 }
16398
16399 if ( is_string( $summary ) ) {
16400 $params['summary'] = $summary;
16401 }
16402
16403 if ( $this->is_addon() ) {
16404 $params['addon_id'] = $this->get_id();
16405
16406 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
16407 } else {
16408 return $this->_get_admin_page_url( 'contact', $params );
16409 }
16410 }
16411
16412 /**
16413 * Add-on direct info URL.
16414 *
16415 * @author Vova Feldman (@svovaf)
16416 * @since 1.1.0
16417 *
16418 * @param string $slug
16419 *
16420 * @return string
16421 */
16422 function addon_url( $slug ) {
16423 return $this->_get_admin_page_url( 'addons', array(
16424 'slug' => $slug
16425 ) );
16426 }
16427
16428 /**
16429 * Add-ons URL.
16430 *
16431 * @author Vova Feldman (@svovaf)
16432 * @since 2.4.5
16433 *
16434 * @return string
16435 */
16436 function get_addons_url() {
16437 return $this->_get_admin_page_url( 'addons' );
16438 }
16439
16440 /* Logger
16441 ------------------------------------------------------------------------------------------------------------------*/
16442 /**
16443 * @param string $id
16444 * @param bool $prefix_slug
16445 *
16446 * @return FS_Logger
16447 */
16448 function get_logger( $id = '', $prefix_slug = true ) {
16449 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16450 }
16451
16452 /**
16453 * Note: This method is used externally so don't delete it.
16454 *
16455 * @param $id
16456 * @param bool $load_options
16457 * @param bool $prefix_slug
16458 *
16459 * @return FS_Option_Manager
16460 */
16461 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16462 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16463 }
16464
16465 /* Security
16466 ------------------------------------------------------------------------------------------------------------------*/
16467 private static function _encrypt( $str ) {
16468 if ( is_null( $str ) ) {
16469 return null;
16470 }
16471
16472 /**
16473 * The encrypt/decrypt functions are used to protect
16474 * the user from messing up with some of the sensitive
16475 * data stored for the module as a JSON in the database.
16476 *
16477 * I used the same suggested hack by the theme review team.
16478 * For more details, look at the function `Base64UrlDecode()`
16479 * in `./sdk/FreemiusBase.php`.
16480 *
16481 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16482 *
16483 * @author Vova Feldman (@svovaf)
16484 * @since 1.2.2
16485 */
16486 $fn = 'base64' . '_encode';
16487
16488 return $fn( $str );
16489 }
16490
16491 static function _decrypt( $str ) {
16492 if ( is_null( $str ) ) {
16493 return null;
16494 }
16495
16496 /**
16497 * The encrypt/decrypt functions are used to protect
16498 * the user from messing up with some of the sensitive
16499 * data stored for the module as a JSON in the database.
16500 *
16501 * I used the same suggested hack by the theme review team.
16502 * For more details, look at the function `Base64UrlDecode()`
16503 * in `./sdk/FreemiusBase.php`.
16504 *
16505 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16506 *
16507 * @author Vova Feldman (@svovaf)
16508 * @since 1.2.2
16509 */
16510 $fn = 'base64' . '_decode';
16511
16512 return $fn( $str );
16513 }
16514
16515 /**
16516 * @author Vova Feldman (@svovaf)
16517 * @since 1.0.5
16518 *
16519 * @param FS_Entity $entity
16520 *
16521 * @return FS_Entity Return an encrypted clone entity.
16522 */
16523 private static function _encrypt_entity( FS_Entity $entity ) {
16524 $clone = clone $entity;
16525 $props = get_object_vars( $entity );
16526
16527 foreach ( $props as $key => $val ) {
16528 $clone->{$key} = self::_encrypt( $val );
16529 }
16530
16531 return $clone;
16532 }
16533
16534 /**
16535 * @author Vova Feldman (@svovaf)
16536 * @since 1.0.5
16537 *
16538 * @param FS_Entity $entity
16539 *
16540 * @return FS_Entity Return an decrypted clone entity.
16541 */
16542 private static function decrypt_entity( FS_Entity $entity ) {
16543 $clone = clone $entity;
16544 $props = get_object_vars( $entity );
16545
16546 foreach ( $props as $key => $val ) {
16547 $clone->{$key} = self::_decrypt( $val );
16548 }
16549
16550 return $clone;
16551 }
16552
16553 /**
16554 * @author Vova Feldman (@svovaf)
16555 * @since 1.0.7
16556 *
16557 * @param string $email
16558 *
16559 * @return FS_User|false
16560 */
16561 public static function _get_user_by_email( $email ) {
16562 self::$_static_logger->entrance();
16563
16564 $email = trim( strtolower( $email ) );
16565
16566 $users = self::get_all_users();
16567
16568 if ( is_array( $users ) ) {
16569 foreach ( $users as $user ) {
16570 if ( $email === trim( strtolower( $user->email ) ) ) {
16571 return $user;
16572 }
16573 }
16574 }
16575
16576 return false;
16577 }
16578
16579 #----------------------------------------------------------------------------------
16580 #region Account (Loading, Updates & Activation)
16581 #----------------------------------------------------------------------------------
16582
16583 /***
16584 * Load account information (user + site).
16585 *
16586 * @author Vova Feldman (@svovaf)
16587 * @since 1.0.1
16588 */
16589 private function _load_account() {
16590 $this->_logger->entrance();
16591
16592 $this->do_action( 'before_account_load' );
16593
16594 $users = self::get_all_users();
16595 $plans = self::get_all_plans( $this->_module_type );
16596
16597 if ( $this->_logger->is_on() && is_admin() ) {
16598 $this->_logger->log( 'users = ' . var_export( $users, true ) );
16599 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
16600 }
16601
16602 $site = fs_is_network_admin() ?
16603 $this->get_network_install() :
16604 $this->get_install_by_blog_id();
16605
16606 if ( fs_is_network_admin() &&
16607 $this->is_network_active() &&
16608 ! is_object( $site ) &&
16609 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
16610 ) {
16611 $first_install = $this->find_first_install();
16612
16613 if ( is_null( $first_install ) ) {
16614 unset( $this->_storage->network_install_blog_id );
16615 } else {
16616 $site = $first_install['install'];
16617 $this->_storage->network_install_blog_id = $first_install['blog_id'];
16618 }
16619 }
16620
16621 if ( is_object( $site ) &&
16622 is_numeric( $site->id ) &&
16623 is_numeric( $site->user_id ) &&
16624 FS_Plugin_Plan::is_valid_id( $site->plan_id )
16625 ) {
16626 // Load site.
16627 $this->_site = $site;
16628 }
16629
16630 $user = null;
16631 if ( fs_is_network_admin() && $this->_is_network_active ) {
16632 $user = $this->get_network_user();
16633 }
16634
16635 if ( is_object( $user ) ) {
16636 $this->_user = clone $user;
16637 } else if ( $this->_site ) {
16638 $user = self::_get_user_by_id( $this->_site->user_id );
16639
16640 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
16641 /**
16642 * Try to load the previous owner. This recovery is used for the following use-case:
16643 * 1. Opt-in
16644 * 2. Cloning site1 to site2
16645 * 3. Ownership switch in site1 (same applies for site2)
16646 * 4. Install data sync on site2
16647 * 5. Now site2's install is associated with the new owner which does not exists locally.
16648 */
16649 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
16650 }
16651
16652 if ( ! is_object( $user ) ) {
16653 /**
16654 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
16655 */
16656 if (
16657 ! isset( $this->_storage->user_recovery_from_install_last_attempt_timestamp ) ||
16658 time() > ( $this->_storage->user_recovery_from_install_last_attempt_timestamp + FS_Clone_Manager::CLONE_RESOLUTION_MAX_EXECUTION_TIME )
16659 ) {
16660 $user = $this->sync_user_by_current_install();
16661 } else {
16662 return;
16663 }
16664
16665 if ( is_object( $user ) ) {
16666 $this->_storage->user_was_recovered_from_install = true;
16667 } else {
16668 $this->_storage->user_recovery_from_install_attempts = isset( $this->_storage->user_recovery_from_install_attempts ) ?
16669 ( $this->_storage->user_recovery_from_install_attempts + 1 ) :
16670 1;
16671
16672 if ( $this->_storage->user_recovery_from_install_attempts >= 3 ) {
16673 $this->delete_current_install( false );
16674 } else {
16675 $this->_storage->user_recovery_from_install_last_attempt_timestamp = time();
16676
16677 return;
16678 }
16679 }
16680 }
16681
16682 $this->_user = ( $user instanceof FS_User ) ?
16683 clone $user :
16684 null;
16685 }
16686
16687 if ( is_object( $this->_user ) ) {
16688 // Load licenses.
16689 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16690 }
16691
16692 if ( is_object( $this->_site ) ) {
16693 // Load plans.
16694 $this->_plans = isset( $plans[ $this->_slug ] ) ?
16695 $plans[ $this->_slug ] :
16696 array();
16697
16698 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
16699 $this->_sync_plans();
16700 } else {
16701 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16702 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
16703 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
16704 } else {
16705 unset( $this->_plans[ $i ] );
16706 }
16707 }
16708 }
16709
16710 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
16711
16712 if ( $this->_site->version != $this->get_plugin_version() ) {
16713 // If stored install version is different than current installed plugin version,
16714 // then update plugin version event.
16715 $this->update_plugin_version_event();
16716 }
16717 }
16718
16719 if ( true === $this->_storage->require_license_activation &&
16720 ! fs_request_get_bool( 'require_license', true )
16721 ) {
16722 $this->_storage->require_license_activation = false;
16723 }
16724
16725 if ( $this->is_theme() ) {
16726 $this->_register_account_hooks();
16727 }
16728
16729 if ( $this->is_user_in_admin() && $this->is_clone() ) {
16730 if ( empty( FS_Clone_Manager::instance()->get_clone_identification_timestamp() ) ) {
16731 FS_Clone_Manager::instance()->store_clone_identification_timestamp();
16732 }
16733
16734 FS_Clone_Manager::instance()->maybe_update_clone_resolution_support_flag( $this->_storage->sdk_last_version );
16735 $this->send_pending_clone_update_once();
16736 }
16737 }
16738
16739 /**
16740 * Special user recovery mechanism.
16741 *
16742 * @author Vova Feldman (@svovaf)
16743 * @since 2.0.0
16744 *
16745 * @param number|null $site_user_id
16746 *
16747 * @return \FS_User|mixed
16748 */
16749 private function sync_user_by_current_install( $site_user_id = null ) {
16750 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
16751 $site_user_id :
16752 $this->_site->user_id;
16753
16754 $api = $this->get_api_site_scope();
16755
16756 $uid = $this->get_anonymous_id();
16757 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
16758
16759 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
16760
16761 if ( $this->is_api_result_entity( $result ) ) {
16762 $user = new FS_User( $result );
16763 $this->_user = $user;
16764 $this->_store_user();
16765
16766 return $user;
16767 }
16768
16769 $error_code = FS_Api::get_error_code( $result );
16770
16771 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
16772 /**
16773 * Those API errors will continue coming and are not recoverable with the
16774 * current site's data. Therefore, extend the API call's cached result to 7 days.
16775 */
16776 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
16777 }
16778
16779 return $result;
16780 }
16781
16782 /**
16783 * @author Vova Feldman (@svovaf)
16784 * @since 1.0.1
16785 *
16786 * @param FS_User $user
16787 * @param FS_Site $site
16788 * @param bool|array $plans
16789 */
16790 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
16791 $site->user_id = $user->id;
16792
16793 $this->_site = $site;
16794 $this->_user = $user;
16795 if ( false !== $plans ) {
16796 $this->_plans = $plans;
16797 }
16798
16799 $this->send_install_update();
16800
16801 $this->_store_account();
16802
16803 }
16804
16805 /**
16806 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
16807 * Each version is trimmed after the 16th char.
16808 *
16809 * @author Vova Feldman (@svovaf)
16810 * @since 2.2.1
16811 *
16812 * @return array
16813 */
16814 private function get_versions() {
16815 $versions = array();
16816 $versions['sdk_version'] = $this->version;
16817
16818 // Collect these diagnostic information only if it's allowed.
16819 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
16820 $versions['platform_version'] = get_bloginfo( 'version' );
16821 $versions['programming_language_version'] = phpversion();
16822 }
16823
16824 foreach ( $versions as $k => $version ) {
16825 $versions[ $k ] = self::get_api_sanitized_property( $k, $version );
16826 }
16827
16828 return $versions;
16829 }
16830
16831 /**
16832 * Get sanitized site language.
16833 *
16834 * @param string $language
16835 * @param int $max_len
16836 *
16837 * @since 2.5.1
16838 * @author Vova Feldman (@svovaf)
16839 *
16840 * @return string
16841 */
16842 private static function get_sanitized_language( $language = '', $max_len = self::LANGUAGE_MAX_CHARS ) {
16843 if ( empty( $language ) ) {
16844 $language = get_bloginfo( 'language' );
16845 }
16846
16847 return substr( $language, 0, $max_len );
16848 }
16849
16850 /**
16851 * Get core version stripped from pre-release and build.
16852 *
16853 * @since 2.5.1
16854 * @author Vova Feldman (@svovaf)
16855 *
16856 * @param string $version
16857 * @param int $parts
16858 * @param int $max_len
16859 * @param bool $include_pre_release
16860 *
16861 * @return string
16862 */
16863 private static function get_core_version(
16864 $version,
16865 $parts = 3,
16866 $max_len = self::VERSION_MAX_CHARS,
16867 $include_pre_release = false
16868 ) {
16869 if ( empty( $version ) ) {
16870 // Version is empty.
16871 return '';
16872 }
16873
16874 if ( is_numeric( $version ) ) {
16875 $is_float_version = is_float( $version );
16876
16877 $version = (string) $version;
16878
16879 /**
16880 * 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.
16881 */
16882 if ( $is_float_version && false === strpos( $version, '.' ) ) {
16883 $version .= '.0';
16884 }
16885 }
16886
16887 if ( ! is_string( $version ) ) {
16888 return '';
16889 }
16890
16891 if ( $parts < 1 ) {
16892 return '';
16893 }
16894
16895 $pre_release_regex = $include_pre_release ?
16896 '(\-(alpha|beta|RC)([0-9]+)?)?' :
16897 '';
16898
16899 if ( 0 === preg_match( '/^([0-9]+(\.[0-9]+){0,' . ( $parts - 1 ) . '}' . $pre_release_regex . ')/i', $version, $matches ) ) {
16900 // Version is not starting with a digit.
16901 return '';
16902 }
16903
16904 return substr( $matches[1], 0, $max_len );
16905 }
16906
16907 /**
16908 * @param string $prop
16909 * @param mixed $val
16910 *
16911 * @return mixed
16912 *@author Vova Feldman (@svovaf)
16913 *
16914 * @since 2.5.1
16915 */
16916 private static function get_api_sanitized_property( $prop, $val ) {
16917 if ( ! is_string( $val ) || empty( $val ) ) {
16918 return $val;
16919 }
16920
16921 switch ( $prop ) {
16922 case 'programming_language_version':
16923 // Get core PHP version, which can have up to 3 parts (ignore pre-releases).
16924 return self::get_core_version( $val );
16925 case 'platform_version':
16926 // Get the exact WordPress version, which can have up to 3 parts (including pre-releases).
16927 return self::get_core_version( $val, 3, self::VERSION_MAX_CHARS, true );
16928 case 'sdk_version':
16929 // Get the exact SDK version, which can have up to 4 parts.
16930 return self::get_core_version( $val, 4 );
16931 case 'version':
16932 // Get the entire version but just limited in length.
16933 return substr( $val, 0, self::VERSION_MAX_CHARS );
16934 case 'language':
16935 return self::get_sanitized_language( $val );
16936 default:
16937 return $val;
16938 }
16939 }
16940
16941 /**
16942 * @author Leo Fajardo (@leorw)
16943 * @since 2.3.0
16944 *
16945 * @return bool
16946 */
16947 function has_beta_update() {
16948 return (
16949 ! empty( $this->_storage->beta_data ) &&
16950 ( true === $this->_storage->beta_data['is_beta'] ) &&
16951 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
16952 );
16953 }
16954
16955 /**
16956 * @author Leo Fajardo (@leorw)
16957 * @since 2.3.0
16958 *
16959 * @return bool
16960 */
16961 function is_beta() {
16962 return (
16963 ! empty( $this->_storage->beta_data ) &&
16964 ( true === $this->_storage->beta_data['is_beta'] ) &&
16965 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
16966 );
16967 }
16968
16969 /**
16970 * @author Vova Feldman (@svovaf)
16971 * @since 1.1.7.4
16972 *
16973 * @param array $override_with
16974 * @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.
16975 * @param bool $skip_user_info
16976 *
16977 * @return array
16978 */
16979 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null, $skip_user_info = false ) {
16980 $this->_logger->entrance();
16981
16982 $activation_action = $this->get_unique_affix() . '_activate_new';
16983 $return_url = $this->is_anonymous() ?
16984 // If skipped already, then return to the account page.
16985 $this->get_account_url( $activation_action, array(), false ) :
16986 // Return to the module's main page.
16987 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
16988
16989 $versions = $this->get_versions();
16990
16991 $params = array_merge( $versions, array(
16992 'plugin_slug' => $this->_slug,
16993 'plugin_id' => $this->get_id(),
16994 'plugin_public_key' => $this->get_public_key(),
16995 'plugin_version' => $this->get_plugin_version(),
16996 'return_url' => fs_nonce_url( $return_url, $activation_action ),
16997 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
16998 'account',
16999 array( 'fs_action' => 'sync_user' )
17000 ), 'sync_user' ),
17001 'is_premium' => $this->is_premium(),
17002 'is_active' => true,
17003 'is_uninstalled' => false,
17004 'is_localhost' => WP_FS__IS_LOCALHOST,
17005 ) );
17006
17007 if (
17008 ! $skip_user_info &&
17009 (
17010 empty( $override_with['user_firstname'] ) ||
17011 empty( $override_with['user_lastname'] ) ||
17012 empty( $override_with['user_email'] )
17013 )
17014 ) {
17015 $current_user = self::_get_current_wp_user();
17016
17017 $params['user_firstname'] = $current_user->user_firstname;
17018 $params['user_lastname'] = $current_user->user_lastname;
17019 $params['user_email'] = $current_user->user_email;
17020 }
17021
17022 if ( $this->is_addon() ) {
17023 $parent_fs = $this->get_parent_instance();
17024
17025 $params['parent_plugin_slug'] = $parent_fs->_slug;
17026 $params['parent_plugin_id'] = $parent_fs->get_id();
17027 }
17028
17029 if ( true === $network_level_or_blog_id ) {
17030 if ( ! isset( $override_with['sites'] ) ) {
17031 $params['sites'] = $this->get_sites_for_network_level_optin();
17032 }
17033 } else {
17034 $site = is_numeric( $network_level_or_blog_id ) ?
17035 array( 'blog_id' => $network_level_or_blog_id ) :
17036 null;
17037
17038 $site = $this->get_site_info( $site );
17039
17040 $diagnostic_info = array();
17041 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17042 $diagnostic_info = array(
17043 'site_name' => $site['title'],
17044 'language' => self::get_sanitized_language( $site['language'] ),
17045 );
17046 }
17047
17048 $params = array_merge( $params, $diagnostic_info, array(
17049 'site_uid' => $site['uid'],
17050 'site_url' => $site['url'],
17051 ) );
17052 }
17053
17054 if ( $this->is_pending_activation() &&
17055 ! empty( $this->_storage->pending_license_key )
17056 ) {
17057 $params['license_key'] = $this->_storage->pending_license_key;
17058 }
17059
17060 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
17061 // Even though rand() is known for its security issues,
17062 // the timestamp adds another layer of protection.
17063 // It would be very hard for an attacker to get the secret key form here.
17064 // Plus, this should never run in production since the secret should never
17065 // be included in the production version.
17066 $params['ts'] = WP_FS__SCRIPT_START_TIME;
17067 $params['salt'] = md5( uniqid( rand() ) );
17068 $params['secure'] = md5(
17069 $params['ts'] .
17070 $params['salt'] .
17071 $this->get_secret_key()
17072 );
17073 }
17074
17075 if ( is_multisite() && function_exists( 'get_network' ) ) {
17076 $params['network_uid'] = $this->get_anonymous_network_id();
17077 }
17078
17079 return array_merge( $params, $override_with );
17080 }
17081
17082 /**
17083 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
17084 * 2. If there was an API error, return the API result.
17085 *
17086 * @author Vova Feldman (@svovaf)
17087 * @since 1.1.7.4
17088 *
17089 * @param string|bool $email
17090 * @param string|bool $first
17091 * @param string|bool $last
17092 * @param string|bool $license_key
17093 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
17094 * In this case, the user and site info will be sent to the server but no
17095 * data will be saved to the WP installation's database.
17096 * @param number|bool $trial_plan_id
17097 * @param bool $is_disconnected Whether to opt in without tracking.
17098 * @param null|bool $is_marketing_allowed
17099 * @param array $sites If network-level opt-in, an array of containing details of sites.
17100 * @param bool $redirect
17101 * @param null|number $license_owner_id
17102 *
17103 * @return string|object
17104 * @use WP_Error
17105 */
17106 function opt_in(
17107 $email = false,
17108 $first = false,
17109 $last = false,
17110 $license_key = false,
17111 $is_uninstall = false,
17112 $trial_plan_id = false,
17113 $is_disconnected = false,
17114 $is_marketing_allowed = null,
17115 $sites = array(),
17116 $redirect = true,
17117 $license_owner_id = null
17118 ) {
17119 $this->_logger->entrance();
17120
17121 /**
17122 * @since 1.2.1 If activating with license key, ignore the context-user
17123 * since the user will be automatically loaded from the license.
17124 */
17125 if ( empty( $license_key ) ) {
17126 // Clean up pending license if opt-ing in again.
17127 $this->_storage->remove( 'pending_license_key' );
17128
17129 if ( ! $is_uninstall ) {
17130 if ( false === $email ) {
17131 $current_user = self::_get_current_wp_user();
17132 $email = $current_user->user_email;
17133 }
17134
17135 $fs_user = Freemius::_get_user_by_email( $email );
17136 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
17137 return $this->install_with_user(
17138 $fs_user,
17139 false,
17140 $trial_plan_id,
17141 $redirect,
17142 true,
17143 $sites
17144 );
17145 }
17146 }
17147 }
17148
17149 $skip_user_info = ( ! empty( $license_key ) && FS_User::is_valid_id( $license_owner_id ) );
17150
17151 $user_info = array();
17152
17153 if ( ! $skip_user_info ) {
17154 if ( ! empty( $email ) ) {
17155 $user_info['user_email'] = $email;
17156 }
17157
17158 if ( ! empty( $first ) ) {
17159 $user_info['user_firstname'] = $first;
17160 }
17161
17162 if ( ! empty( $last ) ) {
17163 $user_info['user_lastname'] = $last;
17164 }
17165 }
17166
17167 if ( ! empty( $sites ) ) {
17168 $is_network = true;
17169
17170 $user_info['sites'] = $sites;
17171 } else {
17172 $is_network = false;
17173 }
17174
17175 $params = $this->get_opt_in_params( $user_info, $is_network, $skip_user_info );
17176
17177 $filtered_license_key = false;
17178 if ( is_string( $license_key ) ) {
17179 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
17180 $params['license_key'] = $filtered_license_key;
17181 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17182 $params['trial_plan_id'] = $trial_plan_id;
17183 }
17184
17185 if ( $is_uninstall ) {
17186 $params['uninstall_params'] = array(
17187 'reason_id' => $this->_storage->uninstall_reason->id,
17188 'reason_info' => $this->_storage->uninstall_reason->info
17189 );
17190 }
17191
17192 if ( isset( $params['license_key'] ) ) {
17193 $fs_user = Freemius::_get_user_by_email( $email );
17194
17195 if ( is_object( $fs_user ) ) {
17196 /**
17197 * If opting in with a context license and the context WP Admin user already opted in
17198 * before from the current site, add the user context security params to avoid the
17199 * unnecessary email activation when the context license is owned by the same context user.
17200 *
17201 * @author Leo Fajardo (@leorw)
17202 * @since 1.2.3
17203 */
17204 $params = array_merge( $params, FS_Security::instance()->get_context_params(
17205 $fs_user,
17206 false,
17207 'install_with_existing_user'
17208 ) );
17209 }
17210 }
17211
17212 if ( is_bool( $is_marketing_allowed ) ) {
17213 $params['is_marketing_allowed'] = $is_marketing_allowed;
17214 }
17215
17216 $params['is_disconnected'] = $is_disconnected;
17217 $params['format'] = 'json';
17218 $params['is_extensions_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed();
17219 $params['is_diagnostic_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed();
17220
17221 $request = array(
17222 'method' => 'POST',
17223 'body' => $params,
17224 'timeout' => 60,
17225 );
17226
17227 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
17228 $response = self::safe_remote_post( $url, $request );
17229
17230 if ( is_wp_error( $response ) ) {
17231 /**
17232 * @var WP_Error $response
17233 */
17234 $result = new stdClass();
17235
17236 $error_code = $response->get_error_code();
17237 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
17238
17239 $result->error = (object) array(
17240 'type' => $error_type,
17241 'message' => $response->get_error_message(),
17242 'code' => $error_code,
17243 'http' => 402
17244 );
17245
17246 $this->maybe_modify_api_curl_error_message( $result );
17247
17248 if ( FS_Api::is_blocked( $result ) ) {
17249 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
17250 }
17251
17252 $is_connected = null;
17253
17254 if ( empty( $license_key ) && $this->is_enable_anonymous() ) {
17255 $this->skip_connection( fs_is_network_admin() );
17256
17257 $is_connected = ( ! FS_Api::is_blocked( $result ) );
17258 }
17259
17260 $this->update_connectivity_info( $is_connected );
17261
17262 return $result;
17263 }
17264
17265 $this->update_connectivity_info( true );
17266
17267 // Module is being uninstalled, don't handle the returned data.
17268 if ( $is_uninstall ) {
17269 return true;
17270 }
17271
17272 /**
17273 * 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.
17274 *
17275 * @author Vova Feldman (@svovaf)
17276 * @since 1.2.3
17277 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
17278 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
17279 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
17280 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
17281 */
17282 $decoded = is_string( $response['body'] ) ?
17283 json_decode( $response['body'] ) :
17284 null;
17285
17286 if ( empty( $decoded ) ) {
17287 return false;
17288 }
17289
17290 if ( ! $this->is_api_result_object( $decoded ) ) {
17291 if ( ! empty( $params['license_key'] ) ) {
17292 // Pass the fully entered license key to the failure handler.
17293 $params['license_key'] = $license_key;
17294 }
17295
17296 return $is_uninstall ?
17297 $decoded :
17298 $this->apply_filters( 'after_install_failure', $decoded, $params );
17299 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
17300 if ( $is_network ) {
17301 $site_ids = array();
17302 foreach ( $sites as $site ) {
17303 $site_ids[] = $site['blog_id'];
17304 }
17305
17306 /**
17307 * Store the sites so that they can be installed once the user has clicked on the activation link
17308 * in the email.
17309 *
17310 * @author Leo Fajardo (@leorw)
17311 */
17312 $this->_storage->pending_sites_info = array(
17313 'blog_ids' => $site_ids,
17314 'license_key' => $license_key,
17315 'trial_plan_id' => $trial_plan_id
17316 );
17317 }
17318
17319 // Pending activation, add message.
17320 return $this->set_pending_confirmation(
17321 ( isset( $decoded->email ) ?
17322 $decoded->email :
17323 true ),
17324 false,
17325 $filtered_license_key,
17326 ! empty( $params['trial_plan_id'] ),
17327 isset( $decoded->is_suspicious_email ) && $decoded->is_suspicious_email
17328 );
17329 } else if ( isset( $decoded->install_secret_key ) ) {
17330 return $this->install_with_new_user(
17331 $decoded->user_id,
17332 $decoded->user_public_key,
17333 $decoded->user_secret_key,
17334 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17335 $decoded->is_marketing_allowed :
17336 null ),
17337 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17338 $decoded->is_extensions_tracking_allowed :
17339 null ),
17340 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17341 $decoded->is_diagnostic_tracking_allowed :
17342 null ),
17343 $decoded->install_id,
17344 $decoded->install_public_key,
17345 $decoded->install_secret_key,
17346 false
17347 );
17348 } else if ( is_array( $decoded->installs ) ) {
17349 return $this->install_many_with_new_user(
17350 $decoded->user_id,
17351 $decoded->user_public_key,
17352 $decoded->user_secret_key,
17353 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17354 $decoded->is_marketing_allowed :
17355 null ),
17356 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17357 $decoded->is_extensions_tracking_allowed :
17358 null ),
17359 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17360 $decoded->is_diagnostic_tracking_allowed :
17361 null ),
17362 $decoded->installs,
17363 false
17364 );
17365 }
17366
17367 return $decoded;
17368 }
17369
17370 /**
17371 * Set user and site identities.
17372 *
17373 * @author Vova Feldman (@svovaf)
17374 * @since 1.0.9
17375 *
17376 * @param FS_User $user
17377 * @param FS_Site $site
17378 * @param bool $redirect
17379 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
17380 * redirect (or return a URL) to the account page with a special parameter to
17381 * trigger the auto installation processes.
17382 *
17383 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17384 */
17385 function setup_account(
17386 FS_User $user,
17387 FS_Site $site,
17388 $redirect = true,
17389 $auto_install = false
17390 ) {
17391 return $this->setup_network_account(
17392 $user,
17393 array( $site ),
17394 $redirect,
17395 $auto_install,
17396 false
17397 );
17398 }
17399
17400 /**
17401 * Set user and site identities.
17402 *
17403 * @author Vova Feldman (@svovaf)
17404 * @since 2.0.0
17405 *
17406 * @param FS_User $user
17407 * @param FS_Site[] $installs
17408 * @param bool $redirect
17409 * @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.
17410 * @param bool $is_network_level_opt_in
17411 *
17412 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17413 */
17414 function setup_network_account(
17415 FS_User $user,
17416 array $installs,
17417 $redirect = true,
17418 $auto_install = false,
17419 $is_network_level_opt_in = true
17420 ) {
17421 $first_install = $installs[0];
17422
17423 $this->_user = $user;
17424 $this->_site = $first_install;
17425
17426 $this->_sync_plans();
17427
17428 if ( $this->_storage->handle_gdpr_admin_notice &&
17429 $this->should_handle_gdpr_admin_notice() &&
17430 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
17431 ) {
17432 /**
17433 * Clear user lock after an opt-in.
17434 */
17435 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
17436 FS_User_Lock::instance()->unlock();
17437 }
17438
17439 if ( 1 < count( $installs ) ) {
17440 // Only network level opt-in can have more than one install.
17441 $is_network_level_opt_in = true;
17442 }
17443
17444 $this->update_connectivity_info( true );
17445
17446 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
17447 // If Freemius was OFF before, turn it on.
17448 $this->turn_on();
17449
17450 $this->handle_account_connection(
17451 $installs,
17452 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
17453 );
17454
17455 if ( is_numeric( $first_install->license_id ) ) {
17456 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
17457 }
17458
17459 $this->_admin_notices->remove_sticky( 'connect_account' );
17460
17461 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
17462 $this->clear_pending_activation_mode();
17463
17464 if ( ! $this->is_paying_or_trial() ) {
17465 $this->_admin_notices->add_sticky(
17466 sprintf( $this->get_text_inline( '%s opt-in was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
17467 'activation_complete'
17468 );
17469 }
17470 }
17471
17472 if ( $this->is_paying_or_trial() ) {
17473 if ( ! $this->is_premium() ||
17474 ! $this->has_premium_version() ||
17475 ! $this->has_settings_menu()
17476 ) {
17477 if ( $this->is_paying() ) {
17478 $this->add_complete_upgrade_instructions_notice(
17479 sprintf(
17480 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
17481 $this->get_plan_title()
17482 ),
17483 'plan_upgraded'
17484 );
17485 } else {
17486 $trial_plan = $this->get_trial_plan();
17487
17488 $this->add_complete_upgrade_instructions_notice(
17489 sprintf(
17490 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
17491 '<i>' . $this->get_plugin_name() . '</i>'
17492 ),
17493 'trial_started',
17494 $trial_plan->title
17495 );
17496 }
17497 }
17498
17499 $this->_admin_notices->remove_sticky( array(
17500 'trial_promotion',
17501 ) );
17502 }
17503
17504 $plugin_id = fs_request_get( 'plugin_id', false );
17505
17506 // Store activation time ONLY for plugins & themes (not add-ons).
17507 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
17508 if ( empty( $this->_storage->activation_timestamp ) ) {
17509 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
17510 }
17511 }
17512
17513 $next_page = '';
17514
17515 $extra = array();
17516 if ( $auto_install ) {
17517 $extra['auto_install'] = 'true';
17518 }
17519
17520 if ( is_numeric( $plugin_id ) ) {
17521 /**
17522 * @author Leo Fajardo (@leorw)
17523 * @since 1.2.1.6
17524 *
17525 * Also sync the license after an anonymous user subscribes.
17526 */
17527 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
17528 // Add-on was installed - sync license right after install.
17529 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
17530 }
17531 } else {
17532 /**
17533 * @author Vova Feldman (@svovaf)
17534 * @since 1.1.9 If site installed with a valid license, sync license.
17535 */
17536 if ( $this->is_paying() ) {
17537 $this->_sync_plugin_license(
17538 true,
17539 // Installs data is already synced in the beginning of this method directly or via _set_account().
17540 false
17541 );
17542 }
17543
17544 // Reload the page with the keys.
17545 $next_page = $this->is_anonymous() ?
17546 // If user previously skipped, redirect to account page.
17547 $this->get_account_url( false, $extra ) :
17548 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
17549 }
17550
17551 if ( ! empty( $next_page ) && $redirect ) {
17552 fs_redirect( $next_page );
17553 }
17554
17555 return $next_page;
17556 }
17557
17558 /**
17559 * Install plugin with new user information after approval.
17560 *
17561 * @author Vova Feldman (@svovaf)
17562 * @since 1.0.7
17563 */
17564 function _install_with_new_user() {
17565 $this->_logger->entrance();
17566
17567 if ( $this->is_registered() ) {
17568 return;
17569 }
17570
17571 $has_pending_activation_confirmation_param = fs_request_has( 'pending_activation' );
17572
17573 $this->update_license_required_permissions_if_anonymous();
17574
17575 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17576 // @todo This logic should be improved because it's executed on every load of a theme.
17577 $this->is_theme()
17578 ) {
17579 // check_admin_referer( $this->_slug . '_activate_new' );
17580
17581 if ( fs_request_has( 'user_secret_key' ) ) {
17582 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17583 $pending_sites_info = $this->_storage->pending_sites_info;
17584
17585 $this->install_many_pending_with_user(
17586 fs_request_get( 'user_id' ),
17587 fs_request_get_raw( 'user_public_key' ),
17588 fs_request_get_raw( 'user_secret_key' ),
17589 fs_request_get_bool( 'is_marketing_allowed', null ),
17590 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17591 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17592 $pending_sites_info['blog_ids'],
17593 $pending_sites_info['license_key'],
17594 $pending_sites_info['trial_plan_id']
17595 );
17596 } else {
17597 $this->install_with_new_user(
17598 fs_request_get( 'user_id' ),
17599 fs_request_get_raw( 'user_public_key' ),
17600 fs_request_get_raw( 'user_secret_key' ),
17601 fs_request_get_bool( 'is_marketing_allowed', null ),
17602 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17603 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17604 fs_request_get( 'install_id' ),
17605 fs_request_get_raw( 'install_public_key' ),
17606 fs_request_get_raw( 'install_secret_key' ),
17607 true,
17608 fs_request_get_bool( 'auto_install' )
17609 );
17610 }
17611 } else if ( $has_pending_activation_confirmation_param ) {
17612 $this->set_pending_confirmation(
17613 fs_request_get( 'user_email' ),
17614 true,
17615 false,
17616 false,
17617 fs_request_get_bool( 'is_suspicious_email' ),
17618 fs_request_get_bool( 'has_upgrade_context' ),
17619 fs_request_get( 'support_email_address' )
17620 );
17621 }
17622 }
17623 }
17624
17625 /**
17626 * @author Vova Feldman (@svovaf)
17627 * @since 2.0.0
17628 *
17629 * @param number $id
17630 * @param string $public_key
17631 * @param string $secret_key
17632 *
17633 * @return \FS_User
17634 */
17635 private function setup_user( $id, $public_key, $secret_key ) {
17636 $user = self::_get_user_by_id( $id );
17637
17638 if ( is_object( $user ) ) {
17639 $this->_user = $user;
17640 } else {
17641 $user = new FS_User();
17642 $user->id = $id;
17643 $user->public_key = $public_key;
17644 $user->secret_key = $secret_key;
17645
17646 $this->_user = $user;
17647 $user_result = $this->get_api_user_scope()->get();
17648 $user = new FS_User( $user_result );
17649
17650 $this->_user = $user;
17651 $this->_store_user();
17652 }
17653
17654 return $user;
17655 }
17656
17657 /**
17658 * Install plugin with new user.
17659 *
17660 * @author Vova Feldman (@svovaf)
17661 * @since 1.1.7.4
17662 *
17663 * @param number $user_id
17664 * @param string $user_public_key
17665 * @param string $user_secret_key
17666 * @param bool|null $is_marketing_allowed
17667 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17668 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17669 * @param number $install_id
17670 * @param string $install_public_key
17671 * @param string $install_secret_key
17672 * @param bool $redirect
17673 * @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.
17674 *
17675 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17676 */
17677 private function install_with_new_user(
17678 $user_id,
17679 $user_public_key,
17680 $user_secret_key,
17681 $is_marketing_allowed,
17682 $is_extensions_tracking_allowed,
17683 $is_diagnostic_tracking_allowed,
17684 $install_id,
17685 $install_public_key,
17686 $install_secret_key,
17687 $redirect = true,
17688 $auto_install = false
17689 ) {
17690 /**
17691 * This method is also executed after opting in with a license key since the
17692 * license can be potentially associated with a different owner.
17693 *
17694 * @since 2.0.0
17695 */
17696 $user = self::_get_user_by_id( $user_id );
17697
17698 if ( ! is_object( $user ) ) {
17699 $user = new FS_User();
17700 $user->id = $user_id;
17701 $user->public_key = $user_public_key;
17702 $user->secret_key = $user_secret_key;
17703
17704 $this->_user = $user;
17705 $user_result = $this->get_api_user_scope()->get();
17706 $user = new FS_User( $user_result );
17707 }
17708
17709 $this->_user = $user;
17710
17711 $site = new FS_Site();
17712 $site->id = $install_id;
17713 $site->public_key = $install_public_key;
17714 $site->secret_key = $install_secret_key;
17715
17716 $this->_site = $site;
17717 $site_result = $this->get_api_site_scope( true )->get();
17718 $site = new FS_Site( $site_result );
17719 $this->_site = $site;
17720
17721 if ( ! is_null( $is_marketing_allowed ) ) {
17722 $this->disable_opt_in_notice_and_lock_user();
17723 }
17724
17725 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17726 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17727 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17728 ) );
17729
17730 return $this->setup_account(
17731 $this->_user,
17732 $this->_site,
17733 $redirect,
17734 $auto_install
17735 );
17736 }
17737
17738 /**
17739 * Install plugin with user.
17740 *
17741 * @author Leo Fajardo (@leorw)
17742 * @since 2.0.0
17743 *
17744 * @param number $user_id
17745 * @param string $user_public_key
17746 * @param string $user_secret_key
17747 * @param bool|null $is_marketing_allowed
17748 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17749 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17750 * @param array $site_ids
17751 * @param bool $license_key
17752 * @param bool $trial_plan_id
17753 * @param bool $redirect
17754 *
17755 * @return void
17756 */
17757 private function install_many_pending_with_user(
17758 $user_id,
17759 $user_public_key,
17760 $user_secret_key,
17761 $is_marketing_allowed,
17762 $is_extensions_tracking_allowed,
17763 $is_diagnostic_tracking_allowed,
17764 $site_ids,
17765 $license_key = false,
17766 $trial_plan_id = false,
17767 $redirect = true
17768 ) {
17769 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17770
17771 if ( ! is_null( $is_marketing_allowed ) ) {
17772 $this->disable_opt_in_notice_and_lock_user();
17773 }
17774
17775 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17776 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17777 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17778 ) );
17779
17780 $sites = array();
17781 foreach ( $site_ids as $site_id ) {
17782 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
17783 }
17784
17785 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17786 }
17787
17788 /**
17789 * Multi-site install with a new user.
17790 *
17791 * @author Vova Feldman (@svovaf)
17792 * @since 2.0.0
17793 *
17794 * @param number $user_id
17795 * @param string $user_public_key
17796 * @param string $user_secret_key
17797 * @param bool|null $is_marketing_allowed
17798 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17799 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17800 * @param object[] $installs
17801 * @param bool $redirect
17802 * @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.
17803 *
17804 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17805 */
17806 private function install_many_with_new_user(
17807 $user_id,
17808 $user_public_key,
17809 $user_secret_key,
17810 $is_marketing_allowed,
17811 $is_extensions_tracking_allowed,
17812 $is_diagnostic_tracking_allowed,
17813 array $installs,
17814 $redirect = true,
17815 $auto_install = false
17816 ) {
17817 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17818
17819 if ( ! is_null( $is_marketing_allowed ) ) {
17820 $this->disable_opt_in_notice_and_lock_user();
17821 }
17822
17823 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17824 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17825 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17826 ) );
17827
17828 $install_ids = array();
17829
17830 foreach ( $installs as $install ) {
17831 $install_ids[] = $install->id;
17832 }
17833
17834 $items_per_request = 25;
17835 $left = count( $install_ids );
17836 $offset = 0;
17837
17838 $installs = array();
17839 while ( $left > 0 ) {
17840 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, $items_per_request ) ) );
17841
17842 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
17843 // @todo Handle API error.
17844 }
17845
17846 $installs = array_merge( $installs, $result->installs );
17847
17848 $left -= $items_per_request;
17849 $offset += $items_per_request;
17850 }
17851
17852 foreach ( $installs as &$install ) {
17853 $install = new FS_Site( $install );
17854 }
17855
17856 return $this->setup_network_account(
17857 $this->_user,
17858 $installs,
17859 $redirect,
17860 $auto_install
17861 );
17862 }
17863
17864 /**
17865 * @author Vova Feldman (@svovaf)
17866 * @since 1.1.7.4
17867 *
17868 * @param string|bool $email
17869 * @param bool $redirect
17870 * @param string|bool $license_key Since 1.2.1.5
17871 * @param bool $is_pending_trial Since 1.2.1.5
17872 * @param bool $is_suspicious_email Since 2.5.0
17873 * @param bool $has_upgrade_context Since 2.5.3
17874 * @param bool|string $support_email_address Since 2.5.3
17875 *
17876 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
17877 */
17878 private function set_pending_confirmation(
17879 $email = false,
17880 $redirect = true,
17881 $license_key = false,
17882 $is_pending_trial = false,
17883 $is_suspicious_email = false,
17884 $has_upgrade_context = false,
17885 $support_email_address = false
17886 ) {
17887 $is_network_admin = fs_is_network_admin();
17888
17889 if ( $this->_ignore_pending_mode && ! $has_upgrade_context ) {
17890 /**
17891 * If explicitly asked to ignore pending mode, set to anonymous mode
17892 * 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).
17893 *
17894 * @author Vova Feldman
17895 * @since 1.2.1.6
17896 */
17897 $this->skip_connection( $is_network_admin );
17898 } else {
17899 // Install must be activated via email since
17900 // user with the same email already exist.
17901 $this->_storage->is_pending_activation = true;
17902 $this->_add_pending_activation_notice(
17903 $email,
17904 $is_pending_trial,
17905 $is_suspicious_email,
17906 $has_upgrade_context,
17907 $support_email_address
17908 );
17909 }
17910
17911 if ( ! empty( $license_key ) ) {
17912 $this->_storage->pending_license_key = $license_key;
17913 }
17914
17915 // Remove the opt-in sticky notice.
17916 $this->_admin_notices->remove_sticky( array(
17917 'connect_account',
17918 'trial_promotion',
17919 ) );
17920
17921 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
17922
17923 if ( $redirect ) {
17924 // Reload the page with a pending activation message.
17925 fs_redirect( $next_page );
17926 }
17927
17928 return $next_page;
17929 }
17930
17931 /**
17932 * Install plugin with current logged WP user info.
17933 *
17934 * @author Vova Feldman (@svovaf)
17935 * @since 1.0.7
17936 */
17937 function _install_with_current_user() {
17938 $this->_logger->entrance();
17939
17940 if ( $this->is_registered() ) {
17941 return;
17942 }
17943
17944 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
17945 check_admin_referer( $this->get_unique_affix() . '_activate_existing' );
17946
17947 /**
17948 * @author Vova Feldman (@svovaf)
17949 * @since 1.1.9 Add license key if given.
17950 */
17951 $license_key = fs_request_get_raw( 'license_secret_key' );
17952
17953 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17954 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17955 FS_Permission_Manager::PERMISSION_EXTENSIONS => fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17956 ) );
17957
17958 $this->install_with_current_user( $license_key );
17959 }
17960 }
17961
17962
17963 /**
17964 * @author Vova Feldman (@svovaf)
17965 * @since 1.1.7.4
17966 *
17967 * @param string|bool $license_key
17968 * @param number|bool $trial_plan_id
17969 * @param array $sites Since 2.0.0
17970 * @param bool $redirect
17971 *
17972 * @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.
17973 */
17974 function install_with_current_user(
17975 $license_key = false,
17976 $trial_plan_id = false,
17977 $sites = array(),
17978 $redirect = true
17979 ) {
17980 // Get current logged WP user.
17981 $current_user = self::_get_current_wp_user();
17982
17983 // Find the relevant FS user by the email.
17984 $user = self::_get_user_by_email( $current_user->user_email );
17985
17986 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17987 }
17988
17989 /**
17990 * @author Vova Feldman (@svovaf)
17991 * @since 2.0.0
17992 *
17993 * @param \FS_User $user
17994 * @param string|bool $license_key
17995 * @param number|bool $trial_plan_id
17996 * @param bool $redirect
17997 * @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.
17998 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
17999 *
18000 * @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.
18001 */
18002 function install_with_user(
18003 FS_User $user,
18004 $license_key = false,
18005 $trial_plan_id = false,
18006 $redirect = true,
18007 $setup_account = true,
18008 $sites = array()
18009 ) {
18010 // We have to set the user before getting user scope API handler.
18011 $this->_user = $user;
18012
18013 // Install the plugin.
18014 $result = $this->create_installs_with_user(
18015 $user,
18016 $license_key,
18017 $trial_plan_id,
18018 $sites,
18019 $redirect
18020 );
18021
18022 if ( ! $this->is_api_result_entity( $result ) &&
18023 ! $this->is_api_result_object( $result, 'installs' )
18024 ) {
18025 // @todo Handler potential API error of the $result
18026 }
18027
18028 if ( empty( $sites ) ) {
18029 $site = new FS_Site( $result );
18030 $this->_site = $site;
18031
18032 if ( ! $setup_account ) {
18033 $this->_store_site();
18034
18035 $this->sync_plan_if_not_exist( $site->plan_id );
18036
18037 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
18038 $this->sync_license_if_not_exist( $site->license_id, $license_key );
18039 }
18040
18041 $this->_admin_notices->remove_sticky( 'connect_account', false );
18042
18043 return $site;
18044 }
18045
18046 return $this->setup_account( $this->_user, $this->_site, $redirect );
18047 } else {
18048 $installs = array();
18049 foreach ( $result->installs as $install ) {
18050 $installs[] = new FS_Site( $install );
18051 }
18052
18053 return $this->setup_network_account(
18054 $user,
18055 $installs,
18056 $redirect
18057 );
18058 }
18059 }
18060
18061 /**
18062 * Initiate an API request to create a collection of installs.
18063 *
18064 * @author Vova Feldman (@svovaf)
18065 * @since 2.0.0
18066 *
18067 * @param \FS_User $user
18068 * @param bool $license_key
18069 * @param bool $trial_plan_id
18070 * @param array $sites
18071 * @param bool $redirect
18072 * @param bool $silent
18073 *
18074 * @return object|mixed
18075 */
18076 private function create_installs_with_user(
18077 FS_User $user,
18078 $license_key = false,
18079 $trial_plan_id = false,
18080 $sites = array(),
18081 $redirect = false,
18082 $silent = false
18083 ) {
18084 $extra_install_params = array(
18085 'uid' => $this->get_anonymous_id(),
18086 'is_disconnected' => false,
18087 );
18088
18089 if ( ! empty( $license_key ) ) {
18090 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
18091
18092 if ( $silent ) {
18093 $extra_install_params['ignore_license_owner'] = true;
18094 }
18095 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
18096 $extra_install_params['trial_plan_id'] = $trial_plan_id;
18097 }
18098
18099 if ( ! empty( $sites ) ) {
18100 $extra_install_params['sites'] = $sites;
18101 }
18102
18103 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
18104
18105 // Install the plugin.
18106 $result = $this->get_api_user_scope_by_user( $user )->call(
18107 "/plugins/{$this->get_id()}/installs.json",
18108 'post',
18109 $args
18110 );
18111
18112 if ( ! $this->is_api_result_entity( $result ) &&
18113 ! $this->is_api_result_object( $result, 'installs' )
18114 ) {
18115 if ( ! empty( $args['license_key'] ) ) {
18116 // Pass the fully entered license key to the failure handler.
18117 $args['license_key'] = $license_key;
18118 }
18119
18120 $result = $this->apply_filters( 'after_install_failure', $result, $args );
18121
18122 if ( ! $silent ) {
18123 $this->_admin_notices->add(
18124 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18125 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
18126 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18127 'error'
18128 );
18129 }
18130
18131 if ( $redirect ) {
18132 /**
18133 * We set the user before getting the user scope API handler, so the user became temporarily
18134 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
18135 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
18136 * activation page based on the return value of `is_registered()`. In addition, in case the
18137 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
18138 * misleading plugin activation errors will be shown on the `Plugins` page.
18139 *
18140 * @author Leo Fajardo (@leorw)
18141 */
18142 $user = $this->_user;
18143
18144 $this->_user = null;
18145
18146 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
18147
18148 /**
18149 * Restore the user after the redirect, this is relevant when there are cases where the redirect will choose not to do anything.
18150 */
18151 $this->_user = $user;
18152 }
18153 }
18154
18155 return $result;
18156 }
18157
18158 /**
18159 * Tries to activate add-on account based on parent plugin info.
18160 *
18161 * @author Vova Feldman (@svovaf)
18162 * @since 1.0.6
18163 *
18164 * @param Freemius $parent_fs
18165 * @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.
18166 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
18167 */
18168 private function _activate_addon_account(
18169 Freemius $parent_fs,
18170 $network_level_or_blog_id = null,
18171 $bundle_license = null
18172 ) {
18173 if ( $this->is_registered() ) {
18174 // Already activated.
18175 return;
18176 }
18177
18178 $permission_ids = FS_Permission_Manager::get_all_permission_ids();
18179 $permissions = array();
18180 foreach ( $permission_ids as $permission_id ) {
18181 $permissions[ $permission_id ] = FS_Permission_Manager::instance( $parent_fs )->is_permission( $permission_id, true );
18182 }
18183
18184 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( $permissions );
18185
18186 /**
18187 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
18188 * already returns the data for the current blog.
18189 *
18190 * @author Leo Fajardo (@leorw)
18191 * @since 2.3.0
18192 */
18193 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
18194 array() :
18195 array( 'uid' => $this->get_anonymous_id() );
18196
18197 $params = $this->get_install_data_for_api(
18198 $uid_param_to_override,
18199 false,
18200 false,
18201 /**
18202 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
18203 * already includes the data for it.
18204 *
18205 * @author Leo Fajardo (@leorw)
18206 * @since 2.3.0
18207 */
18208 ( true !== $network_level_or_blog_id )
18209 );
18210
18211 if ( true === $network_level_or_blog_id ) {
18212 $params['sites'] = $this->get_sites_for_network_level_optin();
18213
18214 if ( empty( $params['sites'] ) ) {
18215 return;
18216 }
18217 }
18218
18219 if ( is_object( $bundle_license ) ) {
18220 $params['license_key'] = $bundle_license->secret_key;
18221 }
18222
18223 // Activate add-on with parent plugin credentials.
18224 $result = $parent_fs->get_api_site_scope()->call(
18225 "/addons/{$this->_plugin->id}/installs.json",
18226 'post',
18227 $params
18228 );
18229
18230 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18231 if ( is_object( $bundle_license ) ) {
18232 /**
18233 * 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.
18234 *
18235 * @author Leo Fajardo (@leorw)
18236 * @since 2.4.0
18237 */
18238 } else {
18239 $error_message = FS_Api::is_api_error_object( $result ) ?
18240 $result->error->message :
18241 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
18242
18243 $this->_admin_notices->add(
18244 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18245 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
18246 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18247 'error'
18248 );
18249 }
18250
18251 return;
18252 }
18253
18254 $addon_installs = $result->installs;
18255 foreach ( $addon_installs as $key => $addon_install ) {
18256 $addon_installs[ $key ] = new FS_Site( $addon_install );
18257 }
18258
18259 $first_install = $addon_installs[0];
18260
18261 // Get user information based on parent's plugin.
18262 $user = $parent_fs->get_user();
18263
18264 // First of all, set site and user info - otherwise we won't
18265 // be able to invoke API calls.
18266 $this->_site = $first_install;
18267 $this->_user = $user;
18268
18269 // Sync add-on plans.
18270 $this->_sync_plans();
18271
18272 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
18273
18274 // Get site's current plan.
18275 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
18276
18277 // Sync licenses.
18278 $this->_sync_licenses();
18279
18280 if ( ! fs_is_network_admin() ) {
18281 // Try to activate premium license.
18282 $this->_activate_license( true, $bundle_license );
18283
18284 if ( is_object( $bundle_license ) ) {
18285 $this->maybe_activate_bundle_license( $bundle_license );
18286 }
18287 } else {
18288 if ( is_object( $bundle_license ) ) {
18289 $premium_license = $bundle_license;
18290 } else {
18291 $license_id = fs_request_get( 'license_id' );
18292
18293 if ( is_object( $this->_site ) &&
18294 FS_Plugin_License::is_valid_id( $license_id ) &&
18295 $license_id == $this->_site->license_id
18296 ) {
18297 // License is already activated.
18298 return;
18299 }
18300
18301 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
18302 $this->_get_license_by_id( $license_id ) :
18303 $this->_get_available_premium_license();
18304 }
18305
18306 if ( is_object( $premium_license ) ) {
18307 $this->maybe_network_activate_addon_license( $premium_license );
18308 }
18309 }
18310 }
18311
18312 /**
18313 * @author Leo Fajardo (@leorw)
18314 * @since 2.3.0
18315 *
18316 * @param FS_Site[] $installs
18317 * @param bool $is_site_level
18318 */
18319 private function handle_account_connection( $installs, $is_site_level ) {
18320 $first_install = $installs[0];
18321
18322 if ( $is_site_level ) {
18323 $this->_set_account( $this->_user, $first_install );
18324
18325 $this->do_action( 'after_account_connection', $this->_user, $first_install );
18326 } else {
18327 $this->_store_user();
18328
18329 // Map site addresses to their blog IDs.
18330 $address_to_blog_map = $this->get_address_to_blog_map();
18331
18332 $first_blog_id = null;
18333 $blog_2_install_map = array();
18334 foreach ( $installs as $install ) {
18335 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
18336 $blog_id = $address_to_blog_map[ $address ];
18337
18338 $this->_store_site( true, $blog_id, $install );
18339
18340 if ( is_null( $first_blog_id ) ) {
18341 $first_blog_id = $blog_id;
18342 }
18343
18344 $blog_2_install_map[ $blog_id ] = $install;
18345 }
18346
18347 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
18348 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
18349 ) {
18350 // Store network user.
18351 $this->_storage->network_user_id = $this->_user->id;
18352 }
18353
18354 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
18355 $this->_storage->network_install_blog_id = $first_blog_id;
18356 }
18357
18358 if ( count( $installs ) === count( $address_to_blog_map ) ) {
18359 // Super admin opted in for all sites in the network.
18360 $this->_storage->is_network_connected = true;
18361 }
18362
18363 $this->_store_licenses( false );
18364
18365 self::$_accounts->store();
18366
18367 // Don't sync the installs data on network upgrade
18368 if ( ! $this->network_upgrade_mode_completed() ) {
18369 $this->send_installs_update();
18370 }
18371
18372 $current_blog = get_current_blog_id();
18373
18374 foreach ( $blog_2_install_map as $blog_id => $install ) {
18375 $this->switch_to_blog( $blog_id );
18376
18377 $this->do_action( 'after_account_connection', $this->_user, $install );
18378 }
18379
18380 // Switch install context back to the first install.
18381 $this->switch_to_blog(
18382 $current_blog,
18383 $first_install,
18384 ( $this->_site->id != $first_install->id )
18385 );
18386
18387 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
18388 }
18389 }
18390
18391 /**
18392 * Tries to activate parent account based on add-on's info.
18393 *
18394 * @author Vova Feldman (@svovaf)
18395 * @since 1.2.2.7
18396 *
18397 * @param Freemius $parent_fs
18398 */
18399 private function activate_parent_account( Freemius $parent_fs ) {
18400 if ( ! $this->is_addon() ) {
18401 // This is not an add-on.
18402 return;
18403 }
18404
18405 if ( $parent_fs->is_registered() ) {
18406 // Already activated.
18407 return;
18408 }
18409
18410 // Activate parent with add-on's user credentials.
18411 $parent_install = $this->get_api_user_scope()->call(
18412 "/plugins/{$parent_fs->_plugin->id}/installs.json",
18413 'post',
18414 $parent_fs->get_install_data_for_api( array(
18415 'uid' => $parent_fs->get_anonymous_id(),
18416 ), false, false )
18417 );
18418
18419 if ( isset( $parent_install->error ) ) {
18420 $this->_admin_notices->add(
18421 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18422 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
18423 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18424 'error'
18425 );
18426
18427 return;
18428 }
18429
18430 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
18431
18432 if ( $parent_fs->is_pending_activation() ) {
18433 $parent_fs->clear_pending_activation_mode();
18434 }
18435
18436 // Get user information based on parent's plugin.
18437 $user = $this->get_user();
18438
18439 // First of all, set site info - otherwise we won't
18440 // be able to invoke API calls.
18441 $parent_fs->_site = new FS_Site( $parent_install );
18442 $parent_fs->_user = $user;
18443
18444 // Sync add-on plans.
18445 $parent_fs->_sync_plans();
18446
18447 $parent_fs->update_license_required_permissions_if_anonymous();
18448
18449 $parent_fs->_set_account( $user, $parent_fs->_site );
18450 }
18451
18452 #endregion
18453
18454 #----------------------------------------------------------------------------------
18455 #region Admin Menu Items
18456 #----------------------------------------------------------------------------------
18457
18458 private $_menu_items = array();
18459
18460 /**
18461 * @author Vova Feldman (@svovaf)
18462 * @since 1.2.1.8
18463 *
18464 * @return array
18465 */
18466 function get_menu_items() {
18467 return $this->_menu_items;
18468 }
18469
18470 /**
18471 * @author Vova Feldman (@svovaf)
18472 * @since 1.0.7
18473 *
18474 * @return string
18475 */
18476 function get_menu_slug() {
18477 return $this->_menu->get_slug();
18478 }
18479
18480 /**
18481 * @author Vova Feldman (@svovaf)
18482 * @since 1.0.9
18483 */
18484 function _prepare_admin_menu() {
18485 // if ( ! $this->is_on() ) {
18486 // return;
18487 // }
18488
18489 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
18490 return;
18491 }
18492
18493 /**
18494 * When running from a site admin with a network activated module and the connection
18495 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
18496 * site level settings.
18497 *
18498 * @author Vova Feldman (@svovaf)
18499 * @since 2.0.0
18500 */
18501 $should_hide_site_admin_settings = (
18502 $this->_is_network_active &&
18503 ! fs_is_network_admin() &&
18504 ! $this->is_delegated_connection() &&
18505 ! $this->is_anonymous() &&
18506 ! $this->is_registered()
18507 );
18508
18509 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
18510
18511 if ( ( false === $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
18512 $should_hide_site_admin_settings
18513 ) {
18514 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
18515 } else {
18516 $this->do_action( fs_is_network_admin() ?
18517 'before_network_admin_menu_init' :
18518 'before_admin_menu_init'
18519 );
18520
18521 $this->add_menu_action();
18522
18523 $this->add_network_menu_when_missing();
18524
18525 $this->add_submenu_items();
18526 }
18527 }
18528
18529 /**
18530 * Admin dashboard menu items modifications.
18531 *
18532 * NOTE: admin_menu action executed before admin_init.
18533 *
18534 * @author Vova Feldman (@svovaf)
18535 * @since 1.0.7
18536 *
18537 */
18538 private function add_menu_action() {
18539 if ( $this->is_activation_mode() ) {
18540 if ( $this->show_opt_in_on_setting_page() ) {
18541 $this->override_plugin_menu_with_activation();
18542 } else {
18543 /**
18544 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
18545 */
18546 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18547 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
18548 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
18549 fs_request_get_bool( 'pending_activation' )
18550 ) {
18551 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
18552 }
18553 }
18554 } else {
18555 if ( ! $this->is_registered() ) {
18556 // If not registered try to install user.
18557 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18558 $this->_install_with_new_user();
18559 }
18560 } else if (
18561 fs_request_is_action( 'sync_user' ) &&
18562 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
18563 ) {
18564 $this->_handle_account_user_sync();
18565 }
18566 }
18567 }
18568
18569 /**
18570 * @author Vova Feldman (@svovaf)
18571 * @since 1.0.1
18572 */
18573 function _redirect_on_clicked_menu_link() {
18574 $this->_logger->entrance();
18575
18576 $page = fs_request_get('page');
18577 $page = is_string($page) ? strtolower($page) : '';
18578
18579 $this->_logger->log( 'page = ' . $page );
18580
18581 foreach ( $this->_menu_items as $priority => $items ) {
18582 foreach ( $items as $item ) {
18583 if ( isset( $item['url'] ) ) {
18584 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
18585 $this->_logger->log( 'Redirecting to ' . $item['url'] );
18586
18587 fs_redirect( $item['url'] );
18588 }
18589 }
18590 }
18591 }
18592 }
18593
18594 /**
18595 * Remove plugin's all admin menu items & pages, and replace with activation page.
18596 *
18597 * @author Vova Feldman (@svovaf)
18598 * @since 1.0.1
18599 */
18600 private function override_plugin_menu_with_activation() {
18601 $this->_logger->entrance();
18602
18603 $hook = false;
18604
18605 if ( ! $this->has_settings_menu() ) {
18606 // Add the opt-in page without a menu item.
18607 $hook = FS_Admin_Menu_Manager::add_subpage(
18608 '',
18609 $this->get_plugin_name(),
18610 $this->get_plugin_name(),
18611 'manage_options',
18612 $this->_slug,
18613 array( &$this, '_connect_page_render' )
18614 );
18615 } else if ( $this->_menu->is_top_level() ) {
18616 if ( $this->_menu->is_override_exact() ) {
18617 // Make sure the current page is matching the activation page.
18618 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18619 return;
18620 }
18621 }
18622
18623 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18624
18625 if ( false === $hook ) {
18626 // Create new menu item just for the opt-in.
18627 $hook = FS_Admin_Menu_Manager::add_page(
18628 $this->get_plugin_name(),
18629 $this->get_plugin_name(),
18630 'manage_options',
18631 $this->_menu->get_slug(),
18632 array( &$this, '_connect_page_render' )
18633 );
18634 }
18635 } else {
18636 $menus = array( $this->_menu->get_parent_slug() );
18637
18638 if ( $this->_menu->is_override_exact() ) {
18639 // Make sure the current page is matching the activation page.
18640 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18641 return;
18642 }
18643 }
18644
18645 foreach ( $menus as $parent_slug ) {
18646 $hook = $this->_menu->override_submenu_action(
18647 $parent_slug,
18648 $this->_menu->get_raw_slug(),
18649 array( &$this, '_connect_page_render' )
18650 );
18651
18652 if ( false !== $hook ) {
18653 // Found plugin's submenu item.
18654 break;
18655 }
18656 }
18657 }
18658
18659 if ( $this->is_activation_page() ) {
18660 // Clean admin page from distracting content.
18661 self::_clean_admin_content_section();
18662 }
18663
18664 if ( false !== $hook ) {
18665 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18666 $this->_install_with_current_user();
18667 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18668 $this->_install_with_new_user();
18669 }
18670 }
18671 }
18672
18673 /**
18674 * If a plugin was network activated and connected but don't have a network
18675 * level settings, then add an artificial menu item for the Account and other
18676 * Freemius settings.
18677 *
18678 * @author Vova Feldman (@svovaf)
18679 * @since 2.0.0
18680 */
18681 private function add_network_menu_when_missing() {
18682 $this->_logger->entrance();
18683
18684 if ( ! $this->_is_network_active ) {
18685 // Plugin wasn't activated on the network level.
18686 return;
18687 }
18688
18689 if ( ! fs_is_network_admin() ) {
18690 // The context is not the network admin.
18691 return;
18692 }
18693
18694 if ( $this->_menu->has_network_menu() ) {
18695 // Plugin already has a network level menu.
18696 return;
18697 }
18698
18699 if ( $this->is_network_activation_mode() ) {
18700 /**
18701 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
18702 * screen is being shown.
18703 *
18704 * @author Leo Fajardo (@leorw)
18705 */
18706 return;
18707 }
18708
18709 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
18710 if ( $this->is_network_delegated_connection() ) {
18711 // Super-admin delegated the connection to the site admins.
18712 return;
18713 }
18714 }
18715
18716 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
18717
18718 if ( $this->_menu->has_menu() ||
18719 ! $this->is_addon() ||
18720 $this->is_activation_mode()
18721 ) {
18722 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
18723 $this->get_plugin_name(),
18724 $this->get_plugin_name(),
18725 'manage_options',
18726 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
18727 );
18728 }
18729 } else {
18730 $this->_menu->add_subpage_and_update(
18731 $this->_menu->get_parent_slug(),
18732 $this->get_plugin_name(),
18733 $this->get_plugin_name(),
18734 'manage_options',
18735 $this->_menu->get_slug()
18736 );
18737 }
18738 }
18739
18740 /**
18741 * @author Leo Fajardo (@leorw)
18742 * @since 1.2.1
18743 *
18744 * return string
18745 */
18746 function get_top_level_menu_capability() {
18747 global $menu;
18748
18749 $top_level_menu_slug = $this->get_top_level_menu_slug();
18750
18751 foreach ( $menu as $menu_info ) {
18752 /**
18753 * The second element in the menu info array is the capability/role that has access to the menu and the
18754 * third element is the menu slug.
18755 */
18756 if ( $menu_info[2] === $top_level_menu_slug ) {
18757 return $menu_info[1];
18758 }
18759 }
18760
18761 return 'read';
18762 }
18763
18764 /**
18765 * @author Vova Feldman (@svovaf)
18766 * @since 1.0.0
18767 *
18768 * @return string
18769 */
18770 private function get_top_level_menu_slug() {
18771 return ( $this->is_addon() ?
18772 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
18773 $this->_menu->get_top_level_menu_slug() );
18774 }
18775
18776 /**
18777 * @author Vova Feldman (@svovaf)
18778 * @since 1.2.2.7
18779 *
18780 * @return string
18781 */
18782 function get_pricing_cta_label() {
18783 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
18784
18785 if ( $this->is_in_trial_promotion() &&
18786 ! $this->is_paying_or_trial()
18787 ) {
18788 // If running a trial promotion, modify the pricing to load the trial.
18789 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
18790 } else if ( $this->is_paying() ) {
18791 $label = $this->get_text_inline( 'Pricing', 'pricing' );
18792 }
18793
18794 return $label;
18795 }
18796
18797 /**
18798 * @author Vova Feldman (@svovaf)
18799 * @since 1.2.2.7
18800 *
18801 * @return bool
18802 */
18803 function is_pricing_page_visible() {
18804 $visible = (
18805 // Has at least one paid plan.
18806 $this->has_paid_plan() &&
18807 // Didn't ask to hide the pricing page.
18808 $this->is_page_visible( 'pricing' ) &&
18809 // Don't have a valid active license or has more than one plan.
18810 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
18811 );
18812
18813 return $this->apply_filters( 'is_pricing_page_visible', $visible );
18814 }
18815
18816 /**
18817 * @author Leo Fajardo (@leorw)
18818 * @since 2.3.0
18819 *
18820 * @param bool $is_activation_mode
18821 *
18822 * @return bool
18823 */
18824 private function should_add_submenu_or_action_links( $is_activation_mode ) {
18825 if ( $this->is_addon() ) {
18826 // No submenu items or action links for add-ons.
18827 return false;
18828 }
18829
18830 if ( $this->show_opt_in_on_themes_page() ) {
18831 if ( ! fs_is_network_admin() ) {
18832 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
18833 return true;
18834 }
18835 } else if ( $is_activation_mode ) {
18836 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
18837 return false;
18838 }
18839
18840 if ( fs_is_network_admin() ) {
18841 /**
18842 * Add submenu items or action links to network level when plugin was network activated and the super
18843 * admin did NOT delegate the connection of all sites to site admins.
18844 */
18845 return (
18846 $this->_is_network_active &&
18847 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
18848 ! $this->is_network_delegated_connection() )
18849 );
18850 }
18851
18852 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
18853 }
18854
18855 /**
18856 * Add default Freemius menu items.
18857 *
18858 * @author Vova Feldman (@svovaf)
18859 * @since 1.0.0
18860 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
18861 */
18862 private function add_submenu_items() {
18863 $this->_logger->entrance();
18864
18865 $is_activation_mode = $this->is_activation_mode();
18866
18867 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
18868
18869 if ( $add_submenu_items ) {
18870 if ( $this->has_affiliate_program() ) {
18871 // Add affiliation page.
18872 $this->add_submenu_item(
18873 $this->get_text_inline( 'Affiliation', 'affiliation' ),
18874 array( &$this, '_affiliation_page_render' ),
18875 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
18876 'manage_options',
18877 'affiliation',
18878 'Freemius::_clean_admin_content_section',
18879 WP_FS__DEFAULT_PRIORITY,
18880 $this->is_submenu_item_visible( 'affiliation' )
18881 );
18882 }
18883 }
18884
18885 if ( $add_submenu_items ||
18886 ( $is_activation_mode &&
18887 $this->is_only_premium() &&
18888 $this->is_admin_page( 'account' ) &&
18889 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
18890 )
18891 ) {
18892 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
18893 $show_account = (
18894 $this->is_submenu_item_visible( 'account' ) &&
18895 /**
18896 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
18897 */
18898 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
18899 );
18900
18901 // Add user account page.
18902 $this->add_submenu_item(
18903 $this->get_text_inline( 'Account', 'account' ),
18904 array( &$this, '_account_page_render' ),
18905 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
18906 'manage_options',
18907 'account',
18908 array( &$this, '_account_page_load' ),
18909 WP_FS__DEFAULT_PRIORITY,
18910 ( $add_submenu_items && $show_account )
18911 );
18912 }
18913 }
18914
18915 if ( $add_submenu_items ) {
18916 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18917 // Add contact page.
18918 if ( $this->is_premium() ) {
18919 $this->add_submenu_item(
18920 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18921 array( &$this, '_contact_page_render' ),
18922 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
18923 'manage_options',
18924 'contact',
18925 'Freemius::_clean_admin_content_section',
18926 WP_FS__DEFAULT_PRIORITY,
18927 $this->is_submenu_item_visible( 'contact' )
18928 );
18929 } else {
18930 $this->add_submenu_link_item(
18931 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18932 FS_Contact_Form_Manager::instance()->get_standalone_link( $this ),
18933 'contact',
18934 'manage_options',
18935 WP_FS__DEFAULT_PRIORITY,
18936 $this->is_submenu_item_visible( 'contact' ),
18937 'fs_external_contact',
18938 true
18939 );
18940 }
18941 }
18942
18943 if ( $this->has_addons() ) {
18944 $this->add_submenu_item(
18945 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18946 array( &$this, '_addons_page_render' ),
18947 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18948 'manage_options',
18949 'addons',
18950 array( &$this, '_addons_page_load' ),
18951 WP_FS__LOWEST_PRIORITY - 1,
18952 $this->is_submenu_item_visible( 'addons' )
18953 );
18954 }
18955 }
18956
18957 if ( $add_submenu_items ||
18958 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
18959 ) {
18960 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18961 $show_pricing = (
18962 $this->is_submenu_item_visible( 'pricing' ) &&
18963 $this->is_pricing_page_visible()
18964 );
18965
18966 $pricing_cta_text = $this->get_pricing_cta_label();
18967 $pricing_class = 'upgrade-mode';
18968 if ( $show_pricing ) {
18969 if ( $this->is_in_trial_promotion() &&
18970 ! $this->is_paying_or_trial()
18971 ) {
18972 // If running a trial promotion, modify the pricing to load the trial.
18973 $pricing_class = 'trial-mode';
18974 } else if ( $this->is_paying() ) {
18975 $pricing_class = '';
18976 }
18977 }
18978
18979 $custom_pricing_url = $this->get_pricing_url_with_filter( null );
18980 $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' ) );
18981 $show_pricing_submenu_item = ( $add_submenu_items && $show_pricing );
18982
18983 // Add upgrade/pricing submenu item.
18984 if ( ! is_null( $custom_pricing_url ) ) {
18985 $this->add_submenu_link_item(
18986 $pricing_menu_title,
18987 $custom_pricing_url,
18988 'pricing',
18989 'manage_options',
18990 WP_FS__LOWEST_PRIORITY,
18991 $show_pricing_submenu_item,
18992 $pricing_class
18993 );
18994 } else {
18995 $this->add_submenu_item(
18996 $pricing_menu_title,
18997 array( &$this, '_pricing_page_render' ),
18998 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
18999 'manage_options',
19000 'pricing',
19001 'Freemius::_clean_admin_content_section',
19002 WP_FS__LOWEST_PRIORITY,
19003 $show_pricing_submenu_item,
19004 $pricing_class
19005 );
19006 }
19007 }
19008 }
19009
19010 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
19011 /**
19012 * Add the other menu items if there are any when not in activation mode or license activation is not
19013 * required (license activation is required for registered or anonymous users after activating the
19014 * premium version when the site is not in trial mode or there's no active valid license).
19015 *
19016 * @author Leo Fajardo (@leorw)
19017 * @since 2.2.1
19018 */
19019 if ( 0 < count( $this->_menu_items ) ) {
19020 if ( ! $this->_menu->is_top_level() ) {
19021 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
19022
19023 // Append submenu items right after the plugin's submenu item.
19024 $this->order_sub_submenu_items();
19025 } else {
19026 // Append submenu items.
19027 $this->embed_submenu_items();
19028 }
19029 }
19030 }
19031 }
19032
19033 /**
19034 * Moved the actual submenu item additions to a separated function,
19035 * in order to support sub-submenu items when the plugin's settings
19036 * only have a submenu and not top-level menu item.
19037 *
19038 * @author Vova Feldman (@svovaf)
19039 * @since 1.1.4
19040 */
19041 private function embed_submenu_items() {
19042 $item_classes = $this->_menu->is_top_level() ? 'fs-submenu-item' : 'fs-submenu-item fs-sub';
19043
19044 $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>';
19045
19046 $top_level_menu_capability = $this->get_top_level_menu_capability();
19047
19048 ksort( $this->_menu_items );
19049
19050 $is_first_submenu_item = true;
19051
19052 foreach ( $this->_menu_items as $priority => $items ) {
19053 foreach ( $items as $item ) {
19054 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
19055
19056 $menu_item = sprintf(
19057 $item_template,
19058 $this->get_unique_affix(),
19059 $item['menu_slug'],
19060 ! empty( $item['class'] ) ? $item['class'] : '',
19061 $item['menu_title'],
19062 esc_attr( isset( $item['url'] ) ? $item['url'] : '' ),
19063 esc_attr( isset( $item['new_tab'] ) ? 'true' : 'false' )
19064 );
19065
19066 $top_level_menu_slug = $this->get_top_level_menu_slug();
19067 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
19068
19069 if ( ! isset( $item['url'] ) ) {
19070 $hook = FS_Admin_Menu_Manager::add_subpage(
19071 $item['show_submenu'] ?
19072 $top_level_menu_slug :
19073 '',
19074 $item['page_title'],
19075 $menu_item,
19076 $capability,
19077 $menu_slug,
19078 $item['render_function']
19079 );
19080
19081 if ( false !== $item['before_render_function'] ) {
19082 add_action( "load-$hook", $item['before_render_function'] );
19083 }
19084 } else {
19085 FS_Admin_Menu_Manager::add_subpage(
19086 $item['show_submenu'] ?
19087 $top_level_menu_slug :
19088 '',
19089 $item['page_title'],
19090 $menu_item,
19091 $capability,
19092 $menu_slug,
19093 array( $this, '' )
19094 );
19095 }
19096
19097 if ( $item['show_submenu'] && $is_first_submenu_item ) {
19098 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
19099 /**
19100 * If the top-level menu has been dynamically created, remove the first submenu item that
19101 * WordPress automatically creates when there's no submenu item whose slug matches the
19102 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
19103 *
19104 * Awesome Plugin
19105 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
19106 *
19107 * @author Leo Fajardo (@leorw)
19108 */
19109 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
19110 }
19111
19112 $is_first_submenu_item = false;
19113 }
19114 }
19115 }
19116 }
19117
19118 /**
19119 * Re-order the submenu items so all Freemius added new submenu items
19120 * are added right after the plugin's settings submenu item.
19121 *
19122 * @author Vova Feldman (@svovaf)
19123 * @since 1.1.4
19124 */
19125 private function order_sub_submenu_items() {
19126 global $submenu;
19127
19128 $menu_slug = $this->_menu->get_top_level_menu_slug();
19129
19130 /**
19131 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
19132 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
19133 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
19134 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
19135 *
19136 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
19137 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
19138 * capability needed to access the parent menu as the capability for the submenus that we will add.
19139 */
19140 if ( empty( $submenu[ $menu_slug ] ) ) {
19141 return;
19142 }
19143
19144 $top_level_menu = &$submenu[ $menu_slug ];
19145
19146 $all_submenu_items_after = array();
19147
19148 $found_submenu_item = false;
19149
19150 foreach ( $top_level_menu as $submenu_id => $meta ) {
19151 if ( $found_submenu_item ) {
19152 // Remove all submenu items after the plugin's submenu item.
19153 $all_submenu_items_after[] = $meta;
19154 unset( $top_level_menu[ $submenu_id ] );
19155 }
19156
19157 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
19158 // Found the submenu item, put all below.
19159 $found_submenu_item = true;
19160 continue;
19161 }
19162 }
19163
19164 // Embed all plugin's new submenu items.
19165 $this->embed_submenu_items();
19166
19167 // Start with specially high number to make sure it's appended.
19168 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
19169 foreach ( $all_submenu_items_after as $meta ) {
19170 $top_level_menu[ $i ] = $meta;
19171 $i ++;
19172 }
19173
19174 // Sort submenu items.
19175 ksort( $top_level_menu );
19176 }
19177
19178 /**
19179 * Helper method to return the module's support forum URL.
19180 *
19181 * @author Vova Feldman (@svovaf)
19182 * @since 1.2.2.7
19183 *
19184 * @return string
19185 */
19186 function get_support_forum_url() {
19187 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
19188 }
19189
19190 /**
19191 * Displays the Support Forum link when enabled.
19192 *
19193 * Can be filtered like so:
19194 *
19195 * function _fs_show_support_menu( $is_visible, $menu_id ) {
19196 * if ( 'support' === $menu_id ) {
19197 * return _fs->is_registered();
19198 * }
19199 * return $is_visible;
19200 * }
19201 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
19202 *
19203 */
19204 function _add_default_submenu_items() {
19205 if ( ! $this->is_on() ) {
19206 return;
19207 }
19208
19209 if ( ! $this->is_activation_mode() &&
19210 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
19211 ( ! $this->_is_network_active && is_admin() ) )
19212 ) {
19213 $this->add_submenu_link_item(
19214 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
19215 $this->get_support_forum_url(),
19216 'wp-support-forum',
19217 null,
19218 50,
19219 $this->is_submenu_item_visible( 'support' ),
19220 '',
19221 true
19222 );
19223 }
19224 }
19225
19226 /**
19227 * @author Vova Feldman (@svovaf)
19228 * @since 1.0.1
19229 *
19230 * @param string $menu_title
19231 * @param callable $render_function
19232 * @param bool|string $page_title
19233 * @param string $capability
19234 * @param bool|string $menu_slug
19235 * @param bool|callable $before_render_function
19236 * @param int $priority
19237 * @param bool $show_submenu
19238 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
19239 */
19240 function add_submenu_item(
19241 $menu_title,
19242 $render_function,
19243 $page_title = false,
19244 $capability = 'manage_options',
19245 $menu_slug = false,
19246 $before_render_function = false,
19247 $priority = WP_FS__DEFAULT_PRIORITY,
19248 $show_submenu = true,
19249 $class = ''
19250 ) {
19251 $this->_logger->entrance( 'Title = ' . $menu_title );
19252
19253 if ( $this->is_addon() ) {
19254 $parent_fs = $this->get_parent_instance();
19255
19256 if ( is_object( $parent_fs ) ) {
19257 $parent_fs->add_submenu_item(
19258 $menu_title,
19259 $render_function,
19260 $page_title,
19261 $capability,
19262 $menu_slug,
19263 $before_render_function,
19264 $priority,
19265 $show_submenu,
19266 $class
19267 );
19268
19269 return;
19270 }
19271 }
19272
19273 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19274 $this->_menu_items[ $priority ] = array();
19275 }
19276
19277 $this->_menu_items[ $priority ][] = array(
19278 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
19279 'menu_title' => $menu_title,
19280 'capability' => $capability,
19281 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19282 'render_function' => $render_function,
19283 'before_render_function' => $before_render_function,
19284 'show_submenu' => $show_submenu,
19285 'class' => $class,
19286 );
19287 }
19288
19289 /**
19290 * @author Vova Feldman (@svovaf)
19291 * @since 1.0.1
19292 *
19293 * @param string $menu_title
19294 * @param string $url
19295 * @param bool $menu_slug
19296 * @param string $capability
19297 * @param int $priority
19298 * @param bool $show_submenu
19299 * @param string $class
19300 * @param bool $new_tab
19301 */
19302 function add_submenu_link_item(
19303 $menu_title,
19304 $url,
19305 $menu_slug = false,
19306 $capability = 'read',
19307 $priority = WP_FS__DEFAULT_PRIORITY,
19308 $show_submenu = true,
19309 $class = '',
19310 $new_tab = false
19311 ) {
19312 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
19313
19314 if ( $this->is_addon() ) {
19315 $parent_fs = $this->get_parent_instance();
19316
19317 if ( is_object( $parent_fs ) ) {
19318 $parent_fs->add_submenu_link_item(
19319 $menu_title,
19320 $url,
19321 $menu_slug,
19322 $capability,
19323 $priority,
19324 $show_submenu,
19325 $class,
19326 $new_tab
19327 );
19328
19329 return;
19330 }
19331 }
19332
19333 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19334 $this->_menu_items[ $priority ] = array();
19335 }
19336
19337 $this->_menu_items[ $priority ][] = array(
19338 'menu_title' => $menu_title,
19339 'capability' => $capability,
19340 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19341 'url' => $url,
19342 'page_title' => $menu_title,
19343 'render_function' => 'fs_dummy',
19344 'before_render_function' => '',
19345 'show_submenu' => $show_submenu,
19346 'class' => $class,
19347 'new_tab' => $new_tab,
19348 );
19349 }
19350
19351 #endregion ------------------------------------------------------------------
19352
19353 #--------------------------------------------------------------------------------
19354 #region Admin Notices
19355 #--------------------------------------------------------------------------------
19356
19357 /**
19358 * @author Vova Feldman (@svovaf)
19359 * @since 2.3.1
19360 *
19361 * @param string|string[] $ids
19362 * @param int|null $network_level_or_blog_id
19363 *
19364 * @uses FS_Admin_Notices::remove_sticky()
19365 */
19366 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
19367 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
19368 }
19369
19370 #endregion
19371
19372 #--------------------------------------------------------------------------------
19373 #region Actions / Hooks / Filters
19374 #--------------------------------------------------------------------------------
19375
19376 /**
19377 * @author Vova Feldman (@svovaf)
19378 * @since 1.1.7
19379 *
19380 * @param string $tag
19381 *
19382 * @return string
19383 */
19384 public function get_action_tag( $tag ) {
19385 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
19386 }
19387
19388 /**
19389 * @author Vova Feldman (@svovaf)
19390 * @since 1.2.1.6
19391 *
19392 * @param string $tag
19393 * @param string $slug
19394 * @param bool $is_plugin
19395 *
19396 * @return string
19397 */
19398 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
19399 $action = "fs_{$tag}";
19400
19401 if ( ! empty( $slug ) ) {
19402 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
19403 }
19404
19405 return $action;
19406 }
19407
19408 /**
19409 * Returns a string that can be used to generate a unique action name,
19410 * option name, HTML element ID, or HTML element class.
19411 *
19412 * @author Leo Fajardo (@leorw)
19413 * @since 1.2.2
19414 *
19415 * @return string
19416 */
19417 public function get_unique_affix() {
19418 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
19419 }
19420
19421 /**
19422 * Returns a string that can be used to generate a unique action name,
19423 * option name, HTML element ID, or HTML element class.
19424 *
19425 * @author Vova Feldman (@svovaf)
19426 * @since 1.2.2.5
19427 *
19428 * @param string $slug
19429 * @param bool $is_plugin
19430 *
19431 * @return string
19432 */
19433 static function get_module_unique_affix( $slug, $is_plugin = true ) {
19434 $affix = $slug;
19435
19436 if ( ! $is_plugin ) {
19437 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
19438 }
19439
19440 return $affix;
19441 }
19442
19443 /**
19444 * @author Vova Feldman (@svovaf)
19445 * @since 1.2.1
19446 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19447 * based on the slug for backward compatibility.
19448 *
19449 * @param string $tag
19450 *
19451 * @return string
19452 */
19453 function get_ajax_action( $tag ) {
19454 return self::get_ajax_action_static( $tag, $this->_module_id );
19455 }
19456
19457 /**
19458 * @author Vova Feldman (@svovaf)
19459 * @since 1.2.1.7
19460 *
19461 * @param string $tag
19462 *
19463 * @return string
19464 */
19465 function get_ajax_security( $tag ) {
19466 return wp_create_nonce( $this->get_ajax_action( $tag ) );
19467 }
19468
19469 /**
19470 * @author Vova Feldman (@svovaf)
19471 * @since 1.2.1.7
19472 *
19473 * @param string $tag
19474 */
19475 function check_ajax_referer( $tag ) {
19476 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
19477 }
19478
19479 /**
19480 * @author Vova Feldman (@svovaf)
19481 * @since 1.2.1.6
19482 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19483 * based on the slug for backward compatibility.
19484 *
19485 * @param string $tag
19486 * @param number|null $module_id
19487 *
19488 * @return string
19489 */
19490 static function get_ajax_action_static( $tag, $module_id = null ) {
19491 $action = "fs_{$tag}";
19492
19493 if ( ! empty( $module_id ) ) {
19494 $action .= "_{$module_id}";
19495 }
19496
19497 return $action;
19498 }
19499
19500 /**
19501 * Do action, specific for the current context plugin.
19502 *
19503 * @author Vova Feldman (@svovaf)
19504 * @since 1.0.1
19505 *
19506 * @param string $tag The name of the action to be executed.
19507 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
19508 * functions hooked to the action. Default empty.
19509 *
19510 * @uses do_action()
19511 */
19512 function do_action( $tag, $arg = '' ) {
19513 $args = func_get_args();
19514
19515 $this->_logger->entrance( $tag );
19516
19517 call_user_func_array( 'do_action', array_merge(
19518 array( $this->get_action_tag( $tag ) ),
19519 array_slice( $args, 1 ) )
19520 );
19521 }
19522
19523 /**
19524 * Add action, specific for the current context plugin.
19525 *
19526 * @author Vova Feldman (@svovaf)
19527 * @since 1.0.1
19528 *
19529 * @param string $tag
19530 * @param callable $function_to_add
19531 * @param int $priority
19532 * @param int $accepted_args
19533 *
19534 * @uses add_action()
19535 */
19536 function add_action(
19537 $tag,
19538 $function_to_add,
19539 $priority = WP_FS__DEFAULT_PRIORITY,
19540 $accepted_args = 1
19541 ) {
19542 $this->_logger->entrance( $tag );
19543
19544 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19545 }
19546
19547 /**
19548 * Add AJAX action, specific for the current context plugin.
19549 *
19550 * @author Vova Feldman (@svovaf)
19551 * @since 1.2.1
19552 *
19553 * @param string $tag
19554 * @param callable $function_to_add
19555 * @param int $priority
19556 *
19557 * @uses add_action()
19558 *
19559 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19560 */
19561 function add_ajax_action(
19562 $tag,
19563 $function_to_add,
19564 $priority = WP_FS__DEFAULT_PRIORITY
19565 ) {
19566 $this->_logger->entrance( $tag );
19567
19568 return self::add_ajax_action_static(
19569 $tag,
19570 $function_to_add,
19571 $priority,
19572 $this->_module_id
19573 );
19574 }
19575
19576 /**
19577 * Add AJAX action.
19578 *
19579 * @author Vova Feldman (@svovaf)
19580 * @since 1.2.1.6
19581 *
19582 * @param string $tag
19583 * @param callable $function_to_add
19584 * @param int $priority
19585 * @param number|null $module_id
19586 *
19587 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19588 * @uses add_action()
19589 *
19590 */
19591 static function add_ajax_action_static(
19592 $tag,
19593 $function_to_add,
19594 $priority = WP_FS__DEFAULT_PRIORITY,
19595 $module_id = null
19596 ) {
19597 self::$_static_logger->entrance( $tag );
19598
19599 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
19600 return false;
19601 }
19602
19603 add_action(
19604 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
19605 $function_to_add,
19606 $priority,
19607 0
19608 );
19609
19610 self::$_static_logger->info( "$tag AJAX callback action added." );
19611
19612 return true;
19613 }
19614
19615 /**
19616 * Send a JSON response back to an Ajax request.
19617 *
19618 * @author Vova Feldman (@svovaf)
19619 * @since 1.2.1.5
19620 *
19621 * @param mixed $response
19622 */
19623 static function shoot_ajax_response( $response ) {
19624 wp_send_json( $response );
19625 }
19626
19627 /**
19628 * Send a JSON response back to an Ajax request, indicating success.
19629 *
19630 * @author Vova Feldman (@svovaf)
19631 * @since 1.2.1.5
19632 *
19633 * @param mixed $data Data to encode as JSON, then print and exit.
19634 */
19635 static function shoot_ajax_success( $data = null ) {
19636 wp_send_json_success( $data );
19637 }
19638
19639 /**
19640 * Send a JSON response back to an Ajax request, indicating failure.
19641 *
19642 * @author Vova Feldman (@svovaf)
19643 * @since 1.2.1.5
19644 *
19645 * @param mixed $error Optional error message.
19646 */
19647 static function shoot_ajax_failure( $error = '' ) {
19648 $result = array( 'success' => false );
19649 if ( ! empty( $error ) ) {
19650 $result['error'] = $error;
19651 }
19652
19653 wp_send_json( $result );
19654 }
19655
19656 /**
19657 * Returns an AJAX URL with a special extra param to indicate whether the request was triggered from the network admin or blog admin.
19658 *
19659 * @author Vova Feldman (@svovaf)
19660 * @since 2.5.1
19661 *
19662 * @param string $wrap_with By default, returns the AJAX URL wrapped with single quotes.
19663 *
19664 * @return string
19665 */
19666 static function ajax_url( $wrap_with = "'") {
19667 if ( fs_is_network_admin() ) {
19668 $param_name = '_fs_network_admin';
19669 } else {
19670 $param_name = '_fs_blog_admin';
19671 }
19672
19673 $url = admin_url( 'admin-ajax.php', 'relative' );
19674 $url .= ( false === strpos( $url, '?' ) ) ? '?' : '&';
19675 $url .= "{$param_name}=true";
19676
19677 return "{$wrap_with}{$url}{$wrap_with}";
19678 }
19679
19680 /**
19681 * Apply filter, specific for the current context plugin.
19682 *
19683 * @author Vova Feldman (@svovaf)
19684 * @since 1.0.9
19685 *
19686 * @param string $tag The name of the filter hook.
19687 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
19688 *
19689 * @return mixed The filtered value after all hooked functions are applied to it.
19690 *
19691 * @uses apply_filters()
19692 */
19693 function apply_filters( $tag, $value ) {
19694 $args = func_get_args();
19695
19696 $this->_logger->entrance( $tag );
19697
19698 array_unshift( $args, $this->get_unique_affix() );
19699
19700 return call_user_func_array( 'fs_apply_filter', $args );
19701 }
19702
19703 /**
19704 * Add filter, specific for the current context plugin.
19705 *
19706 * @author Vova Feldman (@svovaf)
19707 * @since 1.0.9
19708 *
19709 * @param string $tag
19710 * @param callable $function_to_add
19711 * @param int $priority
19712 * @param int $accepted_args
19713 *
19714 * @uses add_filter()
19715 */
19716 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
19717 $this->_logger->entrance( $tag );
19718
19719 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19720 }
19721
19722 /**
19723 * Check if has filter.
19724 *
19725 * @author Vova Feldman (@svovaf)
19726 * @since 1.1.4
19727 *
19728 * @param string $tag
19729 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
19730 *
19731 * @return false|int
19732 *
19733 * @uses has_filter()
19734 */
19735 function has_filter( $tag, $function_to_check = false ) {
19736 $this->_logger->entrance( $tag );
19737
19738 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
19739 }
19740
19741 #endregion
19742
19743 /**
19744 * Override default i18n text phrases.
19745 *
19746 * @author Vova Feldman (@svovaf)
19747 * @since 1.1.6
19748 *
19749 * @param string[] string $key_value
19750 *
19751 * @uses fs_override_i18n()
19752 */
19753 function override_i18n( $key_value ) {
19754 fs_override_i18n( $key_value, $this->_slug );
19755 }
19756
19757 /* Account Page
19758 ------------------------------------------------------------------------------------------------------------------*/
19759 /**
19760 * Update site information.
19761 *
19762 * @author Vova Feldman (@svovaf)
19763 * @since 1.0.1
19764 *
19765 * @param bool $store Flush to Database if true.
19766 * @param null|int $network_level_or_blog_id Since 2.0.0
19767 * @param \FS_Site $site Since 2.0.0
19768 */
19769 private function _store_site( $store = true, $network_level_or_blog_id = null, $site = null, $is_backup = false ) {
19770 $this->_logger->entrance();
19771
19772 if ( is_null( $site ) ) {
19773 $site = $this->_site;
19774 }
19775
19776 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
19777 $this->_logger->error( "Empty install ID, can't store site." );
19778
19779 return;
19780 }
19781
19782 $site_clone = clone $site;
19783
19784 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id, $is_backup );
19785
19786 if (
19787 ! $is_backup &&
19788 is_object( $this->_user ) && $this->_user->id != $site->user_id
19789 ) {
19790 $this->sync_user_by_current_install( $site->user_id );
19791
19792 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
19793
19794 if ( empty( $prev_stored_user_id ) &&
19795 is_object($this->_user) && $this->_user->id != $site->user_id
19796 ) {
19797 /**
19798 * Store the current user ID as the previous user ID so that the previous user can be used
19799 * as the install's owner while the new owner's details are not yet available.
19800 *
19801 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
19802 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
19803 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
19804 * into `replica`.
19805 *
19806 * @author Leo Fajardo (@leorw)
19807 */
19808 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
19809 }
19810 }
19811
19812 $sites[ $this->_slug ] = $site_clone;
19813
19814 $this->set_account_option(
19815 ( $is_backup ? 'prev_' : '' ) . 'sites',
19816 $sites,
19817 $store,
19818 $network_level_or_blog_id
19819 );
19820 }
19821
19822 /**
19823 * 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).
19824 *
19825 * @author Leo Fajardo (@leorw)
19826 * @since 2.5.0
19827 */
19828 private function back_up_site() {
19829 $this->_logger->entrance();
19830
19831 $site_clone = clone $this->_site;
19832
19833 $this->_store_site( true, null, $site_clone, true );
19834 }
19835
19836 /**
19837 * Update plugin's plans information.
19838 *
19839 * @author Vova Feldman (@svovaf)
19840 * @since 1.0.2
19841 *
19842 * @param bool $store Flush to Database if true.
19843 */
19844 private function _store_plans( $store = true ) {
19845 $this->_logger->entrance();
19846
19847 $plans = self::get_all_plans( $this->_module_type );
19848
19849 // Copy plans.
19850 $encrypted_plans = array();
19851 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
19852 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
19853 }
19854
19855 $plans[ $this->_slug ] = $encrypted_plans;
19856
19857 $this->set_account_option( 'plans', $plans, $store );
19858 }
19859
19860 /**
19861 * Update user's plugin licenses.
19862 *
19863 * @author Vova Feldman (@svovaf)
19864 * @since 1.0.5
19865 *
19866 * @param bool $store
19867 * @param number|bool $module_id
19868 * @param FS_Plugin_License[] $licenses
19869 */
19870 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
19871 $this->_logger->entrance();
19872
19873 $all_licenses = self::get_all_licenses();
19874
19875 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
19876 $module_id = $this->_module_id;
19877
19878 $user_licenses = is_array( $this->_licenses ) ?
19879 $this->_licenses :
19880 array();
19881
19882 if ( empty( $user_licenses ) ) {
19883 // If the context user doesn't have any license, don't update the licenses collection.
19884 return;
19885 }
19886
19887 $new_user_licenses_map = array();
19888 foreach ( $user_licenses as $user_license ) {
19889 $new_user_licenses_map[ $user_license->id ] = $user_license;
19890 }
19891
19892 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
19893
19894 // Update user licenses.
19895 $licenses_to_update_count = count( $new_user_licenses_map );
19896 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
19897 if ( 0 === $licenses_to_update_count ) {
19898 break;
19899 }
19900
19901 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
19902 // Update license.
19903 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
19904 unset( $new_user_licenses_map[ $license->id ] );
19905
19906 $licenses_to_update_count --;
19907 }
19908 }
19909
19910 if ( ! empty( $new_user_licenses_map ) ) {
19911 // Add new licenses.
19912 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
19913 }
19914
19915 $licenses = $all_licenses[ $module_id ];
19916 }
19917
19918 if ( ! isset( $all_licenses[ $module_id ] ) ) {
19919 $all_licenses[ $module_id ] = array();
19920 }
19921
19922 $all_licenses[ $module_id ] = $licenses;
19923
19924 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
19925 }
19926
19927 /**
19928 * Update user information.
19929 *
19930 * @author Vova Feldman (@svovaf)
19931 * @since 1.0.1
19932 *
19933 * @param bool $store Flush to Database if true.
19934 */
19935 private function _store_user( $store = true ) {
19936 $this->_logger->entrance();
19937
19938 if ( empty( $this->_user->id ) ) {
19939 $this->_logger->error( "Empty user ID, can't store user." );
19940
19941 return;
19942 }
19943
19944 $users = self::get_all_users();
19945 $users[ $this->_user->id ] = $this->_user;
19946 self::$_accounts->set_option( 'users', $users, $store );
19947 }
19948
19949 /**
19950 * Update new updates information.
19951 *
19952 * @author Vova Feldman (@svovaf)
19953 * @since 1.0.4
19954 *
19955 * @param FS_Plugin_Tag|null $update
19956 * @param bool $store Flush to Database if true.
19957 * @param bool|number $plugin_id
19958 */
19959 private function _store_update( $update, $store = true, $plugin_id = false ) {
19960 $this->_logger->entrance();
19961
19962 if ( $update instanceof FS_Plugin_Tag ) {
19963 $update->updated = time();
19964 }
19965
19966 if ( ! is_numeric( $plugin_id ) ) {
19967 $plugin_id = $this->_plugin->id;
19968 }
19969
19970 $updates = self::get_all_updates();
19971 $updates[ $plugin_id ] = $update;
19972 self::$_accounts->set_option( 'updates', $updates, $store );
19973 }
19974
19975 /**
19976 * Update new updates information.
19977 *
19978 * @author Vova Feldman (@svovaf)
19979 * @since 1.0.6
19980 *
19981 * @param FS_Plugin[] $plugin_addons
19982 * @param bool $store Flush to Database if true.
19983 */
19984 private function _store_addons( $plugin_addons, $store = true ) {
19985 $this->_logger->entrance();
19986
19987 $addons = self::get_all_addons();
19988 $addons[ $this->_plugin->id ] = $plugin_addons;
19989 self::$_accounts->set_option( 'addons', $addons, $store );
19990 }
19991
19992 /**
19993 * Delete plugin's associated add-ons.
19994 *
19995 * @author Vova Feldman (@svovaf)
19996 * @since 1.0.8
19997 *
19998 * @param bool $store
19999 *
20000 * @return bool
20001 */
20002 private function _delete_account_addons( $store = true ) {
20003 $all_addons = self::get_all_account_addons();
20004
20005 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
20006 return false;
20007 }
20008
20009 unset( $all_addons[ $this->_plugin->id ] );
20010
20011 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20012
20013 return true;
20014 }
20015
20016 /**
20017 * Update account add-ons list.
20018 *
20019 * @author Vova Feldman (@svovaf)
20020 * @since 1.0.6
20021 *
20022 * @param FS_Plugin[] $addons
20023 * @param bool $store Flush to Database if true.
20024 */
20025 private function _store_account_addons( $addons, $store = true ) {
20026 $this->_logger->entrance();
20027
20028 $all_addons = self::get_all_account_addons();
20029 $all_addons[ $this->_plugin->id ] = $addons;
20030 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20031 }
20032
20033 /**
20034 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
20035 * the valid user licenses will be fetched again and the account add-ons may be updated.
20036 *
20037 * @author Leo Fajardo (@leorw)
20038 * @since 2.2.4
20039 */
20040 private function purge_valid_user_licenses_cache() {
20041 if ( ! $this->is_registered() ) {
20042 return;
20043 }
20044
20045 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
20046 }
20047
20048 /**
20049 * @author Leo Fajardo (@leorw)
20050 * @since 2.3.0
20051 *
20052 * @param array $all_licenses
20053 * @param number|null $site_license_id
20054 * @param bool $include_parent_licenses
20055 *
20056 * @return array
20057 */
20058 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
20059 $foreign_licenses = array(
20060 'ids' => array(),
20061 'license_keys' => array()
20062 );
20063
20064 $parent_license_ids_map = array();
20065
20066 foreach ( $all_licenses as $license ) {
20067 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
20068 continue;
20069 }
20070
20071 $foreign_licenses['ids'][] = $license->id;
20072 $foreign_licenses['license_keys'][] = $license->secret_key;
20073
20074 if (
20075 $include_parent_licenses &&
20076 is_object( $this->_license ) &&
20077 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
20078 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
20079 ) {
20080 /**
20081 * Include the parent license's info only if it has not been included before since child licenses
20082 * can have the same parent license.
20083 */
20084 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
20085 $foreign_licenses['license_keys'][] = $license->secret_key;
20086
20087 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
20088 }
20089 }
20090
20091 if ( empty( $foreign_licenses['ids'] ) ) {
20092 $foreign_licenses = array();
20093 }
20094
20095 return $foreign_licenses;
20096 }
20097
20098 /**
20099 * @author Leo Fajardo (@leorw)
20100 * @since 2.3.0
20101 *
20102 * @return string
20103 */
20104 private function get_valid_user_licenses_endpoint() {
20105 $user_licenses_endpoint = '/licenses.json?type=active' .
20106 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
20107
20108 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
20109
20110 if ( ! empty ( $foreign_licenses ) ) {
20111 $foreign_licenses = array(
20112 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20113 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20114 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20115 );
20116
20117 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20118 }
20119
20120 return $user_licenses_endpoint;
20121 }
20122
20123 /**
20124 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
20125 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
20126 * method filters out non–add-on product IDs and stores the add-on IDs.
20127 *
20128 * @author Leo Fajardo (@leorw)
20129 * @since 2.2.4
20130 *
20131 * @return stdClass[] array
20132 */
20133 private function fetch_valid_user_licenses() {
20134 $this->_logger->entrance();
20135
20136 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
20137
20138 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
20139 ! is_array( $result->licenses )
20140 ) {
20141 return array();
20142 }
20143
20144 return $result->licenses;
20145 }
20146
20147 /**
20148 * @author Leo Fajardo (@leorw)
20149 * @since 2.2.4
20150 *
20151 * @return number[] Account add-on IDs.
20152 */
20153 function get_updated_account_addons() {
20154 $addons = $this->get_addons();
20155 if ( empty( $addons ) ) {
20156 return array();
20157 }
20158
20159 $account_addons = $this->get_account_addons();
20160 if ( ! is_array( $account_addons ) ) {
20161 $account_addons = array();
20162 }
20163
20164 $user_licenses = $this->is_registered() ?
20165 $this->fetch_valid_user_licenses() :
20166 array();
20167
20168 if ( empty( $user_licenses ) ) {
20169 return $account_addons;
20170 }
20171
20172 $addon_ids = array();
20173 foreach ( $addons as $addon ) {
20174 $addon_ids[] = $addon->id;
20175 }
20176
20177 $license_product_ids = array();
20178
20179 foreach ( $user_licenses as $license ) {
20180 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
20181 $license_product_ids = array_merge( $license_product_ids, $license->products );
20182 } else {
20183 $license_product_ids[] = $license->plugin_id;
20184 }
20185 }
20186
20187 // Filter out non–add-on IDs.
20188 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
20189 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
20190 $this->_store_account_addons( array_unique( $new_account_addons ) );
20191 }
20192
20193 return $new_account_addons;
20194 }
20195
20196 /**
20197 * Store account params in the Database.
20198 *
20199 * @author Vova Feldman (@svovaf)
20200 * @since 1.0.1
20201 *
20202 * @param null|int $blog_id Since 2.0.0
20203 */
20204 private function _store_account( $blog_id = null ) {
20205 $this->_logger->entrance();
20206
20207 $this->_store_site( false, $blog_id );
20208 $this->_store_user( false );
20209 $this->_store_plans( false );
20210 $this->_store_licenses( false );
20211
20212 self::$_accounts->store( $blog_id );
20213 }
20214
20215 /**
20216 * Sync user's information.
20217 *
20218 * @author Vova Feldman (@svovaf)
20219 * @since 1.0.3
20220 * @uses FS_Api
20221 */
20222 private function _handle_account_user_sync() {
20223 $this->_logger->entrance();
20224
20225 $api = $this->get_api_user_scope();
20226
20227 // Get user's information.
20228 $user = $api->get( '/', true );
20229
20230 if ( isset( $user->id ) ) {
20231 $this->_user->first = $user->first;
20232 $this->_user->last = $user->last;
20233 $this->_user->email = $user->email;
20234
20235 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
20236
20237 if ( $user->is_verified &&
20238 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
20239 ) {
20240 $this->_user->is_verified = true;
20241
20242 $this->do_action( 'account_email_verified', $user->email );
20243
20244 $this->_admin_notices->add(
20245 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
20246 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
20247 'success',
20248 // Make admin sticky if account menu item is invisible,
20249 // since the page will be auto redirected to the plugin's
20250 // main settings page, and the non-sticky message
20251 // will disappear.
20252 ! $is_menu_item_account_visible,
20253 'email_verified'
20254 );
20255 }
20256
20257 // Flush user details to DB.
20258 $this->_store_user();
20259
20260 $this->do_action( 'after_account_user_sync', $user );
20261
20262 /**
20263 * If account menu item is hidden, redirect to plugin's main settings page.
20264 *
20265 * @author Vova Feldman (@svovaf)
20266 * @since 1.1.6
20267 *
20268 * @link https://github.com/Freemius/wordpress-sdk/issues/6
20269 */
20270 if ( ! $is_menu_item_account_visible ) {
20271 fs_redirect( $this->_get_admin_page_url() );
20272 }
20273 }
20274 }
20275
20276 /**
20277 * @author Vova Feldman (@svovaf)
20278 * @since 1.0.9
20279 * @uses FS_Api
20280 *
20281 * @param number|bool $license_id
20282 *
20283 * @return FS_Subscription|object|bool
20284 */
20285 private function _fetch_site_license_subscription( $license_id = false ) {
20286 $this->_logger->entrance();
20287 $api = $this->get_api_site_scope();
20288
20289 if ( ! is_numeric( $license_id ) ) {
20290 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
20291 $this->_license->parent_license_id :
20292 $this->_license->id;
20293 }
20294
20295 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
20296
20297 return ! isset( $result->error ) ?
20298 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
20299 new FS_Subscription( $result->subscriptions[0] ) :
20300 false
20301 ) :
20302 $result;
20303 }
20304
20305 /**
20306 * @author Vova Feldman (@svovaf)
20307 * @since 1.0.4
20308 * @uses FS_Api
20309 *
20310 * @param number|bool $plan_id
20311 *
20312 * @return FS_Plugin_Plan|object
20313 */
20314 private function _fetch_site_plan( $plan_id = false ) {
20315 $this->_logger->entrance();
20316 $api = $this->get_api_site_scope();
20317
20318 if ( ! is_numeric( $plan_id ) ) {
20319 $plan_id = $this->_site->plan_id;
20320 }
20321
20322 $plan = $api->get( "/plans/{$plan_id}.json", true );
20323
20324 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
20325 }
20326
20327 /**
20328 * @author Vova Feldman (@svovaf)
20329 * @since 1.0.5
20330 * @uses FS_Api
20331 *
20332 * @return FS_Plugin_Plan[]|object
20333 */
20334 private function _fetch_plugin_plans() {
20335 $this->_logger->entrance();
20336 $api = $this->get_current_or_network_user_api_scope();
20337
20338 /**
20339 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
20340 */
20341 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
20342
20343 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
20344 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
20345 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
20346 }
20347
20348 $result = $result->plans;
20349 }
20350
20351 return $result;
20352 }
20353
20354 /**
20355 * @author Vova Feldman (@svovaf)
20356 * @since 2.0.0
20357 *
20358 * @param number $plan_id
20359 *
20360 * @return \FS_Plugin_Plan|object
20361 */
20362 private function fetch_plan_by_id( $plan_id ) {
20363 $this->_logger->entrance();
20364 $api = $this->get_current_or_network_user_api_scope();
20365
20366 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
20367
20368 return $this->is_api_result_entity( $result ) ?
20369 new FS_Plugin_Plan( $result ) :
20370 $result;
20371 }
20372
20373 /**
20374 * @author Vova Feldman (@svovaf)
20375 * @since 1.0.5
20376 * @uses FS_Api
20377 *
20378 * @param number|bool $plugin_id
20379 * @param number|bool $site_license_id
20380 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
20381 * @param number|null $blog_id
20382 *
20383 * @return FS_Plugin_License[]|object
20384 */
20385 private function _fetch_licenses(
20386 $plugin_id = false,
20387 $site_license_id = false,
20388 $foreign_licenses = array(),
20389 $blog_id = null
20390 ) {
20391 $this->_logger->entrance();
20392
20393 $api = $this->get_api_user_scope();
20394
20395 if ( ! is_numeric( $plugin_id ) ) {
20396 $plugin_id = $this->_plugin->id;
20397 }
20398
20399 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
20400 if ( ! empty ( $foreign_licenses ) ) {
20401 $foreign_licenses = array(
20402 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20403 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20404 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20405 );
20406
20407 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20408 }
20409
20410 $result = $api->get( $user_licenses_endpoint, true );
20411
20412 $is_site_license_synced = false;
20413
20414 $api_errors = array();
20415
20416 if ( $this->is_api_result_object( $result, 'licenses' ) &&
20417 is_array( $result->licenses )
20418 ) {
20419 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
20420 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
20421
20422 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
20423 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
20424 }
20425 }
20426
20427 $result = $result->licenses;
20428 } else {
20429 $api_errors[] = $result;
20430 $result = array();
20431 }
20432
20433 if ( ! $is_site_license_synced ) {
20434 if ( ! is_null( $blog_id ) ) {
20435 /**
20436 * If blog ID is not null, the request is for syncing of the license of a single site via the
20437 * network-level "Account" page.
20438 *
20439 * @author Leo Fajardo (@leorw)
20440 */
20441 $this->switch_to_blog( $blog_id );
20442 }
20443
20444 $api = $this->get_api_site_scope();
20445
20446 if ( is_numeric( $site_license_id ) ) {
20447 // Try to retrieve a foreign license that is linked to the install.
20448 $api_result = $api->call( '/licenses.json?is_enriched=true' );
20449
20450 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
20451 is_array( $api_result->licenses )
20452 ) {
20453 $licenses = $api_result->licenses;
20454
20455 if ( ! empty( $licenses ) ) {
20456 $result[] = new FS_Plugin_License( $licenses[0] );
20457 }
20458 } else {
20459 $api_errors[] = $api_result;
20460 }
20461 } else if (
20462 is_object( $this->_license ) &&
20463 /**
20464 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
20465 * the FS instance that does the syncing is the parent FS instance.
20466 *
20467 * @author Leo Fajardo (@leorw)
20468 * @since 2.3.0
20469 */
20470 $this->_license->plugin_id == $plugin_id
20471 ) {
20472 $is_license_in_result = false;
20473 if ( ! empty( $result ) ) {
20474 foreach ( $result as $license ) {
20475 if ( $license->id == $this->_license->id ) {
20476 $is_license_in_result = true;
20477 break;
20478 }
20479 }
20480 }
20481
20482 if ( ! $is_license_in_result ) {
20483 // Fetch foreign license by ID and license key.
20484 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
20485 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
20486
20487 if ( $this->is_api_result_entity( $license ) ) {
20488 $result[] = new FS_Plugin_License( $license );
20489 } else {
20490 $api_errors[] = $license;
20491 }
20492 }
20493 }
20494
20495 if ( ! is_null( $blog_id ) ) {
20496 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20497 }
20498 }
20499
20500 if ( is_array( $result ) && 0 < count( $result ) ) {
20501 // If found at least one license, return license collection even if there are errors.
20502 return $result;
20503 }
20504
20505 if ( ! empty( $api_errors ) ) {
20506 // If found any errors and no licenses, return first error.
20507 return $api_errors[0];
20508 }
20509
20510 // Fallback to empty licenses list.
20511 return $result;
20512 }
20513
20514 /**
20515 * @author Vova Feldman (@svovaf)
20516 * @since 2.0.0
20517 *
20518 * @param number $license_id
20519 * @param string $license_key
20520 *
20521 * @return \FS_Plugin_License|object
20522 */
20523 private function fetch_license_by_key( $license_id, $license_key ) {
20524 $this->_logger->entrance();
20525
20526 $api = $this->get_current_or_network_user_api_scope();
20527
20528 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
20529
20530 return $this->is_api_result_entity( $result ) ?
20531 new FS_Plugin_License( $result ) :
20532 $result;
20533 }
20534
20535 /**
20536 * @author Vova Feldman (@svovaf)
20537 * @since 1.2.0
20538 * @uses FS_Api
20539 *
20540 * @param number|bool $plugin_id
20541 * @param bool $flush
20542 *
20543 * @return FS_Payment[]|object
20544 */
20545 function _fetch_payments( $plugin_id = false, $flush = false ) {
20546 $this->_logger->entrance();
20547
20548 $api = $this->get_api_user_scope();
20549
20550 if ( ! is_numeric( $plugin_id ) ) {
20551 $plugin_id = $this->_plugin->id;
20552 }
20553
20554 $include_bundles = (
20555 is_object( $this->_plugin ) &&
20556 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
20557 );
20558
20559 $result = $api->get(
20560 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
20561 $flush
20562 );
20563
20564 if ( ! isset( $result->error ) ) {
20565 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
20566 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
20567 }
20568 $result = $result->payments;
20569 }
20570
20571 return $result;
20572 }
20573
20574 /**
20575 * @author Vova Feldman (@svovaf)
20576 * @since 1.2.1.5
20577 * @uses FS_Api
20578 *
20579 * @param bool $flush
20580 *
20581 * @return \FS_Billing|mixed
20582 */
20583 function _fetch_billing( $flush = false ) {
20584 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
20585
20586 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
20587
20588 if ( $this->is_api_result_entity( $billing ) ) {
20589 $billing = new FS_Billing( $billing );
20590 }
20591
20592 return $billing;
20593 }
20594
20595 /**
20596 * @author Vova Feldman (@svovaf)
20597 * @since 1.0.5
20598 *
20599 * @param FS_Plugin_License[] $licenses
20600 * @param number $module_id
20601 */
20602 private function _update_licenses( $licenses, $module_id ) {
20603 $this->_logger->entrance();
20604
20605 if ( is_array( $licenses ) ) {
20606 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
20607 $licenses[ $i ]->updated = time();
20608 }
20609 }
20610
20611 $this->_store_licenses( true, $module_id, $licenses );
20612 }
20613
20614 /**
20615 * @author Vova Feldman (@svovaf)
20616 * @since 1.0.4
20617 *
20618 * @param bool|number $plugin_id
20619 * @param bool $flush Since 1.1.7.3
20620 * @param int $expiration Since 1.2.2.7
20621 * @param bool|string $newer_than Since 2.2.1
20622 * @param bool $fetch_upgrade_notice Since 2.12.1
20623 *
20624 * @return object|false New plugin tag info if exist.
20625 */
20626 private function _fetch_newer_version(
20627 $plugin_id = false,
20628 $flush = true,
20629 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
20630 $newer_than = false,
20631 $fetch_upgrade_notice = true
20632 ) {
20633 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than, false, $fetch_upgrade_notice );
20634
20635 if ( ! is_object( $latest_tag ) ) {
20636 return false;
20637 }
20638
20639 $plugin_version = $this->get_plugin_version();
20640
20641 // Check if version is actually newer.
20642 $has_new_version =
20643 // If it's an non-installed add-on then always return latest.
20644 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
20645 // Compare versions.
20646 version_compare( $plugin_version, $latest_tag->version, '<' );
20647
20648 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
20649
20650 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
20651
20652 $this->_storage->beta_data = array(
20653 'is_beta' => $is_latest_version_beta,
20654 'version' => $latest_tag->version
20655 );
20656
20657 return $has_new_version ? $latest_tag : false;
20658 }
20659
20660 /**
20661 * @author Vova Feldman (@svovaf)
20662 * @since 1.0.5
20663 *
20664 * @param bool|number $plugin_id
20665 * @param bool $flush Since 1.1.7.3
20666 *
20667 * @return bool|FS_Plugin_Tag
20668 */
20669 function get_update( $plugin_id = false, $flush = true ) {
20670 $this->_logger->entrance();
20671
20672 if ( ! is_numeric( $plugin_id ) ) {
20673 $plugin_id = $this->_plugin->id;
20674 }
20675
20676 $this->check_updates( true, $plugin_id, $flush );
20677
20678 $updates = $this->get_all_updates();
20679
20680 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
20681 }
20682
20683 /**
20684 * Check if site assigned with active license.
20685 *
20686 * @author Vova Feldman (@svovaf)
20687 * @since 1.0.6
20688 *
20689 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
20690 */
20691 function has_active_license() {
20692 return (
20693 is_object( $this->_license ) &&
20694 is_numeric( $this->_license->id ) &&
20695 ! $this->_license->is_expired()
20696 );
20697 }
20698
20699 /**
20700 * Check if site assigned with active & valid (not expired) license.
20701 *
20702 * @author Vova Feldman (@svovaf)
20703 * @since 1.2.1
20704 *
20705 * @param bool $check_expiration
20706 */
20707 function has_active_valid_license( $check_expiration = true ) {
20708 return self::is_active_valid_license( $this->_license, $check_expiration );
20709 }
20710
20711 /**
20712 * @author Leo Fajardo (@leorw)
20713 * @since 2.3.1
20714 */
20715 function is_data_debug_mode() {
20716 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
20717 return false;
20718 }
20719
20720 $fs = $this->is_addon() ?
20721 $this->get_parent_instance() :
20722 $this;
20723
20724 if ( $fs->is_network_active() && fs_is_network_admin() ) {
20725 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
20726 } else {
20727 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
20728 }
20729
20730 return ( 'true' === $is_developer_license_debug_mode );
20731 }
20732
20733 /**
20734 * @author Leo Fajardo (@leorw)
20735 * @since 2.3.1
20736 */
20737 function _set_data_debug_mode() {
20738 if ( ! $this->is_whitelabeled( true ) ) {
20739 return;
20740 }
20741
20742 $license_or_user_key = fs_request_get_raw( 'license_or_user_key' );
20743
20744 $transient_value = ( ! empty( $license_or_user_key ) ) ?
20745 'true' :
20746 'false';
20747
20748 if ( 'true' === $transient_value ) {
20749 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
20750 'last_license_key' :
20751 'last_license_user_key'
20752 );
20753
20754 if ( md5( $license_or_user_key ) !== $stored_key ) {
20755 $this->shoot_ajax_failure( sprintf(
20756 '%s... %s',
20757 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
20758 $this->get_text_inline(
20759 'seems like the key you entered doesn\'t match our records.',
20760 'developer-or-license-not-found'
20761 )
20762 ) );
20763 }
20764 }
20765
20766 if ( $this->is_network_active() && fs_is_network_admin() ) {
20767 set_site_transient(
20768 "fs_{$this->get_id()}_data_debug_mode",
20769 $transient_value,
20770 WP_FS__TIME_24_HOURS_IN_SEC / 24
20771 );
20772 } else {
20773 set_transient(
20774 "fs_{$this->get_id()}_data_debug_mode",
20775 $transient_value,
20776 WP_FS__TIME_24_HOURS_IN_SEC / 24
20777 );
20778 }
20779
20780 if ( 'true' === $transient_value ) {
20781 $this->_admin_notices->add_sticky(
20782 $this->get_text_inline(
20783 '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.',
20784 'data_debug_mode_enabled'
20785 ),
20786 'data_debug_mode_enabled'
20787 );
20788 }
20789
20790 $this->shoot_ajax_success();
20791 }
20792
20793 /**
20794 * Check if a given license is active & valid (not expired).
20795 *
20796 * @author Vova Feldman (@svovaf)
20797 * @since 2.1.3
20798 *
20799 * @param FS_Plugin_License $license
20800 * @param bool $check_expiration
20801 *
20802 * @return bool
20803 */
20804 private static function is_active_valid_license( $license, $check_expiration = true ) {
20805 return (
20806 is_object( $license ) &&
20807 FS_Plugin_License::is_valid_id( $license->id ) &&
20808 $license->is_active() &&
20809 ( ! $check_expiration || $license->is_valid() )
20810 );
20811 }
20812
20813 /**
20814 * Checks if there's any site that is associated with an active & valid license.
20815 * This logic is used to determine if the admin can download the premium code base from a network level admin.
20816 *
20817 * @author Vova Feldman (@svovaf)
20818 * @since 2.1.3
20819 *
20820 * @return bool
20821 */
20822 function has_any_active_valid_license() {
20823 if ( ! fs_is_network_admin() ) {
20824 return $this->has_active_valid_license();
20825 }
20826
20827 $installs = $this->get_blog_install_map();
20828 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
20829
20830 foreach ( $installs as $blog_id => $install ) {
20831 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20832 continue;
20833 }
20834
20835 foreach ( $all_plugin_licenses as $license ) {
20836 if ( $license->id == $install->license_id ) {
20837 if ( self::is_active_valid_license( $license ) ) {
20838 return true;
20839 }
20840 }
20841 }
20842 }
20843
20844 return false;
20845 }
20846
20847 /**
20848 * Check if site assigned with license with enabled features.
20849 *
20850 * @author Vova Feldman (@svovaf)
20851 * @since 1.0.6
20852 *
20853 * @return bool
20854 */
20855 function has_features_enabled_license() {
20856 return (
20857 is_object( $this->_license ) &&
20858 is_numeric( $this->_license->id ) &&
20859 $this->_license->is_features_enabled()
20860 );
20861 }
20862
20863 /**
20864 * Checks if the product is activated with a bundle license.
20865 *
20866 * @author Leo Fajardo (@leorw)
20867 * @since 2.4.0
20868 *
20869 * @return bool
20870 */
20871 function is_activated_with_bundle_license() {
20872 if ( ! $this->has_features_enabled_license() ) {
20873 return false;
20874 }
20875
20876 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
20877 }
20878
20879 /**
20880 * Check if user is a trial or have feature enabled license.
20881 *
20882 * @author Vova Feldman (@svovaf)
20883 * @since 1.1.7
20884 *
20885 * @return bool
20886 */
20887 function can_use_premium_code() {
20888 return $this->is_trial() || $this->has_features_enabled_license();
20889 }
20890
20891 /**
20892 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
20893 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
20894 * the context user is authenticated.
20895 *
20896 * @author Leo Fajardo (@leorw)
20897 * @since 1.2.2
20898 *
20899 * @return bool
20900 */
20901 function is_user_admin() {
20902 /**
20903 * Require a super-admin when network activated, running from the network level OR if
20904 * running from the site level but not delegated the opt-in.
20905 *
20906 * @author Vova Feldman (@svovaf)
20907 * @since 2.0.0
20908 */
20909 if ( $this->_is_network_active &&
20910 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
20911 ) {
20912 return is_super_admin();
20913 }
20914
20915 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
20916 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
20917 }
20918
20919 /**
20920 * Sync site's plan.
20921 *
20922 * @author Vova Feldman (@svovaf)
20923 * @since 1.0.3
20924 *
20925 * @uses FS_Api
20926 *
20927 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
20928 * the admin.
20929 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
20930 * network-level "Account" page.
20931 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
20932 * `_sync_plugin_license` method in order to switch to the previous blog when sending
20933 * updates for a single site in case `execute_cron` has switched to a different blog.
20934 */
20935 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
20936 $this->_logger->entrance();
20937
20938 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
20939
20940 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
20941
20942 if ( $is_addon_sync ) {
20943 $this->_sync_addon_license( $plugin_id, $background );
20944 } else {
20945 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
20946 }
20947
20948 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
20949 }
20950
20951 /**
20952 * Sync plugin's add-on license.
20953 *
20954 * @author Vova Feldman (@svovaf)
20955 * @since 1.0.6
20956 * @uses FS_Api
20957 *
20958 * @param number $addon_id
20959 * @param bool $background
20960 */
20961 private function _sync_addon_license( $addon_id, $background ) {
20962 $this->_logger->entrance();
20963
20964 if ( $this->is_addon_activated( $addon_id ) ) {
20965 // If already installed, use add-on sync.
20966 $fs_addon = self::get_instance_by_id( $addon_id );
20967
20968 if (
20969 // Add-on is network activated and network integrated.
20970 $fs_addon->is_network_active() ||
20971 // Background sync cron.
20972 self::is_cron() ||
20973 // Add-on is not network activated or not network integrated.
20974 ! fs_is_network_admin()
20975 ) {
20976 $fs_addon->_sync_license( $background );
20977
20978 return;
20979 }
20980 }
20981
20982 // Validate add-on exists.
20983 $addon = $this->get_addon( $addon_id );
20984
20985 if ( ! is_object( $addon ) ) {
20986 return;
20987 }
20988
20989 // Add add-on into account add-ons.
20990 $account_addons = $this->get_account_addons();
20991 if ( ! is_array( $account_addons ) ) {
20992 $account_addons = array();
20993 }
20994 $account_addons[] = $addon->id;
20995 $account_addons = array_unique( $account_addons );
20996 $this->_store_account_addons( $account_addons );
20997
20998 // Load add-on licenses.
20999 $licenses = $this->_fetch_licenses( $addon->id );
21000
21001 // Sync add-on licenses.
21002 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
21003 $this->_update_licenses( $licenses, $addon->id );
21004
21005 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
21006 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
21007
21008 if ( ! isset( $plans_result->error ) ) {
21009 $plans = array();
21010 foreach ( $plans_result->plans as $plan ) {
21011 $plans[] = new FS_Plugin_Plan( $plan );
21012 }
21013
21014 $this->_admin_notices->add_sticky(
21015 sprintf(
21016 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
21017 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
21018 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
21019 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
21020 $addon->title
21021 ) . ' ' . $this->get_latest_download_link(
21022 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
21023 $addon_id
21024 ),
21025 'addon_plan_upgraded_' . $addon->slug,
21026 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
21027 );
21028 }
21029 }
21030 }
21031 }
21032
21033 /**
21034 * Sync site's plugin plan.
21035 *
21036 * @author Vova Feldman (@svovaf)
21037 * @since 1.0.6
21038 * @uses FS_Api
21039 *
21040 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
21041 * @param bool $send_installs_update Since 2.0.0
21042 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
21043 * syncing its license from the network-level "Account" page (e.g.: after
21044 * activating a license only for the single install).
21045 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
21046 * can be used here to switch to the previous blog in case `execute_cron`
21047 * has switched to a different blog.
21048 */
21049 private function _sync_plugin_license(
21050 $background = false,
21051 $send_installs_update = true,
21052 $is_context_single_site = false,
21053 $current_blog_id = null
21054 ) {
21055 $this->_logger->entrance();
21056
21057 $plan_change = 'none';
21058
21059 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
21060
21061 if ( ! $send_installs_update ) {
21062 $site = $this->_site;
21063 } else {
21064 /**
21065 * Sync site info.
21066 *
21067 * @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.
21068 */
21069 if ( $is_site_level_sync ) {
21070 /**
21071 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
21072 *
21073 * @author Leo Fajardo (@leorw)
21074 * @since 2.2.3
21075 */
21076 if ( is_numeric( $current_blog_id ) ) {
21077 $this->switch_to_blog( $current_blog_id );
21078 }
21079
21080 $result = $this->send_install_update( array(), true, true );
21081 $is_valid = $this->is_api_result_entity( $result );
21082 } else {
21083 $result = $this->send_installs_update( array(), true, true );
21084 $is_valid = $this->is_api_result_object( $result, 'installs' );
21085 }
21086
21087 if ( ! $is_valid ) {
21088 if ( $is_context_single_site ) {
21089 // Switch back to the main blog so that the following logic will have the right entities.
21090 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21091 }
21092
21093 // Show API message only if not background sync or if paying customer.
21094 if ( ! $background || $this->is_paying() ) {
21095 // Try to ping API to see if not blocked.
21096 if ( FS_Api::is_blocked( $result ) ) {
21097 /**
21098 * @author Vova Feldman (@svovaf)
21099 * @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.
21100 */
21101 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
21102 // Add notice immediately if not a background sync.
21103 $add_notice = ( ! $background );
21104
21105 if ( ! $add_notice ) {
21106 $counter = (int) get_transient( '_fs_api_connection_retry_counter' );
21107
21108 // We only want to add the notice after 3 consecutive failures.
21109 $add_notice = ( 3 <= $counter );
21110
21111 if ( ! $add_notice ) {
21112 /**
21113 * 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.
21114 *
21115 * 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.
21116 */
21117 set_transient( '_fs_api_connection_retry_counter', $counter + 1, WP_FS__TIME_WEEK_IN_SEC );
21118 }
21119 }
21120
21121 // Add notice instantly for not-background sync and only after 3 failed attempts for background sync.
21122 if ( $add_notice ) {
21123 self::$_global_admin_notices->add(
21124 $this->generate_api_blocked_notice_message_from_result( $result ),
21125 '',
21126 'error',
21127 $background,
21128 'api_blocked'
21129 );
21130
21131 add_action( 'admin_footer', array( 'Freemius', '_add_api_connectivity_notice_handler_js' ) );
21132
21133 // Notice was just shown, reset connectivity counter.
21134 delete_transient( '_fs_api_connection_retry_counter' );
21135 }
21136 }
21137 } else if ( is_object( $result ) ) {
21138 // Authentication params are broken.
21139 $this->_admin_notices->add(
21140 $this->get_text_inline( 'It seems like one of the authentication parameters is wrong. Update your Public Key, Secret Key & User ID, and try again.', 'wrong-authentication-param-message' ) . '<br> ' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . var_export( $result->error, true ),
21141 '',
21142 'error'
21143 );
21144 }
21145 }
21146
21147 // No reason to continue with license sync while there are API issues.
21148 return;
21149 }
21150
21151 // API is working now. Delete the transient and start afresh.
21152 delete_transient('_fs_api_connection_retry_counter');
21153
21154 if ( $is_site_level_sync ) {
21155 $site = new FS_Site( $result );
21156 } else {
21157 // Map site addresses to their blog IDs.
21158 $address_to_blog_map = $this->get_address_to_blog_map();
21159
21160 // Find the current context install.
21161 $site = null;
21162 foreach ( $result->installs as $install ) {
21163 if ( $install->id == $this->_site->id ) {
21164 $site = new FS_Site( $install );
21165 } else {
21166 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
21167 $blog_id = $address_to_blog_map[ $address ];
21168
21169 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
21170 }
21171 }
21172 }
21173
21174 // Sync plans.
21175 $this->_sync_plans();
21176 }
21177
21178 // Remove sticky API connectivity message.
21179 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
21180
21181 if ( ! $this->has_paid_plan() ) {
21182 $this->_site = $site;
21183 $this->_store_site(
21184 true,
21185 $is_site_level_sync ?
21186 null :
21187 $this->get_network_install_blog_id()
21188 );
21189 } else {
21190 $context_blog_id = 0;
21191
21192 if ( $is_context_single_site ) {
21193 $context_blog_id = get_current_blog_id();
21194
21195 // Switch back to the main blog in order to properly sync the license.
21196 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21197 }
21198
21199 /**
21200 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
21201 * associated with that ID is not included in the user's licenses collection.
21202 * Save previous value to manage remote license renewals.
21203 */
21204 $was_license_expired_before_sync = is_object( $this->_license ) && $this->_license->is_expired();
21205 $this->_sync_licenses(
21206 $site->license_id,
21207 ( $is_context_single_site ?
21208 $context_blog_id :
21209 null
21210 )
21211 );
21212
21213 if ( $is_context_single_site ) {
21214 $this->switch_to_blog( $context_blog_id );
21215 }
21216
21217 // Check if plan / license changed.
21218 if ( $site->plan_id != $this->_site->plan_id ||
21219 // Check if trial started.
21220 $site->trial_plan_id != $this->_site->trial_plan_id ||
21221 $site->trial_ends != $this->_site->trial_ends ||
21222 // Check if license changed.
21223 $site->license_id != $this->_site->license_id
21224 ) {
21225 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
21226 // New trial started.
21227 $this->_site = $site;
21228 $plan_change = 'trial_started';
21229
21230 // For trial with subscription use-case.
21231 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
21232
21233 if ( is_object( $new_license ) && $new_license->is_valid() ) {
21234 $this->_site = $site;
21235 $this->_update_site_license( $new_license );
21236 $this->_store_licenses();
21237
21238 $this->_sync_site_subscription( $this->_license );
21239 }
21240 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
21241 // Was in trial, but now trial expired and no license ID.
21242 // New trial started.
21243 $this->_site = $site;
21244 $plan_change = 'trial_expired';
21245 } else {
21246 $is_free = $this->is_free_plan();
21247
21248 // Make sure license exist and not expired.
21249 $new_license = is_null( $site->license_id ) ?
21250 null :
21251 $this->_get_license_by_id( $site->license_id );
21252
21253 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
21254 // License cancelled.
21255 $this->_site = $site;
21256 $this->_update_site_license( $new_license );
21257 $this->_store_licenses();
21258
21259 $plan_change = 'cancelled';
21260 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
21261 // The license is expired, so ignore upgrade method.
21262 $this->_site = $site;
21263 } else {
21264 // License changed.
21265 $this->_site = $site;
21266
21267 /**
21268 * IMPORTANT:
21269 * 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.
21270 *
21271 * @author Vova Feldman (@svovaf)
21272 * @since 2.0.0
21273 */
21274 $this->_update_site_license( $new_license );
21275
21276 if ( ! $is_context_single_site &&
21277 fs_is_network_admin() &&
21278 $this->_is_network_active &&
21279 $new_license->quota > 1 &&
21280 get_blog_count() > 1
21281 ) {
21282 // See if license can activated on all sites.
21283 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
21284 if ( ! fs_request_get_bool( 'auto_install' ) ) {
21285 // Open the license activation dialog box on the account page.
21286 add_action( 'admin_footer', array(
21287 &$this,
21288 '_open_license_activation_dialog_box'
21289 ) );
21290 }
21291 }
21292 }
21293
21294 $this->_store_licenses();
21295
21296 $plan_change = $is_free ?
21297 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
21298 ( is_object( $new_license ) ?
21299 'changed' :
21300 'downgraded' );
21301 }
21302 }
21303
21304 // Store updated site info.
21305 $this->_store_site(
21306 true,
21307 $is_site_level_sync ?
21308 null :
21309 $this->get_network_install_blog_id()
21310 );
21311 } else {
21312 if ( ! is_object( $this->_license ) ) {
21313 $this->maybe_update_whitelabel_flag(
21314 FS_Plugin_License::is_valid_id( $site->license_id ) ?
21315 $this->get_license_by_id( $site->license_id ) :
21316 null
21317 );
21318 } else {
21319 $this->maybe_update_whitelabel_flag( $this->_license );
21320
21321 if ( $this->_license->is_expired() ) {
21322 if ( ! $this->has_features_enabled_license() ) {
21323 $this->_deactivate_license();
21324 $plan_change = 'downgraded';
21325 } else {
21326 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
21327
21328 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
21329 /**
21330 * Show the expired license notice every 14 days.
21331 *
21332 * @author Leo Fajardo (@leorw)
21333 * @since 2.3.1
21334 */
21335 $plan_change = 'expired';
21336 }
21337 }
21338 } else if ( $was_license_expired_before_sync ) {
21339 /**
21340 * If license was expired but it is not anymore.
21341 *
21342 *
21343 * @author Daniele Alessandra (@danielealessandra)
21344 */
21345 $plan_change = 'extended';
21346 }
21347 }
21348
21349 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
21350 $this->_sync_site_subscription( $this->_license );
21351 }
21352 }
21353
21354 if ( ! $this->is_addon() &&
21355 $this->_site->is_beta() !== $site->is_beta()
21356 ) {
21357 // Beta flag updated.
21358 $this->_site = $site;
21359
21360 $this->_store_site(
21361 true,
21362 $is_site_level_sync ?
21363 null :
21364 $this->get_network_install_blog_id()
21365 );
21366 }
21367
21368 if ( $this->is_addon() || $this->has_addons() ) {
21369 /**
21370 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21371 * an updated valid user licenses collection will be fetched from the server which is used to also
21372 * update the account add-ons (add-ons the user has licenses for).
21373 *
21374 * @author Leo Fajardo (@leorw)
21375 * @since 2.2.4
21376 */
21377 $this->purge_valid_user_licenses_cache();
21378 }
21379 }
21380
21381 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21382
21383 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
21384 switch ( $plan_change ) {
21385 case 'none':
21386 if ( ! $background && is_admin() ) {
21387 $plan = $this->is_trial() ?
21388 $this->get_trial_plan() :
21389 $this->get_plan();
21390
21391 if ( $plan->is_free() ) {
21392 $this->_admin_notices->add(
21393 sprintf(
21394 $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' ),
21395 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
21396 ) . ' ' . sprintf(
21397 '<a href="%s">%s</a>',
21398 $this->contact_url(
21399 'bug',
21400 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' ),
21401 strtoupper( $plan->name )
21402 )
21403 ),
21404 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21405 ),
21406 $hmm_text
21407 );
21408 }
21409 }
21410 break;
21411 case 'upgraded':
21412 case 'activated':
21413 $this->add_after_plan_activation_or_upgrade_instructions_notice( 'upgraded' === $plan_change );
21414
21415 $this->_admin_notices->remove_sticky( array(
21416 'trial_started',
21417 'trial_promotion',
21418 'trial_expired',
21419 'activation_complete',
21420 'license_expired',
21421 ) );
21422 break;
21423 case 'extended':
21424 $this->_admin_notices->remove_sticky( array(
21425 'trial_expired',
21426 'license_expired',
21427 ) );
21428 break;
21429 case 'changed':
21430 $this->_admin_notices->add_sticky(
21431 sprintf(
21432 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
21433 $this->get_plan_title()
21434 ),
21435 'plan_changed'
21436 );
21437
21438 $this->_admin_notices->remove_sticky( array(
21439 'trial_started',
21440 'trial_promotion',
21441 'trial_expired',
21442 'activation_complete',
21443 ) );
21444 break;
21445 case 'downgraded':
21446 $this->_admin_notices->add_sticky(
21447 ($this->has_free_plan() ?
21448 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 ) :
21449 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21450 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) ) ),
21451 'license_expired',
21452 $hmm_text
21453 );
21454 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21455 break;
21456 case 'cancelled':
21457 $this->_admin_notices->add(
21458 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
21459 sprintf(
21460 '<a href="%s">%s</a>',
21461 $this->contact_url( 'bug' ),
21462 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21463 ),
21464 $hmm_text,
21465 'error'
21466 );
21467 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21468 break;
21469 case 'expired':
21470 $this->_admin_notices->add_sticky(
21471 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 ),
21472 'license_expired',
21473 $hmm_text
21474 );
21475
21476 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
21477
21478 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21479 break;
21480 case 'trial_started':
21481 $this->add_complete_upgrade_instructions_notice(
21482 sprintf(
21483 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
21484 '<i>' . $this->get_plugin_name() . '</i>'
21485 ),
21486 'trial_started',
21487 $this->get_trial_plan()->title
21488 );
21489
21490 $this->_admin_notices->remove_sticky( array(
21491 'trial_promotion',
21492 ) );
21493 break;
21494 case 'trial_expired':
21495 $this->_admin_notices->add_sticky(
21496 ($this->has_free_plan() ?
21497 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
21498 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21499 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))),
21500 'trial_expired',
21501 $hmm_text
21502 );
21503 $this->_admin_notices->remove_sticky( array(
21504 'trial_started',
21505 'trial_promotion',
21506 'plan_upgraded',
21507 ) );
21508 break;
21509 }
21510 }
21511
21512 if ( 'none' !== $plan_change ) {
21513 if (
21514 ! is_object( $this->_license ) ||
21515 ! $this->_license->is_whitelabeled
21516 ) {
21517 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
21518 }
21519
21520 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
21521 }
21522 }
21523
21524 /**
21525 * @author Leo Fajardo (@leorw)
21526 * @since 2.5.4
21527 *
21528 * @param mixed $result
21529 *
21530 * @return string
21531 */
21532 private function generate_api_blocked_notice_message_from_result( $result ) {
21533 $api_domains = $this->apply_filters( 'api_domains', array(
21534 'api.freemius.com',
21535 'wp.freemius.com',
21536 ) );
21537
21538 $api_domains_list_items = '';
21539
21540 foreach( $api_domains as $api_domain ) {
21541 $api_domains_list_items .= "<li>{$api_domain}</li>";
21542 }
21543
21544 $error_message = sprintf(
21545 $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' ),
21546 $this->get_plugin_name(),
21547 "<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>"
21548 );
21549
21550 $error_message =
21551 "<div>{$error_message}</div>" .
21552 '<div class="fs-api-request-error-details" style="display: none">' .
21553 '<strong>' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . '</strong><br>' .
21554 $result->error->message .
21555 '</div>';
21556
21557 return $error_message;
21558 }
21559
21560 /**
21561 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
21562 *
21563 * @author Vova Feldman (@svovaf)
21564 * @since 2.0.0
21565 */
21566 public function _open_license_activation_dialog_box() {
21567 $vars = array( 'license_id' => $this->_site->license_id );
21568 fs_require_once_template( 'js/open-license-activation.php', $vars );
21569 }
21570
21571 /**
21572 * @author Vova Feldman (@svovaf)
21573 * @since 1.0.5
21574 *
21575 * @param bool $background
21576 * @param FS_Plugin_License|null $premium_license
21577 */
21578 protected function _activate_license( $background = false, $premium_license = null ) {
21579 $this->_logger->entrance();
21580
21581 if ( is_null( $premium_license ) ) {
21582 $license_id = fs_request_get( 'license_id' );
21583
21584 if ( is_object( $this->_site ) &&
21585 FS_Plugin_License::is_valid_id( $license_id ) &&
21586 $license_id == $this->_site->license_id
21587 ) {
21588 // License is already activated.
21589 return;
21590 }
21591
21592 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
21593 $this->_get_license_by_id( $license_id ) :
21594 $this->_get_available_premium_license();
21595 }
21596
21597 if ( ! is_object( $premium_license ) ) {
21598 return;
21599 }
21600
21601 if ( ! is_object( $this->_site ) ) {
21602 // Not yet opted-in.
21603 $user = $this->get_current_or_network_user();
21604 if ( ! is_object( $user ) ) {
21605 $user = self::_get_user_by_id( $premium_license->user_id );
21606 }
21607
21608 if ( is_object( $user ) ) {
21609 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
21610 } else {
21611 $this->opt_in(
21612 false,
21613 false,
21614 false,
21615 $premium_license->secret_key,
21616 false,
21617 false,
21618 false,
21619 null,
21620 array(),
21621 true,
21622 $premium_license->user_id
21623 );
21624
21625 return;
21626 }
21627 }
21628
21629
21630 /**
21631 * If the premium license is already associated with the install, just
21632 * update the license reference (activation is not required).
21633 *
21634 * @since 1.1.9
21635 */
21636 if ( $premium_license->id == $this->_site->license_id ) {
21637 // License is already activated.
21638 $this->_update_site_license( $premium_license );
21639 $this->_store_account();
21640
21641 return;
21642 }
21643
21644 if ( $this->_site->user_id != $premium_license->user_id ) {
21645 $api_request_params = array( 'license_key' => $premium_license->secret_key );
21646 } else {
21647 $api_request_params = array();
21648 }
21649
21650 $api = $this->get_api_site_scope();
21651 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
21652
21653 if ( ! $this->is_api_result_entity( $license ) ) {
21654 if ( ! $background ) {
21655 $this->_admin_notices->add( sprintf(
21656 '%s %s',
21657 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
21658 ( is_object( $license ) && isset( $license->error ) ?
21659 $license->error->message :
21660 sprintf( '%s<br><code>%s</code>',
21661 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
21662 var_export( $license, true )
21663 )
21664 )
21665 ),
21666 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
21667 'error'
21668 );
21669 }
21670
21671 return;
21672 }
21673
21674 $this->do_action( 'after_license_activation' );
21675
21676 $premium_license = new FS_Plugin_License( $license );
21677
21678 // Updated site plan.
21679 $site = $this->get_api_site_scope()->get( '/', true );
21680 if ( $this->is_api_result_entity( $site ) ) {
21681 $this->_site = new FS_Site( $site );
21682 }
21683 $this->_update_site_license( $premium_license );
21684
21685 $this->_store_account();
21686
21687 if ( $this->is_addon() || $this->has_addons() ) {
21688 /**
21689 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21690 * an updated valid user licenses collection will be fetched from the server which is used to also
21691 * update the account add-ons (add-ons the user has licenses for).
21692 *
21693 * @author Leo Fajardo (@leorw)
21694 * @since 2.2.4
21695 */
21696 $this->purge_valid_user_licenses_cache();
21697 }
21698
21699 if ( ! $background ) {
21700 $this->add_complete_upgrade_instructions_notice(
21701 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ),
21702 'license_activated'
21703 );
21704 }
21705
21706 $this->_admin_notices->remove_sticky( array(
21707 'trial_promotion',
21708 'license_expired',
21709 ) );
21710 }
21711
21712 /**
21713 * @author Vova Feldman (@svovaf)
21714 * @since 1.0.5
21715 *
21716 * @param bool $show_notice
21717 */
21718 protected function _deactivate_license( $show_notice = true ) {
21719 $this->_logger->entrance();
21720
21721 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21722
21723 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
21724 $this->_admin_notices->add(
21725 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() ),
21726 $hmm_text
21727 );
21728
21729 return;
21730 }
21731
21732 $api = $this->get_api_site_scope();
21733 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
21734
21735 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
21736 }
21737
21738 /**
21739 * @author Leo Fajardo (@leorw)
21740 * @since 2.2.1
21741 *
21742 * @param FS_Plugin_License $license
21743 * @param bool|string $hmm_text
21744 * @param bool $show_notice
21745 */
21746 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
21747 if ( isset( $license->error ) ) {
21748 $this->_admin_notices->add(
21749 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
21750 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
21751 $hmm_text,
21752 'error'
21753 );
21754
21755 $this->do_action( 'after_license_deactivation', $license );
21756
21757 return;
21758 }
21759
21760 // Update license cache.
21761 if ( is_array( $this->_licenses ) ) {
21762 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
21763 if ( $license->id == $this->_licenses[ $i ]->id ) {
21764 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
21765 }
21766 }
21767 }
21768
21769 // Update site plan to default.
21770 $this->_sync_plans();
21771 $this->_site->plan_id = $this->_plans[0]->id;
21772 // Unlink license from site.
21773 $this->_update_site_license( null );
21774
21775 $this->_store_account();
21776
21777 $this->do_action( 'after_license_deactivation', $license );
21778
21779 if ( $show_notice ) {
21780 $this->_admin_notices->add(
21781 sprintf( $this->is_only_premium() ?
21782 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
21783 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
21784 $this->get_plan_title()
21785 ),
21786 $this->get_text_inline( 'O.K', 'ok' )
21787 );
21788 }
21789
21790 $this->_admin_notices->remove_sticky( array(
21791 'plan_upgraded',
21792 'license_activated',
21793 ) );
21794 }
21795
21796 /**
21797 * Site plan downgrade.
21798 *
21799 * @author Vova Feldman (@svovaf)
21800 * @since 1.0.4
21801 *
21802 * @return object
21803 *
21804 * @uses FS_Api
21805 */
21806 private function _downgrade_site() {
21807 $this->_logger->entrance();
21808
21809 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
21810
21811 $api = $this->get_api_site_scope();
21812 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
21813
21814 $plan_downgraded = false;
21815 $plan = false;
21816 if ( $this->is_api_result_entity( $site ) ) {
21817 $prev_plan_id = $this->_site->plan_id;
21818
21819 // Update new site plan id.
21820 $this->_site->plan_id = $site->plan_id;
21821
21822 $plan = $this->get_plan();
21823 $subscription = $this->_sync_site_subscription( $this->_license );
21824
21825 // Plan downgraded if plan was changed or subscription was cancelled.
21826 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
21827 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
21828 } else {
21829 // handle different error cases.
21830 $this->handle_license_deactivation_result(
21831 $site,
21832 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
21833 );
21834 }
21835
21836 if ( ! $plan_downgraded ) {
21837 return (object) array(
21838 'error' => (object) array(
21839 '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' )
21840 )
21841 );
21842 }
21843
21844 // Remove previous sticky message about upgrade (if exist).
21845 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21846
21847 $this->_admin_notices->add(
21848 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
21849 $plan->title,
21850 human_time_diff( time(), strtotime( $this->_license->expiration ) )
21851 )
21852 );
21853
21854 // Store site updates.
21855 $this->_store_site();
21856
21857 if ( $deactivate_license &&
21858 ! FS_Plugin_License::is_valid_id( $site->license_id )
21859 ) {
21860 if ( $this->_site->is_localhost() ) {
21861 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
21862 } else {
21863 $this->_license->activated = max( 0, $this->_license->activated - 1 );
21864 }
21865
21866 // Handle successful license deactivation result.
21867 $this->handle_license_deactivation_result( $this->_license );
21868 }
21869
21870 return $site;
21871 }
21872
21873 /**
21874 * @author Vova Feldman (@svovaf)
21875 * @since 1.1.8.1
21876 *
21877 * @param bool|string $plan_name
21878 * @param bool $add_sticky_notice
21879 *
21880 * @return bool If trial was successfully started.
21881 */
21882 function start_trial( $plan_name = false, $add_sticky_notice = false ) {
21883 $this->_logger->entrance();
21884
21885 // Alias.
21886 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
21887
21888 if ( $this->is_trial() ) {
21889 // Already in trial mode.
21890 $this->_admin_notices->add(
21891 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
21892 $oops_text,
21893 'error',
21894 $add_sticky_notice
21895 );
21896
21897 return false;
21898 }
21899
21900 if ( $this->_site->is_trial_utilized() && ! $this->is_payments_sandbox() ) {
21901 // Trial was already utilized.
21902 $this->_admin_notices->add(
21903 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
21904 $oops_text,
21905 'error',
21906 $add_sticky_notice
21907 );
21908
21909 return false;
21910 }
21911
21912 if ( false !== $plan_name ) {
21913 $plan = $this->get_plan_by_name( $plan_name );
21914
21915 if ( false === $plan ) {
21916 // Plan doesn't exist.
21917 $this->_admin_notices->add(
21918 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
21919 $oops_text,
21920 'error',
21921 $add_sticky_notice
21922 );
21923
21924 return false;
21925 }
21926
21927 if ( ! $plan->has_trial() ) {
21928 // Plan doesn't exist.
21929 $this->_admin_notices->add(
21930 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
21931 $oops_text,
21932 'error',
21933 $add_sticky_notice
21934 );
21935
21936 return false;
21937 }
21938 } else {
21939 if ( ! $this->has_trial_plan() ) {
21940 // None of the plans have a trial.
21941 $this->_admin_notices->add(
21942 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
21943 $oops_text,
21944 'error',
21945 $add_sticky_notice
21946 );
21947
21948 return false;
21949 }
21950
21951 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
21952
21953 $plan = $plans_with_trial[0];
21954 }
21955
21956 $trial_params = array();
21957
21958 if ( $this->is_payments_sandbox() ) {
21959 $trial_params['trial_timestamp'] = time();
21960 $trial_params['trial_token'] = FS_Security::instance()->get_trial_token(
21961 $this->get_plugin(),
21962 $plan,
21963 $trial_params['trial_timestamp']
21964 );
21965 }
21966
21967 $api = $this->get_api_site_scope();
21968 $trial = $api->call( "plans/{$plan->id}/trials.json", 'post', $trial_params );
21969
21970 if ( ! $this->is_api_result_entity( $trial ) ) {
21971 // Some API error while trying to start the trial.
21972 $this->_admin_notices->add(
21973 $this->get_api_error_message( $trial ),
21974 $oops_text,
21975 'error',
21976 $add_sticky_notice
21977 );
21978
21979 return false;
21980 }
21981
21982 // Sync license.
21983 $this->_sync_license();
21984
21985 return $this->is_trial();
21986 }
21987
21988 /**
21989 * Cancel site trial.
21990 *
21991 * @author Vova Feldman (@svovaf)
21992 * @since 1.0.9
21993 *
21994 * @return object
21995 *
21996 * @uses FS_Api
21997 */
21998 private function _cancel_trial() {
21999 $this->_logger->entrance();
22000
22001 if ( ! $this->is_trial() ) {
22002 return (object) array(
22003 'error' => (object) array(
22004 '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' )
22005 )
22006 );
22007 }
22008
22009 $trial_plan = $this->get_trial_plan();
22010
22011 $api = $this->get_api_site_scope();
22012 $site = $api->call( 'trials.json', 'delete' );
22013
22014 $trial_cancelled = false;
22015
22016 if ( $this->is_api_result_entity( $site ) ) {
22017 $prev_trial_ends = $this->_site->trial_ends;
22018
22019 if ( $this->is_paid_trial() ) {
22020 $this->_license->expiration = $site->trial_ends;
22021 $this->_license->is_cancelled = true;
22022 $this->_update_site_license( $this->_license );
22023 $this->_store_licenses();
22024
22025 // Clear subscription reference.
22026 $this->_sync_site_subscription( null );
22027 }
22028
22029 // Update site info.
22030 $this->_site = new FS_Site( $site );
22031
22032 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
22033 } else {
22034 // @todo handle different error cases.
22035 }
22036
22037 if ( ! $trial_cancelled ) {
22038 return (object) array(
22039 'error' => (object) array(
22040 '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' )
22041 )
22042 );
22043 }
22044
22045 // Remove previous sticky messages about upgrade or trial (if exist).
22046 $this->_admin_notices->remove_sticky( array(
22047 'trial_started',
22048 'trial_promotion',
22049 'plan_upgraded',
22050 ) );
22051
22052 // Store site updates.
22053 $this->_store_site();
22054
22055 if ( ! $this->is_addon() ||
22056 ! $this->deactivate_premium_only_addon_without_license( true )
22057 ) {
22058 $this->_admin_notices->add(
22059 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
22060 );
22061 }
22062
22063 return $site;
22064 }
22065
22066 /**
22067 * @author Vova Feldman (@svovaf)
22068 * @since 1.0.6
22069 *
22070 * @param bool|number $plugin_id
22071 *
22072 * @return bool
22073 */
22074 private function _is_addon_id( $plugin_id ) {
22075 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
22076 }
22077
22078 /**
22079 * Check if user eligible to download premium version updates.
22080 *
22081 * @author Vova Feldman (@svovaf)
22082 * @since 1.0.6
22083 *
22084 * @return bool
22085 */
22086 private function _can_download_premium() {
22087 return $this->has_any_active_valid_license() ||
22088 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
22089 }
22090
22091 /**
22092 *
22093 * @author Vova Feldman (@svovaf)
22094 * @since 1.0.6
22095 *
22096 * @param bool|number $addon_id
22097 * @param string $type "json" or "zip"
22098 *
22099 * @return string
22100 */
22101 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
22102
22103 $is_addon = $this->_is_addon_id( $addon_id );
22104
22105 $is_premium = null;
22106 if ( ! $is_addon ) {
22107 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
22108 } else if ( $this->is_addon_activated( $addon_id ) ) {
22109 $fs_addon = self::get_instance_by_id( $addon_id );
22110 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
22111 }
22112
22113 // If add-on, then append add-on ID.
22114 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
22115 '/updates/latest.' . $type;
22116
22117 // If add-on and not yet activated, try to fetch based on server licensing.
22118 if ( is_bool( $is_premium ) ) {
22119 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
22120 }
22121
22122 if ( $this->has_secret_key() ) {
22123 $endpoint = add_query_arg( 'type', 'all', $endpoint );
22124 } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) {
22125 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
22126 }
22127
22128 return $endpoint;
22129 }
22130
22131 /**
22132 * @author Vova Feldman (@svovaf)
22133 * @since 1.0.4
22134 *
22135 * @param bool|number $addon_id
22136 * @param bool $flush Since 1.1.7.3
22137 * @param int $expiration Since 1.2.2.7
22138 * @param bool|string $newer_than Since 2.2.1
22139 * @param bool|string $fetch_readme Since 2.2.1
22140 * @param bool $fetch_upgrade_notice Since 2.12.1
22141 *
22142 * @return object|false Plugin latest tag info.
22143 */
22144 function _fetch_latest_version(
22145 $addon_id = false,
22146 $flush = true,
22147 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22148 $newer_than = false,
22149 $fetch_readme = true,
22150 $fetch_upgrade_notice = false
22151 ) {
22152 $this->_logger->entrance();
22153
22154 if ( $this->is_unresolved_clone( true ) ) {
22155 return false;
22156 }
22157
22158 $switch_to_blog_id = null;
22159
22160 /**
22161 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
22162 * @since 1.1.7.4 Also check updates for add-ons.
22163 */
22164 if (
22165 ( ! $this->is_registered() || ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed() ) &&
22166 ! $this->_is_addon_id( $addon_id )
22167 ) {
22168 if ( ! is_multisite() ) {
22169 return false;
22170 }
22171
22172 $installs_map = $this->get_blog_install_map();
22173
22174 foreach ( $installs_map as $blog_id => $install ) {
22175 if ( ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed( $blog_id ) ) {
22176 continue;
22177 }
22178
22179 /**
22180 * @var FS_Site $install
22181 */
22182 if ( $install->is_trial() ) {
22183 $switch_to_blog_id = $blog_id;
22184 break;
22185 }
22186
22187 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
22188 $license = $this->get_license_by_id( $install->license_id );
22189
22190 if ( is_object( $license ) && $license->is_features_enabled() ) {
22191 $switch_to_blog_id = $blog_id;
22192 break;
22193 }
22194 }
22195 }
22196
22197 if ( is_null( $switch_to_blog_id ) ) {
22198 return false;
22199 }
22200 }
22201
22202 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
22203 get_current_blog_id() :
22204 0;
22205
22206 if ( is_numeric( $switch_to_blog_id ) ) {
22207 $this->switch_to_blog( $switch_to_blog_id );
22208 }
22209
22210 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
22211
22212 if ( ! empty( $newer_than ) ) {
22213 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
22214 }
22215
22216 if ( true === $fetch_readme ) {
22217 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
22218
22219 // Don't cache the API response when fetching readme information.
22220 $expiration = null;
22221 }
22222
22223 if ( true === $fetch_upgrade_notice ) {
22224 $latest_version_endpoint = add_query_arg( 'include_upgrade_notice', 'true', $latest_version_endpoint );
22225 }
22226
22227 $tag = $this->get_api_site_or_plugin_scope()->get(
22228 $latest_version_endpoint,
22229 $flush,
22230 $expiration
22231 );
22232
22233 if ( is_numeric( $switch_to_blog_id ) ) {
22234 $this->switch_to_blog( $current_blog_id );
22235 }
22236
22237 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
22238
22239 $this->_logger->departure( 'Latest version ' . $latest_version );
22240
22241 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
22242 }
22243
22244 #----------------------------------------------------------------------------------
22245 #region Download Plugin
22246 #----------------------------------------------------------------------------------
22247
22248 /**
22249 * Download latest plugin version, based on plan.
22250 *
22251 * Not like _download_latest(), this will redirect the page
22252 * to secure download url to prevent dual download (from FS to WP server,
22253 * and then from WP server to the client / browser).
22254 *
22255 * @author Vova Feldman (@svovaf)
22256 * @since 1.0.9
22257 *
22258 * @param bool|number $plugin_id
22259 *
22260 * @uses FS_Api
22261 * @uses wp_redirect()
22262 */
22263 private function download_latest_directly( $plugin_id = false ) {
22264 $this->_logger->entrance();
22265
22266 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
22267 }
22268
22269 /**
22270 * Get latest plugin FS API download URL.
22271 *
22272 * @author Vova Feldman (@svovaf)
22273 * @since 1.0.9
22274 *
22275 * @param bool|number $plugin_id
22276 *
22277 * @return string
22278 */
22279 private function get_latest_download_api_url( $plugin_id = false ) {
22280 $this->_logger->entrance();
22281
22282 $download_api_url = $this->get_api_site_scope()->get_signed_url(
22283 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
22284 );
22285
22286 return str_replace( 'http:', 'https:', $download_api_url );
22287 }
22288
22289 /**
22290 * Get payment invoice URL.
22291 *
22292 * @author Vova Feldman (@svovaf)
22293 * @since 1.2.0
22294 *
22295 * @param bool|number $payment_id
22296 *
22297 * @return string
22298 */
22299 function _get_invoice_api_url( $payment_id = false ) {
22300 $this->_logger->entrance();
22301
22302 $url = $this->get_api_user_scope()->get_signed_url(
22303 "/payments/{$payment_id}/invoice.pdf"
22304 );
22305
22306 if ( ! fs_starts_with( $url, 'https://' ) ) {
22307 // Always use HTTPS for invoices.
22308 $url = 'https' . substr( $url, 4 );
22309 }
22310
22311 return $url;
22312 }
22313
22314 /**
22315 * Get latest plugin download link.
22316 *
22317 * @author Vova Feldman (@svovaf)
22318 * @since 1.0.9
22319 *
22320 * @param string $label
22321 * @param bool|number $plugin_id
22322 *
22323 * @return string
22324 */
22325 private function get_latest_download_link( $label, $plugin_id = false ) {
22326 return sprintf(
22327 '<a target="_blank" rel="noopener" href="%s">%s</a>',
22328 $this->_get_latest_download_local_url( $plugin_id ),
22329 $label
22330 );
22331 }
22332
22333 /**
22334 * Get latest plugin download local URL.
22335 *
22336 * @author Vova Feldman (@svovaf)
22337 * @since 1.0.9
22338 *
22339 * @param bool|number $plugin_id
22340 *
22341 * @return string
22342 */
22343 function _get_latest_download_local_url( $plugin_id = false ) {
22344 // Add timestamp to protect from caching.
22345 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
22346
22347 if ( ! empty( $plugin_id ) ) {
22348 $params['plugin_id'] = $plugin_id;
22349 } else if ( $this->is_addon() ) {
22350 $params['plugin_id'] = $this->get_id();
22351 }
22352
22353 $fs = $this->is_addon() ?
22354 $this->get_parent_instance() :
22355 $this;
22356
22357 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
22358 }
22359
22360 #endregion Download Plugin ------------------------------------------------------------------
22361
22362 /**
22363 * @author Vova Feldman (@svovaf)
22364 * @since 1.0.4
22365 *
22366 * @uses FS_Api
22367 *
22368 * @param bool $background Hints the method if it's a background updates check. If false, it means that
22369 * was initiated by the admin.
22370 * @param bool|number $plugin_id
22371 * @param bool $flush Since 1.1.7.3
22372 */
22373 private function check_updates( $background = false, $plugin_id = false, $flush = true ) {
22374 $this->_logger->entrance();
22375
22376 $newer_than = ( $this->is_premium() ? $this->get_plugin_version() : false );
22377
22378 // Check if there's a newer version for download.
22379 $new_version = $this->_fetch_newer_version(
22380 $plugin_id,
22381 $flush,
22382 FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION,
22383 $newer_than,
22384 ( false !== $newer_than )
22385 );
22386
22387 $update = null;
22388 if ( is_object( $new_version ) ) {
22389 $update = new FS_Plugin_Tag( $new_version );
22390
22391 if ( ! $background ) {
22392 $this->_admin_notices->add(
22393 sprintf(
22394 /* translators: %s: Numeric version number (e.g. '2.1.9' */
22395 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
22396 $update->version,
22397 sprintf(
22398 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22399 $this->get_account_url( 'download_latest' ),
22400 sprintf(
22401 /* translators: %s: plan name (e.g. latest "Professional" version) */
22402 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
22403 $this->get_plan_title()
22404 )
22405 )
22406 ),
22407 $this->get_text_inline( 'New', 'new' ) . '!'
22408 );
22409 }
22410 } else if ( false === $new_version && ! $background ) {
22411 $this->_admin_notices->add(
22412 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
22413 $this->get_text_inline( 'You are all good!', 'you-are-good' )
22414 );
22415 }
22416
22417 $this->_store_update( $update, true, $plugin_id );
22418 }
22419
22420 /**
22421 * @author Vova Feldman (@svovaf)
22422 * @since 1.0.4
22423 *
22424 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
22425 *
22426 * @return FS_Plugin[]
22427 *
22428 * @uses FS_Api
22429 */
22430 private function sync_addons( $flush = false ) {
22431 $this->_logger->entrance();
22432
22433 $api = $this->get_api_site_or_plugin_scope();
22434
22435 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
22436
22437 /**
22438 * @since 1.2.1
22439 *
22440 * If there's a cached version of the add-ons and not asking
22441 * for a flush, just use the currently stored add-ons.
22442 */
22443 if ( ! $flush && $api->is_cached( $path ) ) {
22444 $addons = self::get_all_addons();
22445
22446 return isset( $addons[ $this->_plugin->id ] ) ?
22447 $addons[ $this->_plugin->id ] :
22448 array();
22449 }
22450
22451 $result = $api->get( $path, $flush );
22452
22453 $addons = array();
22454 if ( $this->is_api_result_object( $result, 'plugins' ) &&
22455 is_array( $result->plugins )
22456 ) {
22457 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
22458 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
22459 }
22460
22461 $this->_store_addons( $addons, true );
22462 }
22463
22464 return $addons;
22465 }
22466
22467 /**
22468 * Handle user email update.
22469 *
22470 * @author Vova Feldman (@svovaf)
22471 * @since 1.0.3
22472 * @uses FS_Api
22473 *
22474 * @param string $new_email
22475 *
22476 * @return object
22477 */
22478 private function update_email( $new_email ) {
22479 $this->_logger->entrance();
22480
22481 $api = $this->get_api_user_scope();
22482 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
22483 'email' => $new_email,
22484 'after_email_confirm_url' => $this->_get_admin_page_url(
22485 'account',
22486 array( 'fs_action' => 'sync_user' )
22487 ),
22488 ) );
22489
22490 if ( ! isset( $user->error ) ) {
22491 $this->_user->email = $user->email;
22492 $this->_user->is_verified = $user->is_verified;
22493 $this->_store_user();
22494 } else {
22495 // handle different error cases.
22496 }
22497
22498 return $user;
22499 }
22500
22501 #----------------------------------------------------------------------------------
22502 #region API Error Handling
22503 #----------------------------------------------------------------------------------
22504
22505 /**
22506 * @author Vova Feldman (@svovaf)
22507 * @since 1.1.1
22508 *
22509 * @param mixed $result
22510 *
22511 * @return bool Is API result contains an error.
22512 */
22513 private function is_api_error( $result ) {
22514 return FS_Api::is_api_error( $result );
22515 }
22516
22517 /**
22518 * Checks if given API result is a non-empty and not an error object.
22519 *
22520 * @author Vova Feldman (@svovaf)
22521 * @since 1.2.1.5
22522 *
22523 * @param mixed $result
22524 * @param string|null $required_property Optional property we want to verify that is set.
22525 *
22526 * @return bool
22527 */
22528 function is_api_result_object( $result, $required_property = null ) {
22529 return FS_Api::is_api_result_object( $result, $required_property );
22530 }
22531
22532 /**
22533 * Checks if given API result is a non-empty entity object with non-empty ID.
22534 *
22535 * @author Vova Feldman (@svovaf)
22536 * @since 1.2.1.5
22537 *
22538 * @param mixed $result
22539 *
22540 * @return bool
22541 */
22542 private function is_api_result_entity( $result ) {
22543 return FS_Api::is_api_result_entity( $result );
22544 }
22545
22546 #endregion
22547
22548 /**
22549 * Make sure a given argument is an array of a specific type.
22550 *
22551 * @author Vova Feldman (@svovaf)
22552 * @since 1.2.1.5
22553 *
22554 * @param mixed $array
22555 * @param string $class
22556 *
22557 * @return bool
22558 */
22559 private function is_array_instanceof( $array, $class ) {
22560 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
22561 }
22562
22563 /**
22564 * Start install ownership change.
22565 *
22566 * @author Vova Feldman (@svovaf)
22567 * @since 1.1.1
22568 * @uses FS_Api
22569 *
22570 * @param string $candidate_email
22571 * @param string $transfer_type
22572 *
22573 * @return bool Is ownership change successfully initiated.
22574 */
22575 private function init_change_owner( $candidate_email, $transfer_type ) {
22576 $this->_logger->entrance();
22577
22578 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
22579 $install_ids = array();
22580
22581 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
22582 $install = $install_info['install'];
22583
22584 if ( $this->_user->id != $install->user_id ) {
22585 // Skip add-on installs that are not owned by the parent product's install's owner.
22586 continue;
22587 }
22588
22589 $install_ids[ $slug ] = $install->id;
22590 }
22591
22592 $api = $this->get_api_site_scope();
22593 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
22594 'email' => $candidate_email,
22595 'transfer_type' => $transfer_type,
22596 'install_ids' => implode( ',', array_values( $install_ids ) ),
22597 'after_confirm_url' => $this->_get_admin_page_url(
22598 'account',
22599 array( 'fs_action' => 'change_owner' )
22600 ),
22601 ) );
22602
22603 return ! $this->is_api_error( $result );
22604 }
22605
22606 /**
22607 * Handle install ownership change.
22608 *
22609 * @author Vova Feldman (@svovaf)
22610 * @since 1.1.1
22611 * @uses FS_Api
22612 *
22613 * @return bool Was ownership change successfully complete.
22614 */
22615 private function complete_change_owner() {
22616 $this->_logger->entrance();
22617
22618 $install_ids = fs_request_get( 'install_ids' );
22619
22620 if ( ! empty( $install_ids ) ) {
22621 $install_ids = explode( ',', $install_ids );
22622
22623 foreach ( $install_ids as $key => $install_id ) {
22624 if ( ! FS_Site::is_valid_id( $install_id ) ) {
22625 unset( $install_ids[ $key ] );
22626 }
22627 }
22628 }
22629
22630 if ( ! is_array( $install_ids ) ) {
22631 $install_ids = array();
22632 }
22633
22634 $user = new FS_User();
22635 $user->id = fs_request_get( 'user_id' );
22636 $user->public_key = fs_request_get_raw( 'user_public_key' );
22637 $user->secret_key = fs_request_get_raw( 'user_secret_key' );
22638
22639 $prev_user = $this->_user;
22640 $this->_user = $user;
22641
22642 $result = $this->get_api_user_scope( true )->get(
22643 "/installs.json?install_ids=" . implode( ',', $install_ids )
22644 );
22645
22646 $current_blog_sites = self::get_all_sites( $this->get_module_type() );
22647
22648 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22649 $site_id_slug_map = array();
22650
22651 foreach ( $current_blog_sites as $slug => $site ) {
22652 $site_id_slug_map[ $site->id ] = $slug;
22653 }
22654
22655 foreach ( $result->installs as $install ) {
22656 $site = new FS_Site( $install );
22657
22658 if ( ! isset( $site_id_slug_map[ $install->id ] ) ) {
22659 continue;
22660 }
22661
22662 $current_blog_sites[ $site_id_slug_map[ $install->id ] ] = clone $site;
22663
22664 if ( $this->_site->id == $site->id ) {
22665 $this->_site = $site;
22666 }
22667 }
22668 }
22669
22670 // Validate install's user and given user.
22671 if ( $user->id != $this->_site->user_id ) {
22672 $this->_user = $prev_user;
22673
22674 return false;
22675 }
22676
22677 $this->set_account_option( 'sites', $current_blog_sites, true );
22678
22679 // Fetch new user information.
22680 $user_result = $this->get_api_user_scope( true )->get();
22681 $user = new FS_User( $user_result );
22682 $this->_user = $user;
22683
22684 $this->_set_account( $user, $this->_site );
22685
22686 $remove_user = true;
22687 $all_modules_sites = FS_DebugManager::get_all_modules_sites();
22688
22689 foreach ( $all_modules_sites as $sites_by_module_type ) {
22690 foreach ( $sites_by_module_type as $sites_by_slug ) {
22691 foreach ( $sites_by_slug as $site ) {
22692 if ( $prev_user->id == $site->user_id ) {
22693 $remove_user = false;
22694 break;
22695 }
22696 }
22697
22698 if ( ! $remove_user ) {
22699 break;
22700 }
22701 }
22702
22703 if ( ! $remove_user ) {
22704 break;
22705 }
22706 }
22707
22708 if ( $remove_user ) {
22709 $users = self::get_all_users();
22710
22711 if ( isset( $users[ $prev_user->id ] ) ) {
22712 unset( $users[ $prev_user->id ] );
22713 } else {
22714 // If the prev user wasn't found by the key, iterate over the users collection.
22715 foreach ( $users as $key => $user ) {
22716 if ( $user->id == $prev_user->id ) {
22717 unset( $users[ $key ] );
22718 break;
22719 }
22720 }
22721 }
22722
22723 $this->set_account_option( 'users', $users, true );
22724 }
22725
22726 return true;
22727 }
22728
22729 /**
22730 * Completes ownership change by license.
22731 *
22732 * @author Leo Fajardo (@leorw)
22733 * @since 2.3.2
22734 *
22735 * @param number $user_id
22736 * @param array[string]number $install_ids_by_slug_map
22737 *
22738 */
22739 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
22740 $this->_logger->entrance();
22741
22742 $this->sync_user_by_current_install( $user_id );
22743
22744 $result = $this->get_api_user_scope( true )->get(
22745 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
22746 );
22747
22748 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22749 $sites = self::get_all_sites( $this->get_module_type() );
22750 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
22751
22752 foreach ( $result->installs as $install ) {
22753 $site = new FS_Site( $install );
22754
22755 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
22756 }
22757
22758 $this->set_account_option( 'sites', $sites, true );
22759 }
22760 }
22761
22762 /**
22763 * Handle user name update.
22764 *
22765 * @author Vova Feldman (@svovaf)
22766 * @since 1.0.9
22767 * @uses FS_Api
22768 *
22769 * @return object
22770 */
22771 private function update_user_name() {
22772 $this->_logger->entrance();
22773 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
22774
22775 $api = $this->get_api_user_scope();
22776 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
22777 'name' => $name,
22778 ) );
22779
22780 if ( ! isset( $user->error ) ) {
22781 $this->_user->first = $user->first;
22782 $this->_user->last = $user->last;
22783 $this->_store_user();
22784 } else {
22785 // handle different error cases.
22786
22787 }
22788
22789 return $user;
22790 }
22791
22792 /**
22793 * Verify user email.
22794 *
22795 * @author Vova Feldman (@svovaf)
22796 * @since 1.0.3
22797 * @uses FS_Api
22798 */
22799 private function verify_email() {
22800 $this->_handle_account_user_sync();
22801
22802 if ( $this->_user->is_verified() ) {
22803 return;
22804 }
22805
22806 $api = $this->get_api_site_scope();
22807 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
22808 'after_email_confirm_url' => $this->_get_admin_page_url(
22809 'account',
22810 array( 'fs_action' => 'sync_user' )
22811 )
22812 ) );
22813
22814 if ( ! isset( $result->error ) ) {
22815 $this->_admin_notices->add( sprintf(
22816 $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' ),
22817 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
22818 ) );
22819 } else {
22820 // handle different error cases.
22821
22822 }
22823 }
22824
22825 /**
22826 * @author Vova Feldman (@svovaf)
22827 * @since 1.1.2
22828 *
22829 * @param array $params
22830 * @param bool|null $network
22831 *
22832 * @return string
22833 */
22834 function get_activation_url( $params = array(), $network = null ) {
22835 if ( $this->is_addon() && $this->has_free_plan() ) {
22836 /**
22837 * @author Vova Feldman (@svovaf)
22838 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
22839 */
22840 return $this->get_parent_instance()->get_activation_url( $params );
22841 }
22842
22843 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
22844 }
22845
22846 /**
22847 * @author Vova Feldman (@svovaf)
22848 * @since 1.2.1.5
22849 *
22850 * @param array $params
22851 *
22852 * @return string
22853 */
22854 function get_reconnect_url( $params = array() ) {
22855 $params['fs_action'] = 'reset_anonymous_mode';
22856 $params['fs_unique_affix'] = $this->get_unique_affix();
22857
22858 return $this->get_activation_url( $params );
22859 }
22860
22861 /**
22862 * Get the URL of the page that should be loaded after the user connect
22863 * or skip in the opt-in screen.
22864 *
22865 * @author Vova Feldman (@svovaf)
22866 * @since 1.1.3
22867 *
22868 * @param string $filter Filter name.
22869 * @param array $params Since 1.2.2.7
22870 * @param bool|null $network
22871 *
22872 * @return string
22873 */
22874 function get_after_activation_url( $filter, $params = array(), $network = null ) {
22875 if ( $this->show_opt_in_on_themes_page() &&
22876 ( fs_request_has( 'pending_activation' ) ||
22877 // For cases when the first time path is set, even though it's a WP.org theme.
22878 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
22879 ) {
22880 $first_time_path = '';
22881 } else {
22882 $first_time_path = $this->_menu->get_first_time_path(
22883 fs_is_network_admin() && $this->_is_network_active
22884 );
22885 }
22886
22887 if ( $this->_is_network_active &&
22888 fs_is_network_admin() &&
22889 ! $this->_menu->has_network_menu() &&
22890 $this->is_network_registered()
22891 ) {
22892 $target_url = $this->get_account_url();
22893 } else {
22894 // Default plugin's page.
22895 $target_url = $this->_get_admin_page_url( '', array(), $network );
22896 }
22897
22898 return add_query_arg( $params, $this->apply_filters(
22899 $filter,
22900 empty( $first_time_path ) ?
22901 $target_url :
22902 $first_time_path
22903 ) );
22904 }
22905
22906 /**
22907 * Handle account page updates / edits / actions.
22908 *
22909 * @author Vova Feldman (@svovaf)
22910 * @since 1.0.2
22911 *
22912 */
22913 private function _handle_account_edits() {
22914 if ( ! $this->is_user_admin() ) {
22915 return;
22916 }
22917
22918 $action = fs_get_action();
22919
22920 if ( empty( $action ) ) {
22921 return;
22922 }
22923
22924 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
22925 $install_id = fs_request_get( 'install_id', '' );
22926
22927 // Alias.
22928 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
22929
22930 $is_network_action = $this->is_network_level_action();
22931 $blog_id = $this->is_network_level_site_specific_action();
22932 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
22933
22934 if ( is_numeric( $blog_id ) ) {
22935 $this->switch_to_blog( $blog_id );
22936 } else {
22937 $blog_id = '';
22938 }
22939
22940 switch ( $action ) {
22941 case 'opt_in':
22942 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22943
22944 if ( $is_parent_plugin_action ) {
22945 if ( $is_network_action && ! empty( $blog_id ) ) {
22946 if ( ! $this->is_registered() ) {
22947 $this->install_with_user(
22948 $this->get_network_user(),
22949 false,
22950 false,
22951 false,
22952 false
22953 );
22954
22955 $this->_admin_notices->add(
22956 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
22957 $this->get_text_inline( 'Awesome', 'awesome' )
22958 );
22959 }
22960 }
22961 }
22962 break;
22963
22964 case 'toggle_tracking':
22965 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22966
22967 if ( $is_parent_plugin_action ) {
22968 if ( $is_network_action && ! empty( $blog_id ) ) {
22969 if ( $this->is_registered( true ) ) {
22970 if ( $this->is_tracking_prohibited( $blog_id ) ) {
22971 if ( $this->toggle_site_tracking( true, $blog_id ) ) {
22972 $this->_admin_notices->add(
22973 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>" ),
22974 $this->get_text_inline( 'Thank you!', 'thank-you' )
22975 );
22976 }
22977 } else {
22978 if ( $this->toggle_site_tracking( false, $blog_id ) ) {
22979 $install = $this->get_install_by_blog_id( $blog_id );
22980
22981 $this->_admin_notices->add(
22982 sprintf(
22983 $this->get_text_inline( 'Diagnostic data will no longer be sent from %s to %s.', 'opted-out-successfully' ),
22984 self::get_unfiltered_site_url( $blog_id, true ),
22985 "<b>{$this->get_plugin_title()}</b>"
22986 )
22987 );
22988 }
22989 }
22990 }
22991 }
22992 }
22993
22994 break;
22995
22996 case 'delete_account':
22997 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22998
22999 $is_network_deletion = $is_network_action && empty( $blog_id );
23000
23001 if ( $is_parent_plugin_action ) {
23002 // Delete add-on installs if have any.
23003 $installed_addons = $this->get_installed_addons();
23004 foreach ( $installed_addons as $fs_addon ) {
23005 if ( $is_network_deletion ) {
23006 $fs_addon->delete_network_account_event();
23007 } else {
23008 $fs_addon->delete_account_event();
23009 }
23010 }
23011
23012 if ( $is_network_deletion ) {
23013 $this->delete_network_account_event();
23014 } else {
23015 $this->delete_account_event();
23016 }
23017
23018 // Clear user and site.
23019 $this->_site = null;
23020 $this->_user = null;
23021
23022 $this->maybe_set_slug_and_network_menu_exists_flag();
23023
23024 fs_redirect( $this->get_activation_url() );
23025 } else {
23026 if ( $this->is_addon_activated( $plugin_id ) ) {
23027 $fs_addon = self::get_instance_by_id( $plugin_id );
23028
23029 if ( $is_network_deletion ) {
23030 $fs_addon->delete_network_account_event();
23031 } else {
23032 $fs_addon->delete_account_event();
23033 }
23034
23035 fs_redirect( $this->_get_admin_page_url( 'account' ) );
23036 }
23037 }
23038
23039 return;
23040
23041 case 'downgrade_account':
23042 if ( is_numeric( $blog_id ) ) {
23043 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23044 } else {
23045 check_admin_referer( $action );
23046 }
23047
23048 $switch_to_network_install_blog_after_cancellation = (
23049 is_numeric( $blog_id ) &&
23050 $plugin_id == $this->get_id() &&
23051 ! $this->is_trial()
23052 );
23053
23054 $result = $this->cancel_subscription_or_trial( $plugin_id );
23055 if ( $this->is_api_error( $result ) ) {
23056 $this->_admin_notices->add(
23057 $result->error->message,
23058 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23059 'error'
23060 );
23061 }
23062
23063 if ( $switch_to_network_install_blog_after_cancellation ) {
23064 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23065 }
23066
23067 return;
23068
23069 case 'activate_license':
23070 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23071
23072 $fs = $this;
23073 if ( $plugin_id != $this->get_id() ) {
23074 $fs = $this->is_addon_activated( $plugin_id ) ?
23075 self::get_instance_by_id( $plugin_id ) :
23076 null;
23077 }
23078
23079 if ( is_object( $fs ) ) {
23080 $fs->_activate_license();
23081
23082 /**
23083 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
23084 *
23085 * @author Leo Fajardo (@leorw)
23086 * @since 2.4.0
23087 */
23088 unset( $_REQUEST['plugin_id'] );
23089
23090 if ( $this->is_bundle_license_auto_activation_enabled() ) {
23091 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
23092 }
23093 }
23094
23095 return;
23096
23097 case 'deactivate_license':
23098 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23099
23100 if ( $plugin_id == $this->get_id() ) {
23101 $this->_deactivate_license();
23102
23103 if ( $this->is_only_premium() ) {
23104 // Clear user and site.
23105 $this->_site = null;
23106 $this->_user = null;
23107
23108 if ( ! $is_network_action ) {
23109 fs_redirect( $this->get_activation_url() );
23110 } else if ( is_numeric( $blog_id ) ) {
23111 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23112 }
23113 }
23114 } else {
23115 if ( $this->is_addon_activated( $plugin_id ) ) {
23116 $fs_addon = self::get_instance_by_id( $plugin_id );
23117 $fs_addon->_deactivate_license();
23118 }
23119 }
23120
23121 return;
23122
23123 case 'check_updates':
23124 check_admin_referer( $action );
23125 $this->check_updates();
23126
23127 return;
23128
23129 case 'change_owner':
23130 $state = fs_request_get( 'state', 'init' );
23131 switch ( $state ) {
23132 case 'init':
23133 // The nonce is injected by the error handler in `_email_address_update_ajax_handler` function.
23134 check_admin_referer( 'change_owner' );
23135
23136 $candidate_email = fs_request_get( 'candidate_email' );
23137 $transfer_type = fs_request_get( 'transfer_type' );
23138
23139 if ( $this->init_change_owner( $candidate_email, $transfer_type ) ) {
23140 if ( 'transfer' === $transfer_type ) {
23141 $this->_admin_notices->add( sprintf( $this->get_text_inline( 'A confirmation email was just sent to %s. The email owner must confirm the update within the next 4 hours.', 'change-owner-request-sent-x-transfer' ), '<b>' . $this->_user->email . '</b>' ) );
23142 } else {
23143 $this->_admin_notices->add( sprintf( $this->get_text_inline( 'A confirmation email was just sent to %s. You must confirm the update within the next 4 hours. If you cannot find the email, please check your spam folder.', 'change-owner-request-sent-x' ), '<b>' . $this->_user->email . '</b>' ) );
23144 }
23145 }
23146 break;
23147 case 'owner_confirmed':
23148 // We cannot (or need not to) check the nonce and referer here, because the link comes from the email sent by our API.
23149 $candidate_email = fs_request_get( 'candidate_email', '' );
23150
23151 if ( ! is_email($candidate_email ) ) {
23152 return;
23153 }
23154
23155 $this->_admin_notices->add( sprintf( $this->get_text_inline( 'Thanks for confirming the ownership change. An email was just sent to %s for final approval.', 'change-owner-request_owner-confirmed' ), '<b>' . $candidate_email . '</b>' ) );
23156 break;
23157 case 'candidate_confirmed':
23158 // We do not need to validate the authenticity of this request here, because the `complete_change_owner` does that for us through API calls.
23159 if ( $this->complete_change_owner() ) {
23160 $this->_admin_notices->add_sticky(
23161 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
23162 'ownership_changed',
23163 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
23164 );
23165 } else {
23166 // @todo Handle failed ownership change message.
23167 }
23168 break;
23169 }
23170
23171 return;
23172
23173 case 'update_user_name':
23174 check_admin_referer( 'update_user_name' );
23175
23176 $result = $this->update_user_name();
23177
23178 if ( isset( $result->error ) ) {
23179 $this->_admin_notices->add(
23180 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
23181 $oops_text,
23182 'error'
23183 );
23184 } else {
23185 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
23186 }
23187
23188 return;
23189
23190 #region Actions that might be called from external links (e.g. email)
23191
23192 /**
23193 * !!IMPORTANT!!: We cannot check for a valid nonce in this region, because the links could be coming from emails.
23194 */
23195
23196 case 'cancel_trial':
23197 $result = $this->cancel_subscription_or_trial( $plugin_id );
23198 if ( $this->is_api_error( $result ) ) {
23199 $this->_admin_notices->add(
23200 $result->error->message,
23201 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23202 'error'
23203 );
23204 }
23205
23206 return;
23207
23208 case 'verify_email':
23209 $this->verify_email();
23210
23211 return;
23212
23213 case 'sync_user':
23214 $this->_handle_account_user_sync();
23215
23216 return;
23217
23218 case $this->get_unique_affix() . '_sync_license':
23219 $this->_sync_license();
23220
23221 return;
23222
23223 case 'download_latest':
23224 $this->download_latest_directly( $plugin_id );
23225
23226 return;
23227
23228 #endregion
23229 }
23230
23231 if ( WP_FS__IS_POST_REQUEST ) {
23232 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
23233 foreach ( $properties as $p ) {
23234 if ( 'update_' . $p === $action ) {
23235 check_admin_referer( $action );
23236
23237 $this->_logger->log( $action );
23238
23239 $site_property = substr( $p, strlen( 'site_' ) );
23240 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
23241 $this->get_site()->{$site_property} = $site_property_value;
23242
23243 // Store account after modification.
23244 $this->_store_site();
23245
23246 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
23247
23248 $this->_admin_notices->add( sprintf(
23249 /* translators: %s: User's account property (e.g. email address, name) */
23250 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
23251 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
23252 ) );
23253
23254 return;
23255 }
23256 }
23257 }
23258 }
23259
23260 /**
23261 * Adds CSS classes for the body tag in the admin.
23262 *
23263 * @param string $classes Space-separated string of class names.
23264 *
23265 * @return string $classes FS Admin body tag class names.
23266 */
23267 public function fs_addons_body_class( $classes ) {
23268 $classes .= ' plugins-php';
23269 return $classes;
23270 }
23271
23272 /**
23273 * Account page resources load.
23274 *
23275 * @author Vova Feldman (@svovaf)
23276 * @since 1.0.6
23277 */
23278 function _account_page_load() {
23279 $this->_logger->entrance();
23280
23281 $this->_logger->info( var_export( $_REQUEST, true ) );
23282
23283 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
23284
23285 if ( $this->has_addons() ) {
23286 wp_enqueue_script( 'plugin-install' );
23287 add_thickbox();
23288 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23289 }
23290
23291 if ( $this->has_paid_plan() &&
23292 ! $this->has_any_license() &&
23293 ! $this->is_sync_executed() &&
23294 $this->is_tracking_allowed()
23295 ) {
23296 /**
23297 * If no licenses found and no sync job was executed during the last 24 hours,
23298 * just execute the sync job right away (blocking execution).
23299 *
23300 * @since 1.1.7.3
23301 */
23302 $this->run_manual_sync();
23303 }
23304
23305 $this->_handle_account_edits();
23306
23307 if (
23308 is_object( $this->_license ) &&
23309 $this->_license->user_id == $this->_user->id &&
23310 ! $this->is_whitelabeled( true )
23311 ) {
23312 $this->_admin_notices->add(
23313 sprintf(
23314 $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' ),
23315 sprintf(
23316 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
23317 $this->get_text_inline( 'Click here', 'click-here' )
23318 )
23319 ),
23320 '',
23321 'success',
23322 false,
23323 'license_not_whitelabeled'
23324 );
23325 }
23326
23327 $this->do_action( 'account_page_load_before_departure' );
23328 }
23329
23330 /**
23331 * Renders the "Affiliation" page.
23332 *
23333 * @author Leo Fajardo (@leorw)
23334 * @since 1.2.3
23335 */
23336 function _affiliation_page_render() {
23337 $this->_logger->entrance();
23338
23339 $this->fetch_affiliate_and_terms();
23340
23341 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
23342
23343 $is_bundle_context = $this->has_bundle_context();
23344
23345 $plugin_title = $this->get_plugin_title();
23346
23347 if ( $is_bundle_context ) {
23348 $plugin_title = $this->plugin_affiliate_terms->plugin_title;
23349
23350 // Add the suffix "Bundle" only if the word is not present in the title itself.
23351 if ( false === mb_stripos( $plugin_title, fs_text_inline( 'Bundle', 'bundle' ) ) ) {
23352 $plugin_title = $this->apply_filters(
23353 'formatted_bundle_title',
23354 $plugin_title . ' ' . fs_text_inline( 'Bundle', 'bundle' )
23355 );
23356 }
23357 }
23358
23359 $vars = array(
23360 'id' => $this->_module_id,
23361 'plugin_title' => $plugin_title,
23362 );
23363 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
23364 }
23365
23366
23367 /**
23368 * Render account page.
23369 *
23370 * @author Vova Feldman (@svovaf)
23371 * @since 1.0.0
23372 */
23373 function _account_page_render() {
23374 $this->_logger->entrance();
23375
23376 $template = 'account.php';
23377 $vars = array( 'id' => $this->_module_id );
23378
23379 /**
23380 * Added filter to the template to allow developers wrapping the template
23381 * in custom HTML (e.g. within a wizard/tabs).
23382 *
23383 * @author Vova Feldman (@svovaf)
23384 * @since 1.2.1.6
23385 */
23386 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
23387 }
23388
23389 /**
23390 * Render account connect page.
23391 *
23392 * @author Vova Feldman (@svovaf)
23393 * @since 1.0.7
23394 */
23395 function _connect_page_render() {
23396 $this->_logger->entrance();
23397
23398 $vars = array( 'id' => $this->_module_id );
23399
23400 /**
23401 * Added filter to the template to allow developers wrapping the template
23402 * in custom HTML (e.g. within a wizard/tabs).
23403 *
23404 * @author Vova Feldman (@svovaf)
23405 * @since 1.2.1.6
23406 */
23407 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
23408 }
23409
23410 /**
23411 * Load required resources before add-ons page render.
23412 *
23413 * @author Vova Feldman (@svovaf)
23414 * @since 1.0.6
23415 */
23416 function _addons_page_load() {
23417 $this->_logger->entrance();
23418
23419 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
23420
23421 wp_enqueue_script( 'plugin-install' );
23422 add_thickbox();
23423 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23424
23425 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
23426 $this->_admin_notices->add(
23427 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>' ),
23428 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
23429 'update-nag'
23430 );
23431 }
23432 }
23433
23434 /**
23435 * Render add-ons page.
23436 *
23437 * @author Vova Feldman (@svovaf)
23438 * @since 1.0.6
23439 */
23440 function _addons_page_render() {
23441 $this->_logger->entrance();
23442
23443 $vars = array( 'id' => $this->_module_id );
23444
23445 /**
23446 * Added filter to the template to allow developers wrapping the template
23447 * in custom HTML (e.g. within a wizard/tabs).
23448 *
23449 * @author Vova Feldman (@svovaf)
23450 * @since 1.2.1.6
23451 */
23452 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
23453 }
23454
23455 /* Pricing & Upgrade
23456 ------------------------------------------------------------------------------------------------------------------*/
23457 /**
23458 * Render pricing page.
23459 *
23460 * @author Vova Feldman (@svovaf)
23461 * @since 1.0.0
23462 */
23463 function _pricing_page_render() {
23464 $this->_logger->entrance();
23465
23466 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
23467 fs_enqueue_local_style( 'fs_checkout', '/admin/checkout.css' );
23468
23469 $vars = array( 'id' => $this->_module_id );
23470
23471 if ( 'true' === fs_request_get( 'checkout', false ) ) {
23472 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
23473 } else {
23474 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
23475 }
23476 }
23477
23478 /**
23479 * @author Leo Fajardo (@leorw)
23480 * @since 2.3.1
23481 */
23482 function _add_pricing_ajax_handler() {
23483 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
23484 }
23485
23486 /**
23487 * @author Leo Fajardo (@leorw)
23488 * @since 2.3.1
23489 */
23490 function _fs_pricing_ajax_action_handler() {
23491 $this->check_ajax_referer( 'pricing_ajax_action' );
23492
23493 $result = null;
23494 $pricing_action = fs_request_get( 'pricing_action' );
23495
23496 switch ( $pricing_action ) {
23497 case 'fetch_pricing_data':
23498 $params = array(
23499 'is_enriched' => true,
23500 'trial' => fs_request_get_bool( 'trial' ),
23501 'sandbox' => fs_request_get_raw( 'sandbox' ),
23502 's_ctx_type' => fs_request_get_raw( 's_ctx_type' ),
23503 's_ctx_id' => fs_request_get_raw( 's_ctx_id' ),
23504 's_ctx_ts' => fs_request_get_raw( 's_ctx_ts' ),
23505 's_ctx_secure' => fs_request_get_raw( 's_ctx_secure' ),
23506 );
23507
23508 $bundle_id = $this->get_bundle_id();
23509 $bundle_public_key = $this->get_bundle_public_key();
23510
23511 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
23512
23513 if ( ! $has_bundle_context ) {
23514 $api = $this->get_api_plugin_scope();
23515 } else {
23516 $api = FS_Api::instance(
23517 $bundle_id,
23518 'plugin',
23519 $bundle_id,
23520 $bundle_public_key,
23521 ! $this->is_live(),
23522 false,
23523 $this->get_sdk_version()
23524 );
23525
23526 $params['plugin_id'] = $this->get_id();
23527 $params['plugin_public_key'] = $this->get_public_key();
23528 }
23529
23530 $result = $api->get( $this->add_show_pending( 'pricing.json?' . http_build_query( $params ) ) );
23531 break;
23532 case 'start_trial':
23533 $trial_plan_id = fs_request_get( 'plan_id' );
23534
23535 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
23536 $plan = $this->_get_plan_by_id( $trial_plan_id );
23537
23538 if ( ! $plan ) {
23539 $this->shoot_ajax_failure( 'Invalid plan ID.' );
23540 return;
23541 }
23542
23543 $result = $this->start_trial( $plan->name, true );
23544 } else {
23545 // @todo - This fails for sandbox trial at the moment if the trial was already utilized.
23546 $result = $this->opt_in(
23547 false,
23548 false,
23549 false,
23550 false,
23551 false,
23552 $trial_plan_id
23553 );
23554 }
23555 }
23556
23557 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
23558 $this->_logger->api_error( $result );
23559
23560 self::shoot_ajax_failure(
23561 isset( $result->error ) ?
23562 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
23563 var_export( $result, true )
23564 );
23565 }
23566
23567 $this->shoot_ajax_success( $result );
23568 }
23569
23570 #----------------------------------------------------------------------------------
23571 #region Contact Us
23572 #----------------------------------------------------------------------------------
23573
23574 /**
23575 * Render contact-us page.
23576 *
23577 * @author Vova Feldman (@svovaf)
23578 * @since 1.0.3
23579 */
23580 function _contact_page_render() {
23581 $this->_logger->entrance();
23582
23583 $vars = array( 'id' => $this->_module_id );
23584
23585 /**
23586 * Added filter to the template to allow developers wrapping the template
23587 * in custom HTML (e.g. within a wizard/tabs).
23588 *
23589 * @author Vova Feldman (@svovaf)
23590 * @since 2.1.3
23591 */
23592 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
23593 }
23594
23595 #endregion ------------------------------------------------------------------------
23596
23597 /**
23598 * Hide all admin notices to prevent distractions.
23599 *
23600 * @author Vova Feldman (@svovaf)
23601 * @since 1.0.3
23602 *
23603 * @uses remove_all_actions()
23604 */
23605 private static function _hide_admin_notices() {
23606 remove_all_actions( 'admin_notices' );
23607 remove_all_actions( 'network_admin_notices' );
23608 remove_all_actions( 'all_admin_notices' );
23609 remove_all_actions( 'user_admin_notices' );
23610 }
23611
23612 static function _clean_admin_content_section_hook() {
23613 $hide_admin_notices = true;
23614
23615 if ( fs_request_is_action( 'allow_clone_resolution_notice' ) ) {
23616 check_admin_referer( 'fs_allow_clone_resolution_notice' );
23617
23618 $hide_admin_notices = false;
23619 }
23620
23621 if ( $hide_admin_notices ) {
23622 self::_hide_admin_notices();
23623 }
23624
23625 // Hide footer.
23626 echo '<style>#wpfooter { display: none !important; }</style>';
23627 }
23628
23629 /**
23630 * Attach to admin_head hook to hide all admin notices.
23631 *
23632 * @author Vova Feldman (@svovaf)
23633 * @since 1.0.3
23634 */
23635 static function _clean_admin_content_section() {
23636 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
23637 }
23638
23639 /* CSS & JavaScript
23640 ------------------------------------------------------------------------------------------------------------------*/
23641 /* function _enqueue_script($handle, $src) {
23642 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
23643
23644 $this->_logger->entrance( 'script = ' . $url );
23645
23646 wp_enqueue_script( $handle, $url );
23647 }*/
23648
23649 /* SDK
23650 ------------------------------------------------------------------------------------------------------------------*/
23651 private $_user_api;
23652
23653 /**
23654 *
23655 * @author Vova Feldman (@svovaf)
23656 * @since 1.0.2
23657 *
23658 * @param bool $flush
23659 *
23660 * @return FS_Api
23661 */
23662 function get_api_user_scope( $flush = false ) {
23663 if ( ! isset( $this->_user_api ) || $flush ) {
23664 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
23665 }
23666
23667 return $this->_user_api;
23668 }
23669
23670 /**
23671 * @author Vova Feldman (@svovaf)
23672 * @since 2.0.0
23673 *
23674 * @param \FS_User $user
23675 *
23676 * @return \FS_Api
23677 */
23678 private function get_api_user_scope_by_user( FS_User $user ) {
23679 return FS_Api::instance(
23680 $this->_module_id,
23681 'user',
23682 $user->id,
23683 $user->public_key,
23684 ! $this->is_live(),
23685 $user->secret_key,
23686 $this->get_sdk_version()
23687 );
23688 }
23689
23690 /**
23691 *
23692 * @author Leo Fajardo (@leorw)
23693 * @since 2.0.0
23694 *
23695 * @param bool $flush
23696 *
23697 * @return FS_Api
23698 */
23699 private function get_current_or_network_user_api_scope( $flush = false ) {
23700 if ( ! $this->_is_network_active ||
23701 ( isset( $this->_user ) && $this->_user instanceof FS_User )
23702 ) {
23703 return $this->get_api_user_scope( $flush );
23704 }
23705
23706 $user = $this->get_current_or_network_user();
23707
23708 $this->_user_api = FS_Api::instance(
23709 $this->_module_id,
23710 'user',
23711 $user->id,
23712 $user->public_key,
23713 ! $this->is_live(),
23714 $user->secret_key,
23715 $this->get_sdk_version()
23716 );
23717
23718 return $this->_user_api;
23719 }
23720
23721 private $_site_api;
23722
23723 /**
23724 *
23725 * @author Vova Feldman (@svovaf)
23726 * @since 1.0.2
23727 *
23728 * @param bool $flush
23729 *
23730 * @return FS_Api
23731 */
23732 private function get_api_site_scope( $flush = false ) {
23733 if ( ! isset( $this->_site_api ) || $flush ) {
23734 $this->_site_api = FS_Api::instance(
23735 $this->_module_id,
23736 'install',
23737 $this->_site->id,
23738 $this->_site->public_key,
23739 ! $this->is_live(),
23740 $this->_site->secret_key,
23741 $this->get_sdk_version(),
23742 self::get_unfiltered_site_url()
23743 );
23744 }
23745
23746 return $this->_site_api;
23747 }
23748
23749 /**
23750 * @author Leo Fajardo (@leorw)
23751 * @since 2.5.0
23752 *
23753 * @param string $path
23754 * @param string $method
23755 * @param array $params
23756 * @param bool $flush_instance
23757 *
23758 * @return array|mixed|string|void
23759 * @throws Freemius_Exception
23760 */
23761 private function api_site_call( $path, $method = 'GET', $params = array(), $flush_instance = false ) {
23762 $result = $this->get_api_site_scope( $flush_instance )->call( $path, $method, $params );
23763
23764 /**
23765 * 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.
23766 *
23767 * @author Leo Fajardo (@leorw)
23768 * @since 2.5.0
23769 */
23770 if (
23771 $this->is_registered() &&
23772 FS_Api::is_api_result_entity( $result ) &&
23773 isset( $result->url )
23774 ) {
23775 $stored_local_url = trailingslashit( $this->_site->url );
23776 $stored_remote_url = trailingslashit( $result->url );
23777
23778 if ( $stored_local_url !== $stored_remote_url ) {
23779 $this->_site->url = $result->url;
23780 $this->_store_site();
23781 }
23782
23783 if ( fs_strip_url_protocol( $stored_remote_url ) !== self::get_unfiltered_site_url( null, true, true ) ) {
23784 FS_Clone_Manager::instance()->maybe_run_clone_resolution();
23785 }
23786 }
23787
23788 return $result;
23789 }
23790
23791 private $_plugin_api;
23792
23793 /**
23794 * Get plugin public API scope.
23795 *
23796 * @author Vova Feldman (@svovaf)
23797 * @since 1.0.7
23798 *
23799 * @return FS_Api
23800 */
23801 function get_api_plugin_scope() {
23802 if ( ! isset( $this->_plugin_api ) ) {
23803 $this->_plugin_api = FS_Api::instance(
23804 $this->_module_id,
23805 'plugin',
23806 $this->_plugin->id,
23807 $this->_plugin->public_key,
23808 ! $this->is_live(),
23809 false,
23810 $this->get_sdk_version()
23811 );
23812 }
23813
23814 return $this->_plugin_api;
23815 }
23816
23817 /**
23818 * Get bundle public API scope.
23819 *
23820 * @author Vova Feldman (@svovaf)
23821 * @since 2.3.1
23822 *
23823 * @return FS_Api
23824 */
23825 function get_api_bundle_scope() {
23826 return FS_Api::instance(
23827 $this->get_bundle_id(),
23828 'plugin',
23829 $this->get_bundle_id(),
23830 $this->get_bundle_public_key(),
23831 ! $this->is_live(),
23832 false,
23833 $this->get_sdk_version()
23834 );
23835 }
23836
23837 /**
23838 * Get site API scope object (fallback to public plugin scope when not registered).
23839 *
23840 * @author Vova Feldman (@svovaf)
23841 * @since 1.0.7
23842 *
23843 * @return FS_Api
23844 */
23845 function get_api_site_or_plugin_scope() {
23846 return $this->is_registered() ?
23847 $this->get_api_site_scope() :
23848 $this->get_api_plugin_scope();
23849 }
23850
23851 /**
23852 * @author Leo Fajardo (@leorw)
23853 * @since 2.2.3.1
23854 *
23855 * @param object $result
23856 */
23857 private function maybe_modify_api_curl_error_message( $result ) {
23858 if (
23859 'cUrlMissing' !== $result->error->type &&
23860 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
23861 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
23862 ) {
23863 return;
23864 }
23865
23866 $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' ) .
23867 ' ' .
23868 $this->esc_html_inline(
23869 sprintf(
23870 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
23871 implode(
23872 ', ',
23873 $this->apply_filters( 'api_domains', array(
23874 'api.freemius.com',
23875 'wp.freemius.com'
23876 ) )
23877 )
23878 ),
23879 'connectivity-whitelist'
23880 ) .
23881 ' ' .
23882 sprintf(
23883 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
23884 $this->get_module_type()
23885 );
23886 }
23887
23888 /**
23889 * Show trial promotional notice (if any trial exist).
23890 *
23891 * @author Vova Feldman (@svovaf)
23892 * @since 1.0.9
23893 *
23894 * @param FS_Plugin_Plan[] $plans
23895 */
23896 function _check_for_trial_plans( $plans ) {
23897 /**
23898 * 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.
23899 *
23900 * @author Vova Feldman (@svovaf)
23901 * @since 2.1.2
23902 */
23903 if ( ! is_array( $plans ) && is_object( $plans ) ) {
23904 $plans = array( $plans );
23905 }
23906
23907 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
23908 $plans = array();
23909 }
23910
23911 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
23912 }
23913
23914 /**
23915 * During trial promotion the "upgrade" submenu item turns to
23916 * "start trial" to encourage the trial. Since we want to keep
23917 * the same menu item handler and there's no robust way to
23918 * add new arguments to the menu item link's querystring,
23919 * use JavaScript to find the menu item and update the href of
23920 * the link.
23921 *
23922 * @author Vova Feldman (@svovaf)
23923 * @since 1.2.1.5
23924 */
23925 function _fix_start_trial_menu_item_url() {
23926 $template_args = array( 'id' => $this->_module_id );
23927 fs_require_template( 'add-trial-to-pricing.php', $template_args );
23928 }
23929
23930 /**
23931 * Check if module is currently in a trial promotion mode.
23932 *
23933 * @author Vova Feldman (@svovaf)
23934 * @since 1.2.2.7
23935 *
23936 * @return bool
23937 */
23938 function is_in_trial_promotion() {
23939 return $this->_admin_notices->has_sticky( 'trial_promotion' );
23940 }
23941
23942 /**
23943 * Show trial promotional notice (if any trial exist).
23944 *
23945 * @author Vova Feldman (@svovaf)
23946 * @since 1.0.9
23947 *
23948 * @return bool If trial notice added.
23949 */
23950 function _add_trial_notice() {
23951 if ( ! $this->is_user_admin() ) {
23952 return false;
23953 }
23954
23955 if ( ! $this->is_user_in_admin() ) {
23956 return false;
23957 }
23958
23959 if ( $this->_is_network_active ) {
23960 if ( fs_is_network_admin() ) {
23961 // Network level trial is disabled at the moment.
23962 return false;
23963 }
23964
23965 if ( ! $this->is_delegated_connection() ) {
23966 // Only delegated sites should support trials.
23967 return false;
23968 }
23969 }
23970
23971 // Check if trial message is already shown.
23972 if ( $this->is_in_trial_promotion() ) {
23973 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
23974
23975 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
23976
23977 return false;
23978 }
23979
23980 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
23981 // Don't show trial if running the premium code, unless running in DEV mode.
23982 return false;
23983 }
23984
23985 if ( ! $this->has_trial_plan() ) {
23986 // No plans with trial.
23987 return false;
23988 }
23989
23990 if ( ! $this->apply_filters( 'show_trial', true ) ) {
23991 // Developer explicitly asked not to show the trial promo.
23992 return false;
23993 }
23994
23995 if ( $this->is_registered() ) {
23996 // Check if trial already utilized.
23997 if ( $this->_site->is_trial_utilized() ) {
23998 return false;
23999 }
24000
24001 if ( $this->is_paying_or_trial() ) {
24002 // Don't show trial if paying or already in trial.
24003 return false;
24004 }
24005 }
24006
24007 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24008 // If not yet opted-in/skipped, or pending activation, don't show trial.
24009 return false;
24010 }
24011
24012 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
24013 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
24014
24015 // Show promotion if never shown before and 24 hours after initial activation with FS.
24016 if ( ! $was_promotion_shown_before &&
24017 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
24018 ) {
24019 return false;
24020 }
24021
24022 // OR if promotion was shown before, try showing it every 30 days.
24023 if ( $was_promotion_shown_before &&
24024 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
24025 ) {
24026 return false;
24027 }
24028
24029 $trial_period = $this->_trial_days;
24030 $require_payment = $this->_is_trial_require_payment;
24031 $trial_url = $this->get_trial_url();
24032 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
24033
24034 if ( $this->is_registered() ) {
24035 // If opted-in, override trial with up to date data from API.
24036 $trial_plans = FS_Plan_Manager::instance()->get_visible_trial_plans( $this->_plans );
24037 $trial_plans_count = count( $trial_plans );
24038
24039 if ( 0 === $trial_plans_count ) {
24040 // If there's no plans with a trial just exit.
24041 return false;
24042 }
24043
24044 /**
24045 * @var FS_Plugin_Plan $paid_plan
24046 */
24047 $paid_plan = $trial_plans[0];
24048 $require_payment = $paid_plan->is_require_subscription;
24049 $trial_period = $paid_plan->trial_period;
24050
24051 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
24052
24053 if ( $total_paid_plans !== $trial_plans_count ) {
24054 // Not all paid plans have a trial - generate a string of those that have it.
24055 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
24056 $plans_string .= sprintf(
24057 ' <a href="%s">%s</a>',
24058 $trial_url,
24059 $trial_plans[ $i ]->title
24060 );
24061
24062 if ( $i < $trial_plans_count - 2 ) {
24063 $plans_string .= ', ';
24064 } else if ( $i == $trial_plans_count - 2 ) {
24065 $plans_string .= ' and ';
24066 }
24067 }
24068 }
24069 }
24070
24071 $message = sprintf(
24072 $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' ),
24073 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
24074 $plans_string,
24075 $trial_period
24076 );
24077
24078 // "No Credit-Card Required" or "No Commitment for N Days".
24079 $cc_string = $require_payment ?
24080 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
24081 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
24082
24083
24084 // Start trial button.
24085 $button = ' ' . sprintf(
24086 '<div><a class="button button-primary" href="%s">%s &nbsp;&#10140;</a></div>',
24087 $trial_url,
24088 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
24089 );
24090
24091 $message_text = $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string}" );
24092
24093 $this->_admin_notices->add_sticky(
24094 "<div class=\"fs-trial-message-container\"><div>{$message_text}</div> {$button}</div>",
24095 'trial_promotion',
24096 '',
24097 'promotion'
24098 );
24099
24100 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
24101
24102 return true;
24103 }
24104
24105 /**
24106 * Lets users/customers know that the product has an affiliate program.
24107 *
24108 * @author Leo Fajardo (@leorw)
24109 * @since 1.2.2.11
24110 *
24111 * @return bool Returns true if the notice has been added.
24112 */
24113 function _add_affiliate_program_notice() {
24114 if ( ! $this->is_user_admin() ) {
24115 return false;
24116 }
24117
24118 if ( ! $this->is_user_in_admin() ) {
24119 return false;
24120 }
24121
24122 // Check if the notice is already shown.
24123 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
24124 return false;
24125 }
24126
24127 if (
24128 // Product has no affiliate program.
24129 ! $this->has_affiliate_program() ||
24130 // User has applied for an affiliate account.
24131 ! empty( $this->_storage->affiliate_application_data )
24132 ) {
24133 return false;
24134 }
24135
24136 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
24137 // Developer explicitly asked not to show the notice about the affiliate program.
24138 return false;
24139 }
24140
24141 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24142 // If not yet opted in/skipped, or pending activation, don't show the notice.
24143 return false;
24144 }
24145
24146 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
24147 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
24148
24149 /**
24150 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
24151 * activation with FS.
24152 */
24153 if ( $was_notice_shown_before ||
24154 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
24155 ) {
24156 return false;
24157 }
24158
24159 if ( ! $this->is_paying() &&
24160 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
24161 ) {
24162 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
24163 return false;
24164 }
24165
24166 $message = sprintf(
24167 $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' ),
24168 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
24169 $this->get_module_label( true )
24170 );
24171
24172 // HTML code for the "Learn more..." button.
24173 $button = ' ' . sprintf(
24174 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24175 $this->_get_admin_page_url( 'affiliation' ),
24176 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
24177 );
24178
24179 $this->_admin_notices->add_sticky(
24180 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
24181 'affiliate_program',
24182 '',
24183 'promotion'
24184 );
24185
24186 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
24187
24188 return true;
24189 }
24190
24191 /**
24192 * @author Vova Feldman (@svovaf)
24193 * @since 1.2.1.5
24194 */
24195 function _enqueue_common_css() {
24196 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
24197 // Add basic CSS for admin-notices and menu-item colors.
24198 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
24199 }
24200 }
24201
24202 /**
24203 * @author Leo Fajardo (@leorw)
24204 * @since 1.2.2
24205 */
24206 function _show_theme_activation_optin_dialog() {
24207 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
24208
24209 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
24210 }
24211
24212 /**
24213 * @author Leo Fajardo (@leorw)
24214 * @since 1.2.2
24215 */
24216 function _add_fs_theme_activation_dialog() {
24217 global $pagenow;
24218
24219 if ( 'themes.php' !== $pagenow ) {
24220 return;
24221 }
24222
24223 $vars = array( 'id' => $this->_module_id );
24224 fs_require_once_template( 'connect.php', $vars );
24225 }
24226
24227 /* Action Links
24228 ------------------------------------------------------------------------------------------------------------------*/
24229 private $_action_links_hooked = false;
24230 private $_action_links = array();
24231
24232 /**
24233 * Hook to plugin action links filter.
24234 *
24235 * @author Vova Feldman (@svovaf)
24236 * @since 1.0.0
24237 */
24238 private function hook_plugin_action_links() {
24239 $this->_logger->entrance();
24240
24241 $this->_action_links_hooked = true;
24242
24243 $this->_logger->log( 'Adding action links hooks.' );
24244
24245 // Add action link to settings page.
24246 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
24247 &$this,
24248 '_modify_plugin_action_links_hook'
24249 ), WP_FS__DEFAULT_PRIORITY, 2 );
24250 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
24251 &$this,
24252 '_modify_plugin_action_links_hook'
24253 ), WP_FS__DEFAULT_PRIORITY, 2 );
24254 }
24255
24256 /**
24257 * Add plugin action link.
24258 *
24259 * @author Vova Feldman (@svovaf)
24260 * @since 1.0.0
24261 *
24262 * @param $label
24263 * @param $url
24264 * @param bool $external
24265 * @param int $priority
24266 * @param bool $key
24267 */
24268 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
24269 $this->_logger->entrance();
24270
24271 if ( ! isset( $this->_action_links[ $priority ] ) ) {
24272 $this->_action_links[ $priority ] = array();
24273 }
24274
24275 if ( false === $key ) {
24276 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
24277 }
24278
24279 $this->_action_links[ $priority ][] = array(
24280 'label' => $label,
24281 'href' => $url,
24282 'key' => $key,
24283 'external' => $external
24284 );
24285 }
24286
24287 /**
24288 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
24289 *
24290 * @author Vova Feldman (@svovaf)
24291 * @since 1.0.0
24292 */
24293 function _add_upgrade_action_link() {
24294 $this->_logger->entrance();
24295
24296 $is_activation_mode = $this->is_activation_mode();
24297
24298 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
24299
24300 /**
24301 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
24302 * and "Add-Ons" menus should be added.
24303 *
24304 * @author Leo Fajardo (@leorw)
24305 * @since 2.3.0
24306 */
24307 $add_upgrade_link = (
24308 $add_action_links ||
24309 ( $is_activation_mode && $this->is_only_premium() )
24310 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
24311
24312 $add_addons_link = ( $add_action_links && $this->has_addons() );
24313
24314 if ( ! $add_upgrade_link && ! $add_addons_link ) {
24315 return;
24316 }
24317
24318 if (
24319 $add_upgrade_link &&
24320 $this->is_pricing_page_visible() &&
24321 $this->is_submenu_item_visible( 'pricing' )
24322 ) {
24323 $this->add_plugin_action_link(
24324 $this->get_text_inline( 'Upgrade', 'upgrade' ),
24325 $this->get_upgrade_url(),
24326 false,
24327 7,
24328 'upgrade'
24329 );
24330 }
24331
24332 if (
24333 $add_addons_link &&
24334 $this->has_addons() &&
24335 $this->is_submenu_item_visible( 'addons' )
24336 ) {
24337 $this->add_plugin_action_link(
24338 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
24339 $this->_get_admin_page_url( 'addons' ),
24340 false,
24341 9,
24342 'addons'
24343 );
24344 }
24345 }
24346
24347 /**
24348 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
24349 *
24350 * @author Leo Fajardo (@leorw)
24351 * @since 1.1.9
24352 */
24353 function _add_license_action_link() {
24354 $this->_logger->entrance();
24355
24356 if ( ! self::is_ajax() ) {
24357 // Inject license activation dialog UI and client side code.
24358 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
24359 }
24360
24361 $link_text = $this->is_free_plan() ?
24362 $this->get_text_inline( 'Activate License', 'activate-license' ) :
24363 $this->get_text_inline( 'Change License', 'change-license' );
24364
24365 $this->add_plugin_action_link(
24366 $link_text,
24367 '#',
24368 false,
24369 11,
24370 ( 'activate-license ' . $this->get_unique_affix() )
24371 );
24372 }
24373
24374 /**
24375 * @author Leo Fajardo (@leorw)
24376 * @since 2.0.2
24377 */
24378 function _add_premium_version_upgrade_selection_action() {
24379 $this->_logger->entrance();
24380
24381 if ( ! self::is_ajax() ) {
24382 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
24383 }
24384 }
24385
24386 /**
24387 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
24388 *
24389 * @author Leo Fajardo (@leorw)
24390 * @since 1.2.1.5
24391 */
24392 function _add_tracking_links() {
24393 if ( ! current_user_can( 'manage_options' ) ) {
24394 return;
24395 }
24396
24397 $this->_logger->entrance();
24398
24399 if ( $this->is_only_premium() && $this->is_free_plan() ) {
24400 // 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.
24401 return;
24402 }
24403
24404 if (
24405 $this->is_addon() &&
24406 ! $this->is_only_premium()
24407 ) {
24408 $parent = $this->get_parent_instance();
24409
24410 if ( is_object( $parent ) && $parent->is_anonymous() ) {
24411 return;
24412 }
24413 }
24414
24415 if ( fs_is_network_admin() ) {
24416 if ( ! $this->_is_network_active ) {
24417 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
24418 return;
24419 } else if ( $this->is_network_delegated_connection() ) {
24420 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
24421 return;
24422 }
24423 } else {
24424 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
24425 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
24426 return;
24427 }
24428 }
24429
24430 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
24431 if ( ! $this->is_registered() && $this->is_anonymous() ) {
24432 $this->connect_again();
24433
24434 return;
24435 }
24436 }
24437
24438 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
24439 ( $this->is_theme() && ! self::is_themes_page() )
24440 ) {
24441 // Only show tracking links on the plugins and themes pages.
24442 return;
24443 }
24444
24445 if (
24446 $this->is_activation_mode() &&
24447 $this->is_premium() &&
24448 ! $this->is_registered()
24449 ) {
24450 // If not yet registered and running the premium code base, a license activation link will already be shown.
24451 return;
24452 }
24453
24454 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24455 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
24456 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
24457 return;
24458 }
24459 }
24460
24461 if ( $this->add_ajax_action( 'toggle_permission_tracking', array( &$this, '_toggle_permission_tracking_callback' ) ) ) {
24462 return;
24463 }
24464
24465 $link_text_id = '';
24466 $url = '#';
24467
24468 if ( $this->is_registered( true ) ) {
24469 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24470 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
24471 } else {
24472 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24473 }
24474 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
24475 /**
24476 * Show opt-in link only if skipped or in activation mode.
24477 */
24478 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24479
24480 $params = ! $this->is_anonymous() ?
24481 array() :
24482 array(
24483 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
24484 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
24485 );
24486
24487 $url = $this->get_activation_url( $params );
24488 }
24489
24490 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
24491
24492 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
24493 $this->add_plugin_action_link(
24494 $link_text_id,
24495 $url,
24496 false,
24497 13,
24498 "opt-in-or-opt-out {$this->_slug}"
24499 );
24500 }
24501 }
24502
24503 /**
24504 * Get the URL of the page that should be loaded right after the plugin activation.
24505 *
24506 * @author Vova Feldman (@svovaf)
24507 * @since 1.1.7.4
24508 *
24509 * @return string
24510 */
24511 function get_after_plugin_activation_redirect_url() {
24512 $url = false;
24513
24514 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
24515 $first_time_path = $this->_menu->get_first_time_path(
24516 fs_is_network_admin() && $this->_is_network_active
24517 );
24518
24519 if ( $this->is_activation_mode() ) {
24520 $url = $this->get_activation_url();
24521 } else if ( ! empty( $first_time_path ) ) {
24522 $url = $first_time_path;
24523 } else {
24524 $page = '';
24525 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
24526 if ( $this->is_network_registered() ) {
24527 $page = 'account';
24528 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
24529 $this->maybe_set_slug_and_network_menu_exists_flag();
24530 }
24531 }
24532
24533 $url = $this->_get_admin_page_url( $page );
24534 }
24535 } else {
24536 $plugin_fs = false;
24537
24538 if ( $this->is_parent_plugin_installed() ) {
24539 $plugin_fs = self::get_parent_instance();
24540 }
24541
24542 if ( is_object( $plugin_fs ) ) {
24543 if ( ! $plugin_fs->is_registered() ) {
24544 // Forward to parent plugin connect when parent not registered.
24545 $url = $plugin_fs->get_activation_url();
24546 } else {
24547 // Forward to account page.
24548 $url = $plugin_fs->_get_admin_page_url( 'account' );
24549 }
24550 }
24551 }
24552
24553 return $url;
24554 }
24555
24556 /**
24557 * Forward page to activation page.
24558 *
24559 * @author Vova Feldman (@svovaf)
24560 * @since 1.0.3
24561 */
24562 function _redirect_on_activation_hook() {
24563 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
24564 $url = $this->get_after_plugin_activation_redirect_url();
24565
24566 if ( is_string( $url ) ) {
24567 fs_redirect( $url );
24568 }
24569 }
24570 }
24571
24572 /**
24573 * Modify plugin's page action links collection.
24574 *
24575 * @author Vova Feldman (@svovaf)
24576 * @since 1.0.0
24577 *
24578 * @param array $links
24579 * @param $file
24580 *
24581 * @return array
24582 */
24583 function _modify_plugin_action_links_hook( $links, $file ) {
24584 $this->_logger->entrance();
24585
24586 $passed_deactivate = false;
24587 $deactivate_link = '';
24588 $before_deactivate = array();
24589 $after_deactivate = array();
24590 foreach ( $links as $key => $link ) {
24591 if ( 'deactivate' === $key ) {
24592 $deactivate_link = $link;
24593 $passed_deactivate = true;
24594 continue;
24595 }
24596
24597 if ( ! $passed_deactivate ) {
24598 $before_deactivate[ $key ] = $link;
24599 } else {
24600 $after_deactivate[ $key ] = $link;
24601 }
24602 }
24603
24604 ksort( $this->_action_links );
24605
24606 foreach ( $this->_action_links as $new_links ) {
24607 foreach ( $new_links as $link ) {
24608 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
24609 }
24610 }
24611
24612 if ( ! empty( $deactivate_link ) ) {
24613 /**
24614 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
24615 *
24616 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
24617 */
24618 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
24619
24620 // Append deactivation link.
24621 $before_deactivate['deactivate'] = $deactivate_link;
24622 }
24623
24624 return array_merge( $before_deactivate, $after_deactivate );
24625 }
24626
24627 /**
24628 * Adds admin message.
24629 *
24630 * @author Vova Feldman (@svovaf)
24631 * @since 1.0.4
24632 *
24633 * @param string $message
24634 * @param string $title
24635 * @param string $type
24636 */
24637 function add_admin_message( $message, $title = '', $type = 'success' ) {
24638 $this->_admin_notices->add( $message, $title, $type );
24639 }
24640
24641 /**
24642 * Adds sticky admin message.
24643 *
24644 * @author Vova Feldman (@svovaf)
24645 * @since 1.1.0
24646 *
24647 * @param string $message
24648 * @param string $id
24649 * @param string $title
24650 * @param string $type
24651 */
24652 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
24653 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
24654 }
24655
24656 /**
24657 * Check if the paid version of the module is installed.
24658 *
24659 * @author Vova Feldman (@svovaf)
24660 * @since 2.2.0
24661 *
24662 * @return bool
24663 */
24664 private function is_premium_version_installed() {
24665 $premium_plugin_basename = $this->premium_plugin_basename();
24666
24667 if ( $this->is_theme() ) {
24668 return $this->can_activate_theme( $this->get_premium_slug() );
24669 }
24670
24671 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_plugin_basename ) );
24672 }
24673
24674 /**
24675 * Helper function that returns the final steps for the upgrade completion.
24676 *
24677 * If the module is already running the premium code, returns an empty string.
24678 *
24679 * @author Vova Feldman (@svovaf)
24680 * @since 1.2.1
24681 *
24682 * @param string $plan_title
24683 *
24684 * @return string
24685 */
24686 private function get_complete_upgrade_instructions( $plan_title = '' ) {
24687 $this->_logger->entrance();
24688
24689 $activate_license_string = $this->get_license_network_activation_notice();
24690
24691 if ( ! $this->has_premium_version() || $this->is_premium() ) {
24692 return '' . $activate_license_string;
24693 }
24694
24695 if ( empty( $plan_title ) ) {
24696 $plan_title = $this->get_plan_title();
24697 }
24698
24699 if ( $this->is_premium_version_installed() ) {
24700 /**
24701 * If the premium version is already installed, instead of showing the installation instructions,
24702 * tell the current user to activate it.
24703 *
24704 * @author Leo Fajardo (@leorw)
24705 * @since 2.2.1
24706 */
24707 $premium_theme_slug_or_plugin_basename = $this->is_theme() ?
24708 $this->get_premium_slug() :
24709 $this->premium_plugin_basename();
24710
24711 if ( is_admin() ) {
24712 return sprintf(
24713 /* translators: %1$s: Product title; %2$s: Plan title */
24714 $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' ),
24715 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24716 $plan_title,
24717 sprintf(
24718 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
24719 ( $this->is_theme() ?
24720 wp_nonce_url( 'themes.php?action=activate&amp;stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) :
24721 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ),
24722 esc_html( sprintf(
24723 /* translators: %s: Plan title */
24724 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
24725 $plan_title
24726 ) )
24727 )
24728 );
24729 } else {
24730 return sprintf(
24731 /* translators: %1$s: Product title; %3$s: Plan title */
24732 $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' ),
24733 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24734 sprintf(
24735 '<a href="%s">%s</a>',
24736 admin_url( $this->is_theme() ? 'themes.php' : 'plugins.php' ),
24737 ( $this->is_theme() ?
24738 $this->get_text_inline( 'Themes page', 'themes-page' ) :
24739 $this->get_text_inline( 'Plugins page', 'plugins-page' ) )
24740 ),
24741 $plan_title
24742 );
24743 }
24744 } else {
24745 // @since 1.2.1.5 The free version is auto deactivated.
24746 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
24747 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
24748 '';
24749
24750 return sprintf(
24751 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
24752 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
24753 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
24754 $this->get_latest_download_link( sprintf(
24755 /* translators: %s: Plan title */
24756 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
24757 $plan_title
24758 ) ),
24759 $deactivation_step,
24760 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
24761 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/wp-' . $this->_module_type . '-upload' ),
24762 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
24763 );
24764 }
24765 }
24766
24767 /**
24768 * @author Leo Fajardo (@leorw)
24769 * @since 2.5.3
24770 *
24771 * @param string $message_before_the_instructions
24772 * @param string $message_id
24773 * @param string $plan_title
24774 */
24775 private function add_complete_upgrade_instructions_notice(
24776 $message_before_the_instructions,
24777 $message_id,
24778 $plan_title = ''
24779 ) {
24780 $this->_admin_notices->add_sticky(
24781 $message_before_the_instructions .
24782 $this->get_complete_upgrade_instructions( $plan_title ),
24783 $message_id,
24784 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
24785 );
24786 }
24787
24788 /**
24789 * @author Leo Fajardo (@leorw)
24790 * @since 2.5.3
24791 *
24792 * @param bool $is_upgrade
24793 */
24794 private function add_after_plan_activation_or_upgrade_instructions_notice( $is_upgrade = true ) {
24795 $this->add_complete_upgrade_instructions_notice(
24796 $is_upgrade ?
24797 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) :
24798 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ),
24799 'plan_upgraded'
24800 );
24801 }
24802
24803 /**
24804 * @author Leo Fajardo (@leorw)
24805 * @since 2.1.0
24806 *
24807 * @param string $url
24808 * @param array $request
24809 * @param int $success_cache_expiration
24810 * @param int $failure_cache_expiration
24811 * @param bool $maybe_enrich_request_for_debug
24812 *
24813 * @return WP_Error|array
24814 */
24815 static function safe_remote_post(
24816 &$url,
24817 $request,
24818 $success_cache_expiration = 0,
24819 $failure_cache_expiration = 0,
24820 $maybe_enrich_request_for_debug = true
24821 ) {
24822 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
24823
24824 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
24825
24826 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
24827 get_transient( $cache_key ) :
24828 false;
24829
24830 if ( false === $response ) {
24831 if ( $maybe_enrich_request_for_debug ) {
24832 FS_DebugManager::enrich_request_for_debug( $url, $request );
24833 }
24834
24835 if ( ! isset( $request['method'] ) ) {
24836 $request['method'] = 'POST';
24837 }
24838
24839 $response = FS_Api::remote_request( $url, $request );
24840
24841 if (
24842 'https://' === substr( $url, 0, 8 ) &&
24843 FS_Api::is_ssl_error_response( $response )
24844 ) {
24845 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
24846 $url = 'http://' . substr( $url, 8 );
24847
24848 $request['timeout'] = 15;
24849 $response = FS_Api::remote_request( $url, $request );
24850 }
24851
24852 if ( false !== $cache_key ) {
24853 set_transient(
24854 $cache_key,
24855 $response,
24856 ( ( $response instanceof WP_Error ) ?
24857 $failure_cache_expiration :
24858 $success_cache_expiration )
24859 );
24860 }
24861 }
24862
24863 return $response;
24864 }
24865
24866 /**
24867 * This method is used to enrich the after upgrade notice instructions when the upgraded
24868 * license cannot be activated network wide (license quota isn't large enough).
24869 *
24870 * @author Vova Feldman (@svovaf)
24871 * @since 2.0.0
24872 *
24873 * @return string
24874 */
24875 private function get_license_network_activation_notice() {
24876 if ( ! $this->_is_network_active ) {
24877 // Module isn't network level activated.
24878 return '';
24879 }
24880
24881 if ( ! fs_is_network_admin() ) {
24882 // Not network level admin.
24883 return '';
24884 }
24885
24886 if ( get_blog_count() == 1 ) {
24887 // There's only a single site in the network so if there's a context license it was already activated.
24888 return '';
24889 }
24890
24891 if ( ! is_object( $this->_license ) ) {
24892 // No context license.
24893 return '';
24894 }
24895
24896 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
24897 // 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).
24898 return '';
24899 }
24900
24901 if ( $this->can_activate_license_on_network( $this->_license ) ) {
24902 // 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).
24903 return '';
24904 }
24905
24906 return sprintf(
24907 $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' ),
24908 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
24909 '</a>'
24910 );
24911 }
24912
24913 /**
24914 * @author Vova Feldman (@svovaf)
24915 * @since 1.2.1.7
24916 *
24917 * @param string $key
24918 *
24919 * @return string
24920 */
24921 function get_text( $key ) {
24922 return fs_text( $key, $this->_slug );
24923 }
24924
24925 /**
24926 * @author Vova Feldman (@svovaf)
24927 * @since 1.2.3
24928 *
24929 * @param string $text Translatable string.
24930 * @param string $key String key for overrides.
24931 *
24932 * @return string
24933 */
24934 function get_text_inline( $text, $key = '' ) {
24935 return _fs_text_inline( $text, $key, $this->_slug );
24936 }
24937
24938 /**
24939 * @author Vova Feldman (@svovaf)
24940 * @since 1.2.3
24941 *
24942 * @param string $text Translatable string.
24943 * @param string $context Context information for the translators.
24944 * @param string $key String key for overrides.
24945 *
24946 * @return string
24947 */
24948 function get_text_x_inline( $text, $context, $key ) {
24949 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
24950 }
24951
24952 /**
24953 * @author Vova Feldman (@svovaf)
24954 * @since 1.2.3
24955 *
24956 * @param string $text Translatable string.
24957 * @param string $key String key for overrides.
24958 *
24959 * @return string
24960 */
24961 function esc_html_inline( $text, $key ) {
24962 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
24963 }
24964
24965 #----------------------------------------------------------------------------------
24966 #region Versioning
24967 #----------------------------------------------------------------------------------
24968
24969 /**
24970 * Check if Freemius in SDK upgrade mode.
24971 *
24972 * @author Vova Feldman (@svovaf)
24973 * @since 1.0.9
24974 *
24975 * @return bool
24976 */
24977 function is_sdk_upgrade_mode() {
24978 return isset( $this->_storage->sdk_upgrade_mode ) ?
24979 $this->_storage->sdk_upgrade_mode :
24980 false;
24981 }
24982
24983 /**
24984 * Turn SDK upgrade mode off.
24985 *
24986 * @author Vova Feldman (@svovaf)
24987 * @since 1.0.9
24988 */
24989 function set_sdk_upgrade_complete() {
24990 $this->_storage->sdk_upgrade_mode = false;
24991 }
24992
24993 /**
24994 * Check if plugin upgrade mode.
24995 *
24996 * @author Vova Feldman (@svovaf)
24997 * @since 1.0.9
24998 *
24999 * @return bool
25000 */
25001 function is_plugin_upgrade_mode() {
25002 return isset( $this->_storage->plugin_upgrade_mode ) ?
25003 $this->_storage->plugin_upgrade_mode :
25004 false;
25005 }
25006
25007 /**
25008 * Turn plugin upgrade mode off.
25009 *
25010 * @author Vova Feldman (@svovaf)
25011 * @since 1.0.9
25012 */
25013 function set_plugin_upgrade_complete() {
25014 $this->_storage->plugin_upgrade_mode = false;
25015
25016 $license_migration = ! empty( $this->_storage->license_migration ) ?
25017 $this->_storage->license_migration :
25018 array();
25019
25020 $license_migration['is_migrating'] = false;
25021
25022 $this->_storage->license_migration = $license_migration;
25023 }
25024
25025 #endregion
25026
25027 #----------------------------------------------------------------------------------
25028 #region Permissions
25029 #----------------------------------------------------------------------------------
25030
25031 /**
25032 * Check if specific permission requested.
25033 *
25034 * @author Vova Feldman (@svovaf)
25035 * @since 1.1.6
25036 *
25037 * @param string $permission
25038 *
25039 * @return bool
25040 */
25041 function is_permission_requested( $permission ) {
25042 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
25043 }
25044
25045 #endregion
25046
25047 #----------------------------------------------------------------------------------
25048 #region Auto Activation
25049 #----------------------------------------------------------------------------------
25050
25051 /**
25052 * Hints the SDK if running an auto-installation.
25053 *
25054 * @var bool
25055 */
25056 private $_isAutoInstall = false;
25057
25058 /**
25059 * After upgrade callback to install and auto activate a plugin.
25060 * This code will only be executed on explicit request from the user,
25061 * following the practice Jetpack are using with their theme installations.
25062 *
25063 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
25064 *
25065 * @author Vova Feldman (@svovaf)
25066 * @since 1.2.1.7
25067 */
25068 function _install_premium_version_ajax_action() {
25069 $this->_logger->entrance();
25070
25071 $this->check_ajax_referer( 'install_premium_version' );
25072
25073 if ( ! $this->is_registered() ) {
25074 // Not registered.
25075 self::shoot_ajax_failure( array(
25076 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
25077 'code' => 'premium_installed',
25078 ) );
25079 }
25080
25081 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
25082
25083 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25084 // Invalid ID.
25085 self::shoot_ajax_failure( array(
25086 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25087 'code' => 'invalid_module_id',
25088 ) );
25089 }
25090
25091 if ( $plugin_id == $this->get_id() ) {
25092 if ( $this->is_premium() ) {
25093 // Already using the premium code version.
25094 self::shoot_ajax_failure( array(
25095 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
25096 'code' => 'premium_installed',
25097 ) );
25098 }
25099 if ( ! $this->can_use_premium_code() ) {
25100 // Don't have access to the premium code.
25101 self::shoot_ajax_failure( array(
25102 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
25103 'code' => 'invalid_license',
25104 ) );
25105 }
25106 if ( ! $this->has_release_on_freemius() ) {
25107 // Plugin is a serviceware, no premium code version.
25108 self::shoot_ajax_failure( array(
25109 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
25110 'code' => 'premium_version_missing',
25111 ) );
25112 }
25113 } else {
25114 $addon = $this->get_addon( $plugin_id );
25115
25116 if ( ! is_object( $addon ) ) {
25117 // Invalid add-on ID.
25118 self::shoot_ajax_failure( array(
25119 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25120 'code' => 'invalid_module_id',
25121 ) );
25122 }
25123
25124 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25125 // Premium add-on version is already activated.
25126 self::shoot_ajax_failure( array(
25127 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
25128 'code' => 'premium_installed',
25129 ) );
25130 }
25131 }
25132
25133 $this->_isAutoInstall = true;
25134
25135 // Try to install and activate.
25136 $updater = FS_Plugin_Updater::instance( $this );
25137 $result = $updater->install_and_activate_plugin( $plugin_id );
25138
25139 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
25140 self::shoot_ajax_failure( array(
25141 'message' => $result['message'],
25142 'code' => $result['code'],
25143 ) );
25144 }
25145
25146 self::shoot_ajax_success( $result );
25147 }
25148
25149 /**
25150 * Displays module activation dialog box after a successful upgrade
25151 * where the user explicitly requested to auto download and install
25152 * the premium version.
25153 *
25154 * @author Vova Feldman (@svovaf)
25155 * @since 1.2.1.7
25156 */
25157 function _add_auto_installation_dialog_box() {
25158 $this->_logger->entrance();
25159
25160 if ( ! $this->is_registered() ) {
25161 // Not registered.
25162 return;
25163 }
25164
25165 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
25166
25167 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25168 // Invalid module ID.
25169 return;
25170 }
25171
25172 if ( $plugin_id == $this->get_id() ) {
25173 if ( $this->is_premium() ) {
25174 // Already using the premium code version.
25175 return;
25176 }
25177 if ( ! $this->can_use_premium_code() ) {
25178 // Don't have access to the premium code.
25179 return;
25180 }
25181 if ( ! $this->has_release_on_freemius() ) {
25182 // Plugin is a serviceware, no premium code version.
25183 return;
25184 }
25185 } else {
25186 $addon = $this->get_addon( $plugin_id );
25187
25188 if ( ! is_object( $addon ) ) {
25189 // Invalid add-on ID.
25190 return;
25191 }
25192
25193 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25194 // Premium add-on version is already activated.
25195 return;
25196 }
25197 }
25198
25199 $vars = array(
25200 'id' => $this->_module_id,
25201 'target_module_id' => $plugin_id,
25202 'slug' => $this->_slug,
25203 );
25204
25205 fs_require_template( 'auto-installation.php', $vars );
25206 }
25207
25208 #endregion
25209
25210 #--------------------------------------------------------------------------------
25211 #region Tabs Integration
25212 #--------------------------------------------------------------------------------
25213
25214 #region Module's Original Tabs
25215
25216 /**
25217 * Inject a JavaScript logic to capture the theme tabs HTML.
25218 *
25219 * @author Vova Feldman (@svovaf)
25220 * @since 1.2.2.7
25221 */
25222 function _tabs_capture() {
25223 $this->_logger->entrance();
25224
25225 if (
25226 ! $this->is_product_settings_page() ||
25227 ! $this->should_page_include_tabs() ||
25228 ! $this->is_matching_url( $this->main_menu_url() )
25229 ) {
25230 return;
25231 }
25232
25233 $params = array(
25234 'id' => $this->_module_id,
25235 );
25236
25237 fs_require_once_template( 'tabs-capture-js.php', $params );
25238 }
25239
25240 /**
25241 * Cache theme's tabs HTML for a week. The cache will also be set as expired
25242 * after version and type (free/premium) changes, in addition to the week period.
25243 *
25244 * @author Vova Feldman (@svovaf)
25245 * @since 1.2.2.7
25246 */
25247 function _store_tabs_ajax_action() {
25248 $this->_logger->entrance();
25249
25250 $this->check_ajax_referer( 'store_tabs' );
25251
25252 // Init filesystem if not yet initiated.
25253 WP_Filesystem();
25254
25255 // Get POST body HTML data.
25256 global $wp_filesystem;
25257 $tabs_html = $wp_filesystem->get_contents( "php://input" );
25258
25259 if ( is_string( $tabs_html ) ) {
25260 $tabs_html = trim( $tabs_html );
25261 }
25262
25263 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
25264 self::shoot_ajax_failure();
25265 }
25266
25267 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25268
25269 self::shoot_ajax_success();
25270 }
25271
25272 /**
25273 * Cache theme's settings page custom styles. The cache will also be set as expired
25274 * after version and type (free/premium) changes, in addition to the week period.
25275 *
25276 * @author Vova Feldman (@svovaf)
25277 * @since 1.2.2.7
25278 */
25279 function _store_tabs_styles() {
25280 $this->_logger->entrance();
25281
25282 if (
25283 ! $this->is_product_settings_page() ||
25284 ! $this->should_page_include_tabs() ||
25285 ! $this->is_matching_url( $this->main_menu_url() )
25286 ) {
25287 return;
25288 }
25289
25290 $wp_styles = wp_styles();
25291
25292 $theme_styles_url = get_template_directory_uri();
25293
25294 $stylesheets = array();
25295 foreach ( $wp_styles->queue as $handler ) {
25296 if ( fs_starts_with( $handler, 'fs_' ) ) {
25297 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
25298 continue;
25299 }
25300
25301 /**
25302 * @var _WP_Dependency $stylesheet
25303 */
25304 $stylesheet = $wp_styles->registered[ $handler ];
25305
25306 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
25307 $stylesheets[] = $stylesheet->src;
25308 }
25309 }
25310
25311 if ( ! empty( $stylesheets ) ) {
25312 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25313 }
25314 }
25315
25316 /**
25317 * Check if module's original settings page has any tabs.
25318 *
25319 * @author Vova Feldman (@svovaf)
25320 * @since 1.2.2.7
25321 *
25322 * @return bool
25323 */
25324 private function has_tabs() {
25325 return $this->_cache->has( 'tabs' );
25326 }
25327
25328 /**
25329 * Get module's settings page HTML content, starting
25330 * from the beginning of the <div class="wrap"> element,
25331 * until the tabs HTML (including).
25332 *
25333 * @author Vova Feldman (@svovaf)
25334 * @since 1.2.2.7
25335 *
25336 * @return string
25337 */
25338 private function get_tabs_html() {
25339 $this->_logger->entrance();
25340
25341 return $this->_cache->get( 'tabs' );
25342 }
25343
25344 /**
25345 * Check if page should include tabs.
25346 *
25347 * @author Vova Feldman (@svovaf)
25348 * @since 1.2.2.7
25349 *
25350 * @return bool
25351 */
25352 private function should_page_include_tabs() {
25353 if ( ! $this->has_settings_menu() ) {
25354 // Don't add tabs if no settings at all.
25355 return false;
25356 }
25357
25358 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
25359 // Only add tabs to themes for now.
25360 return false;
25361 }
25362
25363 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
25364 // Only add tabs to monetizing themes.
25365 return false;
25366 }
25367
25368 if ( ! $this->is_product_settings_page() ) {
25369 // Only add tabs if browsing one of the product's setting pages.
25370 return false;
25371 }
25372
25373 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
25374 // Don't include tabs in the activation page.
25375 return false;
25376 }
25377
25378 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
25379 // Don't add tabs on checkout page, we want to reduce distractions
25380 // as much as possible.
25381 return false;
25382 }
25383
25384 return true;
25385 }
25386
25387 /**
25388 * Add the tabs HTML before the setting's page content and
25389 * enqueue any required stylesheets.
25390 *
25391 * @author Vova Feldman (@svovaf)
25392 * @since 1.2.2.7
25393 *
25394 * @return bool If tabs were included.
25395 */
25396 function _add_tabs_before_content() {
25397 $this->_logger->entrance();
25398
25399 if ( ! $this->should_page_include_tabs() ) {
25400 return false;
25401 }
25402
25403 $tabs_html = $this->get_tabs_html();
25404
25405 if ( empty( $tabs_html ) ) {
25406 return false;
25407 }
25408
25409 /**
25410 * Enqueue the original stylesheets that are included in the
25411 * theme settings page. That way, if the theme settings has
25412 * some custom _styled_ content above the tabs UI, this
25413 * will make sure that the styling is preserved.
25414 */
25415 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
25416 if ( is_array( $stylesheets ) ) {
25417 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
25418 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
25419 }
25420 }
25421
25422 // Cut closing </div> tag.
25423 echo substr( trim( $tabs_html ), 0, - 6 );
25424
25425 return true;
25426 }
25427
25428 /**
25429 * Add the tabs closing HTML after the setting's page content.
25430 *
25431 * @author Vova Feldman (@svovaf)
25432 * @since 1.2.2.7
25433 *
25434 * @return bool If tabs closing HTML was included.
25435 */
25436 function _add_tabs_after_content() {
25437 $this->_logger->entrance();
25438
25439 if ( ! $this->should_page_include_tabs() ) {
25440 return false;
25441 }
25442
25443 echo '</div>';
25444
25445 return true;
25446 }
25447
25448 #endregion
25449
25450 /**
25451 * Add in-page JavaScript to inject the Freemius tabs into
25452 * the module's setting tabs section.
25453 *
25454 * @author Vova Feldman (@svovaf)
25455 * @since 1.2.2.7
25456 */
25457 function _add_freemius_tabs() {
25458 $this->_logger->entrance();
25459
25460 if ( ! $this->should_page_include_tabs() ) {
25461 return;
25462 }
25463
25464 $params = array( 'id' => $this->_module_id );
25465 fs_require_once_template( 'tabs.php', $params );
25466 }
25467
25468 #endregion
25469
25470 #--------------------------------------------------------------------------------
25471 #region Customizer Integration for Themes
25472 #--------------------------------------------------------------------------------
25473
25474 /**
25475 * @author Vova Feldman (@svovaf)
25476 * @since 1.2.2.7
25477 *
25478 * @param WP_Customize_Manager $customizer
25479 */
25480 function _customizer_register( $customizer ) {
25481 $this->_logger->entrance();
25482
25483 if ( $this->is_pricing_page_visible() ) {
25484 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
25485
25486 $customizer->add_section( 'freemius_upsell', array(
25487 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
25488 'priority' => 1,
25489 ) );
25490 $customizer->add_setting( 'freemius_upsell', array(
25491 'sanitize_callback' => 'esc_html',
25492 ) );
25493
25494 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
25495 'fs' => $this,
25496 'section' => 'freemius_upsell',
25497 'priority' => 100,
25498 ) ) );
25499 }
25500
25501 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
25502 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
25503
25504 // Main Documentation Link In Customizer Root.
25505 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
25506 'fs' => $this,
25507 'priority' => 1000,
25508 ) ) );
25509 }
25510 }
25511
25512 #endregion
25513
25514 /**
25515 * If the theme has a paid version, add some custom
25516 * styling to the theme's premium version (if exists)
25517 * to highlight that it's the premium version of the
25518 * same theme, making it easier for identification
25519 * after the user upgrades and upload it to the site.
25520 *
25521 * @author Vova Feldman (@svovaf)
25522 * @since 1.2.2.7
25523 */
25524 function _style_premium_theme() {
25525 $this->_logger->entrance();
25526
25527 if ( ! self::is_themes_page() ) {
25528 // Only include in the themes page.
25529 return;
25530 }
25531
25532 if ( ! $this->has_paid_plan() ) {
25533 // Only include if has any paid plans.
25534 return;
25535 }
25536
25537 $params = null;
25538 fs_require_once_template( '/js/jquery.content-change.php', $params );
25539
25540 $params = array(
25541 'slug' => $this->_slug,
25542 'id' => $this->_module_id,
25543 );
25544
25545 fs_require_template( '/js/style-premium-theme.php', $params );
25546 }
25547
25548 /**
25549 * This method will return the absolute URL of the module's local icon.
25550 *
25551 * When you are running your plugin or theme on a **localhost** environment, if the icon
25552 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
25553 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
25554 * If an icon is found, this method will automatically attempt to download the icon and store it
25555 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
25556 *
25557 * It's important to mention that this method is NOT phoning home since the developer will deploy
25558 * the product with the local icon in the assets folder. The download process just simplifies
25559 * the process for the developer.
25560 *
25561 * @author Vova Feldman (@svovaf)
25562 * @since 2.0.0
25563 *
25564 * @return string
25565 */
25566 function get_local_icon_url() {
25567 global $fs_active_plugins;
25568
25569 /**
25570 * @since 1.1.7.5
25571 */
25572 $local_path = $this->apply_filters( 'plugin_icon', false );
25573
25574 if ( is_string( $local_path ) ) {
25575 $icons = array( $local_path );
25576 } else {
25577 $img_dir = WP_FS__DIR_IMG;
25578
25579 // Locate the main assets folder.
25580 if ( ! empty( $fs_active_plugins->plugins ) ) {
25581 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
25582
25583 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
25584 if ( $data->plugin_path == $this->get_plugin_basename() ) {
25585 $img_dir = $plugin_or_theme_img_dir
25586 . '/'
25587 /**
25588 * The basename will be `themes` or the basename of a custom themes directory.
25589 *
25590 * @author Leo Fajardo (@leorw)
25591 * @since 2.2.3
25592 */
25593 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
25594 . '/assets/img';
25595
25596 break;
25597 }
25598 }
25599 }
25600
25601 // Try to locate the icon in the assets folder.
25602 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
25603
25604 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
25605 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
25606 $icons = array(
25607 fs_normalize_path( $img_dir . '/theme-icon.png' )
25608 );
25609 } else {
25610 $icon_found = false;
25611 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
25612
25613 if ( ! function_exists( 'get_filesystem_method' ) ) {
25614 require_once ABSPATH . 'wp-admin/includes/file.php';
25615 }
25616
25617 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
25618
25619 /**
25620 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
25621 *
25622 * This code will only be executed once during the testing
25623 * of the plugin in a local environment. The plugin icon file WILL
25624 * already exist in the assets folder when the plugin is deployed to
25625 * the repository.
25626 */
25627 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
25628 // Fetch icon from Freemius.
25629 $icon = $this->fetch_remote_icon_url();
25630
25631 // Fetch icon from WordPress.org.
25632 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
25633 if ( ! function_exists( 'plugins_api' ) ) {
25634 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
25635 }
25636
25637 $plugin_information = plugins_api( 'plugin_information', array(
25638 'slug' => $this->_slug,
25639 'fields' => array(
25640 'sections' => false,
25641 'tags' => false,
25642 'icons' => true
25643 )
25644 ) );
25645
25646 if (
25647 ! is_wp_error( $plugin_information )
25648 && isset( $plugin_information->icons )
25649 && ! empty( $plugin_information->icons )
25650 ) {
25651 /**
25652 * Get the smallest icon.
25653 *
25654 * @author Leo Fajardo (@leorw)
25655 * @since 1.2.2
25656 */
25657 $icon = end( $plugin_information->icons );
25658 }
25659 }
25660
25661 if ( ! empty( $icon ) ) {
25662 if ( 0 !== strpos( $icon, 'http' ) ) {
25663 $icon = 'http:' . $icon;
25664 }
25665
25666 /**
25667 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
25668 *
25669 * @author Leo Fajardo (@leorw)
25670 * @since 1.2.2
25671 */
25672 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
25673
25674 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
25675
25676 // Try to download the icon.
25677 $icon_found = fs_download_image( $icon, $local_path );
25678 }
25679 }
25680
25681 if ( ! $icon_found ) {
25682 // No icons found, fallback to default icon.
25683 if ( $have_write_permissions ) {
25684 // If have write permissions, copy default icon.
25685 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
25686 } else {
25687 // If doesn't have write permissions, use default icon path.
25688 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
25689 }
25690 }
25691
25692 $icons = array( $local_path );
25693 }
25694 }
25695 }
25696
25697 $icon_dir = dirname( $icons[0] );
25698
25699 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
25700 }
25701
25702 /**
25703 * Fetch module's extended info.
25704 *
25705 * @author Vova Feldman (@svovaf)
25706 * @since 2.0.0
25707 *
25708 * @return object|mixed
25709 */
25710 private function fetch_module_info() {
25711 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
25712 }
25713
25714 /**
25715 * Fetch module's remote icon URL.
25716 *
25717 * @author Vova Feldman (@svovaf)
25718 * @since 2.0.0
25719 *
25720 * @return string
25721 */
25722 function fetch_remote_icon_url() {
25723 $info = $this->fetch_module_info();
25724
25725 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
25726 $info->icon :
25727 '';
25728 }
25729
25730 #--------------------------------------------------------------------------------
25731 #region GDPR
25732 #--------------------------------------------------------------------------------
25733
25734 /**
25735 * @author Leo Fajardo (@leorw)
25736 * @since 2.1.0
25737 *
25738 * @param array $user_plugins
25739 *
25740 * @return string
25741 */
25742 private function get_gdpr_admin_notice_string( $user_plugins ) {
25743 $this->_logger->entrance();
25744
25745 $addons = self::get_all_addons();
25746
25747 foreach ( $user_plugins as $user_plugin ) {
25748 $has_addons = isset( $addons[ $user_plugin->id ] );
25749
25750 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
25751 if ( $this->_module_id == $user_plugin->id ) {
25752 $addons = $this->get_addons();
25753 $has_addons = ( ! empty( $addons ) );
25754 } else {
25755 $plugin_api = FS_Api::instance(
25756 $user_plugin->id,
25757 'plugin',
25758 $user_plugin->id,
25759 $user_plugin->public_key,
25760 ! $user_plugin->is_live,
25761 false,
25762 $this->get_sdk_version()
25763 );
25764
25765 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
25766
25767 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
25768 is_array( $addons_result->plugins ) &&
25769 ! empty( $addons_result->plugins )
25770 ) {
25771 $has_addons = true;
25772 }
25773 }
25774 }
25775
25776 $user_plugin->has_addons = $has_addons;
25777 }
25778
25779 $is_single_parent_product = ( 1 === count( $user_plugins ) );
25780
25781 $multiple_products_text = '';
25782
25783 if ( $is_single_parent_product ) {
25784 $single_parent_product = reset( $user_plugins );
25785
25786 $thank_you = sprintf(
25787 "<span data-plugin-id='%d'>%s</span>",
25788 $single_parent_product->id,
25789 sprintf(
25790 $single_parent_product->has_addons ?
25791 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
25792 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
25793 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
25794 )
25795 );
25796
25797 $already_opted_in = sprintf(
25798 $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' ),
25799 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
25800 );
25801 } else {
25802 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
25803 $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' );
25804
25805 $products_and_add_ons = '';
25806 foreach ( $user_plugins as $user_plugin ) {
25807 if ( ! empty( $products_and_add_ons ) ) {
25808 $products_and_add_ons .= ', ';
25809 }
25810
25811 if ( ! $user_plugin->has_addons ) {
25812 $products_and_add_ons .= sprintf(
25813 "<span data-plugin-id='%d'>%s</span>",
25814 $user_plugin->id,
25815 $user_plugin->title
25816 );
25817 } else {
25818 $products_and_add_ons .= sprintf(
25819 "<span data-plugin-id='%d'>%s</span>",
25820 $user_plugin->id,
25821 sprintf(
25822 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
25823 $user_plugin->title
25824 )
25825 );
25826 }
25827 }
25828
25829 $multiple_products_text = sprintf(
25830 "<small class='products'><strong>%s:</strong> %s</small>",
25831 $this->get_text_inline( 'Products', 'products' ),
25832 $products_and_add_ons
25833 );
25834 }
25835
25836 $actions = sprintf(
25837 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
25838 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
25839 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
25840 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
25841 sprintf(
25842 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
25843 '<span class="underlined">',
25844 '</span>'
25845 )
25846 );
25847
25848 return sprintf(
25849 '%s %s %s',
25850 $thank_you,
25851 $already_opted_in,
25852 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>' ) .
25853 '<br><br>' .
25854 '<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>' .
25855 $actions .
25856 ( $is_single_parent_product ? '' : $multiple_products_text )
25857 );
25858 }
25859
25860 /**
25861 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
25862 * plugins and themes they've opted in to.
25863 *
25864 * @author Leo Fajardo (@leorw)
25865 * @since 2.1.0
25866 *
25867 * @param string $user_email
25868 * @param string $license_key
25869 * @param array $plugin_ids
25870 * @param string|null $license_key
25871 *
25872 * @return array|false
25873 */
25874 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
25875 $request = array(
25876 'method' => 'POST',
25877 'body' => array(),
25878 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
25879 );
25880
25881 if ( is_string( $user_email ) ) {
25882 $request['body']['email'] = $user_email;
25883 } else {
25884 $request['body']['license_key'] = $license_key;
25885 }
25886
25887 $result = array();
25888
25889 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
25890 $total_plugin_ids = count( $plugin_ids );
25891
25892 $plugin_ids_count_per_request = 10;
25893 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
25894 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
25895
25896 $request['body']['plugin_ids'] = $plugin_ids_set;
25897
25898 $response = self::safe_remote_post(
25899 $url,
25900 $request,
25901 WP_FS__TIME_24_HOURS_IN_SEC,
25902 WP_FS__TIME_12_HOURS_IN_SEC
25903 );
25904
25905 if ( ! is_wp_error( $response ) ) {
25906 $decoded = is_string( $response['body'] ) ?
25907 json_decode( $response['body'] ) :
25908 null;
25909
25910 if (
25911 !is_object($decoded) ||
25912 !isset($decoded->success) ||
25913 true !== $decoded->success ||
25914 !isset( $decoded->data ) ||
25915 !is_array( $decoded->data )
25916 ) {
25917 return false;
25918 }
25919
25920 $result = array_merge( $result, $decoded->data );
25921 }
25922 }
25923
25924 return $result;
25925 }
25926
25927 /**
25928 * @author Leo Fajardo (@leorw)
25929 * @since 2.1.0
25930 */
25931 function _maybe_show_gdpr_admin_notice() {
25932 if ( ! $this->is_user_in_admin() ) {
25933 return;
25934 }
25935
25936 if ( ! $this->should_handle_gdpr_admin_notice() ) {
25937 return;
25938 }
25939
25940 if ( ! $this->is_user_admin() ) {
25941 return;
25942 }
25943
25944 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25945
25946 $lock = FS_User_Lock::instance();
25947
25948 /**
25949 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
25950 */
25951 if ( ! $lock->try_lock( 60 ) ) {
25952 return;
25953 }
25954
25955 /**
25956 * @var $current_wp_user WP_User
25957 */
25958 $current_wp_user = self::_get_current_wp_user();
25959
25960 /**
25961 * @var FS_User $current_fs_user
25962 */
25963 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
25964
25965 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
25966
25967 if ( ! is_object( $current_fs_user ) ) {
25968 // 10-year lock.
25969 $lock->lock( $ten_years_in_sec );
25970
25971 return;
25972 }
25973
25974 $gdpr = FS_GDPR_Manager::instance();
25975
25976 if ( $gdpr->is_opt_in_notice_shown() ) {
25977 // 30-day lock.
25978 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
25979
25980 return;
25981 }
25982
25983 if ( ! $gdpr->should_show_opt_in_notice() ) {
25984 // 10-year lock.
25985 $lock->lock( $ten_years_in_sec );
25986
25987 return;
25988 }
25989
25990 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
25991 $was_notice_shown_before = ( false !== $last_time_notice_shown );
25992
25993 if ( $was_notice_shown_before &&
25994 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
25995 ) {
25996 // If the notice was shown before, show it again after 30 days from the last time it was shown.
25997 return;
25998 }
25999
26000 /**
26001 * Find all plugin IDs that were installed by the current admin.
26002 */
26003 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
26004
26005 if ( empty( $plugin_ids_map )) {
26006 $lock->lock( $ten_years_in_sec );
26007
26008 return;
26009 }
26010
26011 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26012 $current_fs_user->email,
26013 null,
26014 array_keys( $plugin_ids_map )
26015 );
26016
26017 if ( empty( $user_plugins ) ) {
26018 $lock->lock(
26019 is_array($user_plugins) ?
26020 $ten_years_in_sec :
26021 // Lock for 24-hours on errors.
26022 WP_FS__TIME_24_HOURS_IN_SEC
26023 );
26024
26025 return;
26026 }
26027
26028 $has_unset_marketing_optin = false;
26029
26030 foreach ( $user_plugins as $user_plugin ) {
26031 if ( true == $user_plugin->is_marketing_allowed ) {
26032 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
26033 }
26034
26035 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
26036 $has_unset_marketing_optin = true;
26037 }
26038 }
26039
26040 if ( empty( $plugin_ids_map ) ||
26041 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
26042 ) {
26043 $lock->lock( $ten_years_in_sec );
26044
26045 return;
26046 }
26047
26048 $modules = array_merge(
26049 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26050 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26051 );
26052
26053 foreach ( $modules as $module ) {
26054 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
26055 $plugin_ids_map[ $module->id ] = $module;
26056 }
26057 }
26058
26059 $plugin_title = null;
26060 if ( 1 === count( $plugin_ids_map ) ) {
26061 $module = reset( $plugin_ids_map );
26062 $plugin_title = $module->title;
26063 }
26064
26065 $gdpr->add_opt_in_sticky_notice(
26066 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
26067 $plugin_title
26068 );
26069
26070 $this->add_gdpr_optin_ajax_handler_and_style();
26071
26072 $gdpr->notice_was_just_shown();
26073
26074 // 30-day lock.
26075 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26076 }
26077
26078 /**
26079 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
26080 * marketing.
26081 *
26082 * @author Leo Fajardo (@leorw)
26083 * @since 2.1.0
26084 */
26085 private function disable_opt_in_notice_and_lock_user() {
26086 FS_GDPR_Manager::instance()->disable_opt_in_notice();
26087
26088 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26089
26090 // 10-year lock.
26091 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26092 }
26093
26094 /**
26095 * @author Leo Fajardo (@leorw)
26096 * @since 2.5.4
26097 */
26098 static function _add_api_connectivity_notice_handler_js() {
26099 fs_require_once_template( 'api-connectivity-message-js.php' );
26100 }
26101
26102 /**
26103 * @author Leo Fajardo (@leorw)
26104 * @since 2.1.0
26105 */
26106 function _add_gdpr_optin_js() {
26107 $vars = array( 'id' => $this->_module_id );
26108
26109 fs_require_once_template( 'gdpr-optin-js.php', $vars );
26110 }
26111
26112 /**
26113 * @author Leo Fajardo (@leorw)
26114 * @since 2.1.0
26115 */
26116 function enqueue_gdpr_optin_notice_style() {
26117 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
26118 }
26119
26120 /**
26121 * @author Leo Fajardo (@leorw)
26122 * @since 2.1.0
26123 */
26124 function _maybe_add_gdpr_optin_ajax_handler() {
26125 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
26126
26127 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
26128 $this->add_gdpr_optin_ajax_handler_and_style();
26129 }
26130 }
26131
26132 /**
26133 * @author Leo Fajardo (@leorw)
26134 * @since 2.1.0
26135 */
26136 function _fetch_is_marketing_required_flag_value_ajax_action() {
26137 $this->_logger->entrance();
26138
26139 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
26140
26141 $license_key = fs_request_get_raw( 'license_key' );
26142
26143 if ( empty($license_key) ) {
26144 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
26145 }
26146
26147 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26148 null,
26149 $license_key,
26150 array( $this->_module_id )
26151 );
26152
26153 if ( ! is_array( $user_plugins ) ||
26154 empty($user_plugins) ||
26155 !isset($user_plugins[0]->plugin_id) ||
26156 $user_plugins[0]->plugin_id != $this->_module_id
26157 ) {
26158 /**
26159 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
26160 *
26161 * @author Vova Feldman (@svovaf)
26162 */
26163 self::shoot_ajax_success( array(
26164 'is_marketing_allowed' => null,
26165 'license_owner_id' => null
26166 ) );
26167 }
26168
26169 self::shoot_ajax_success( array(
26170 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
26171 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
26172 ) );
26173 }
26174
26175 /**
26176 * @author Leo Fajardo (@leorw)
26177 * @since 2.3.2
26178 *
26179 * @param number[] $install_ids
26180 *
26181 * @return array {
26182 * An array of objects containing the installs' licenses owners data.
26183 *
26184 * @property number $id User ID.
26185 * @property string $email User email (can be masked email).
26186 * }
26187 */
26188 private function fetch_installs_licenses_owners_data( $install_ids ) {
26189 $this->_logger->entrance();
26190
26191 $response = $this->get_api_user_scope()->get(
26192 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
26193 );
26194
26195 $license_owners = array();
26196
26197 if ( $this->is_api_result_object( $response, 'owners' ) ) {
26198 $license_owners = $response->owners;
26199 }
26200
26201 return $license_owners;
26202 }
26203
26204 /**
26205 * @author Leo Fajardo (@leorw)
26206 * @since 2.1.0
26207 */
26208 private function add_gdpr_optin_ajax_handler_and_style() {
26209 // Add GDPR action AJAX callback.
26210 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
26211
26212 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
26213 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
26214 }
26215
26216 /**
26217 * @author Leo Fajardo (@leorw)
26218 * @since 2.1.0
26219 */
26220 function _gdpr_optin_ajax_action() {
26221 $this->_logger->entrance();
26222
26223 $this->check_ajax_referer( 'gdpr_optin_action' );
26224
26225 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
26226 self::shoot_ajax_failure();
26227 }
26228
26229 $current_wp_user = self::_get_current_wp_user();
26230
26231 $plugin_ids = fs_request_get( 'plugin_ids', array() );
26232 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
26233 self::shoot_ajax_failure();
26234 }
26235
26236 $modules = array_merge(
26237 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26238 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26239 );
26240
26241 foreach ( $modules as $key => $module ) {
26242 if ( ! in_array( $module->id, $plugin_ids ) ) {
26243 unset( $modules[ $key ] );
26244 }
26245 }
26246
26247 if ( empty( $modules ) ) {
26248 self::shoot_ajax_failure();
26249 }
26250
26251 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
26252
26253 foreach ( $modules as $module ) {
26254 $user_api->call( "?plugin_id={$module->id}", 'put', array(
26255 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
26256 ) );
26257 }
26258
26259 FS_GDPR_Manager::instance()->remove_opt_in_notice();
26260
26261 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26262
26263 // 10-year lock.
26264 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26265
26266 self::shoot_ajax_success();
26267 }
26268
26269 /**
26270 * 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.
26271 *
26272 * @author Vova Feldman (@svovaf)
26273 * @since 2.1.0
26274 *
26275 * @return bool
26276 */
26277 private function should_handle_gdpr_admin_notice() {
26278 return $this->apply_filters(
26279 'handle_gdpr_admin_notice',
26280 // Default to false.
26281 false
26282 );
26283 }
26284
26285 #endregion
26286
26287 #----------------------------------------------------------------------------------
26288 #region Marketing
26289 #----------------------------------------------------------------------------------
26290
26291 /**
26292 * Check if current user purchased any other plugins before.
26293 *
26294 * @author Vova Feldman (@svovaf)
26295 * @since 1.0.9
26296 *
26297 * @return bool
26298 */
26299 function has_purchased_before() {
26300 // TODO: Implement has_purchased_before() method.
26301 throw new Exception( 'not implemented' );
26302 }
26303
26304 /**
26305 * Check if current user classified as an agency.
26306 *
26307 * @author Vova Feldman (@svovaf)
26308 * @since 1.0.9
26309 *
26310 * @return bool
26311 */
26312 function is_agency() {
26313 // TODO: Implement is_agency() method.
26314 throw new Exception( 'not implemented' );
26315 }
26316
26317 /**
26318 * Check if current user classified as a developer.
26319 *
26320 * @author Vova Feldman (@svovaf)
26321 * @since 1.0.9
26322 *
26323 * @return bool
26324 */
26325 function is_developer() {
26326 // TODO: Implement is_developer() method.
26327 throw new Exception( 'not implemented' );
26328 }
26329
26330 /**
26331 * Check if current user classified as a business.
26332 *
26333 * @author Vova Feldman (@svovaf)
26334 * @since 1.0.9
26335 *
26336 * @return bool
26337 */
26338 function is_business() {
26339 // TODO: Implement is_business() method.
26340 throw new Exception( 'not implemented' );
26341 }
26342
26343 #endregion
26344
26345 #----------------------------------------------------------------------------------
26346 #region Helper
26347 #----------------------------------------------------------------------------------
26348
26349 /**
26350 * If running with a secret key, assume it's the developer and show pending plans as well.
26351 *
26352 * @author Vova Feldman (@svovaf)
26353 * @since 2.1.2
26354 *
26355 * @param string $path
26356 *
26357 * @return string
26358 */
26359 function add_show_pending( $path ) {
26360 if ( ! $this->has_secret_key() ) {
26361 return $path;
26362 }
26363
26364 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
26365 }
26366
26367 #endregion
26368 }
26369