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

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

26,331 lines 990.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Freemius
4 * @copyright Copyright (c) 2015, Freemius, Inc.
5 * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
6 * @since 1.0.3
7 */
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 // "final class"
13 class Freemius extends Freemius_Abstract {
14 /**
15 * SDK Version
16 *
17 * @var string
18 */
19 public $version = WP_FS__SDK_VERSION;
20
21 #region Plugin Info
22
23 /**
24 * @since 1.0.1
25 *
26 * @var string
27 */
28 private $_slug;
29
30 /**
31 * @since 1.0.0
32 *
33 * @var string
34 */
35 private $_plugin_basename;
36 /**
37 * @since 2.2.1
38 *
39 * @var string
40 */
41 private $_premium_plugin_basename;
42 /**
43 * @since 1.0.0
44 *
45 * @var string
46 */
47 private $_free_plugin_basename;
48 /**
49 * @since 1.0.0
50 *
51 * @var string
52 */
53 private $_plugin_dir_path;
54 /**
55 * @since 1.0.0
56 *
57 * @var string
58 */
59 private $_plugin_dir_name;
60 /**
61 * @since 1.0.0
62 *
63 * @var string
64 */
65 private $_plugin_main_file_path;
66 /**
67 * @var string[]
68 */
69 private $_plugin_data;
70 /**
71 * @since 1.0.9
72 *
73 * @var string
74 */
75 private $_plugin_name;
76 /**
77 * @since 1.2.2
78 *
79 * @var string
80 */
81 private $_module_type;
82
83 #endregion Plugin Info
84
85 /**
86 * @since 1.0.9
87 *
88 * @var bool If false, don't turn Freemius on.
89 */
90 private $_is_on;
91
92 /**
93 * @since 1.1.3
94 *
95 * @var bool If false, don't turn Freemius on.
96 */
97 private $_is_anonymous;
98
99 /**
100 * @since 1.0.9
101 * @var bool If false, issues with connectivity to Freemius API.
102 */
103 private $_has_api_connection;
104
105 /**
106 * @since 1.0.9
107 * @since 2.0.0 Default to true since we need the property during the instance construction, prior to the dynamic_init() execution.
108 * @var bool Hints the SDK if plugin can support anonymous mode (if skip connect is visible).
109 */
110 private $_enable_anonymous = true;
111
112 /**
113 * @since 2.9.1
114 * @var string|null Hints the SDK whether the plugin supports parallel activation mode, preventing the auto-deactivation of the free version when the premium version is activated, and vice versa.
115 */
116 private $_premium_plugin_basename_from_parallel_activation;
117
118 /**
119 * @since 1.1.7.5
120 * @var bool Hints the SDK if plugin should run in anonymous mode (only adds feedback form).
121 */
122 private $_anonymous_mode;
123
124 /**
125 * @since 1.1.9
126 * @var bool Hints the SDK if plugin have any free plans.
127 */
128 private $_is_premium_only;
129
130 /**
131 * @since 1.2.1.6
132 * @var bool Hints the SDK if plugin have premium code version at all.
133 */
134 private $_has_premium_version;
135
136 /**
137 * @since 1.2.1.6
138 * @var bool Hints the SDK if plugin should ignore pending mode by simulating a skip.
139 */
140 private $_ignore_pending_mode;
141
142 /**
143 * @since 1.0.8
144 * @var bool Hints the SDK if the plugin has any paid plans.
145 */
146 private $_has_paid_plans;
147
148 /**
149 * @since 1.2.1.5
150 * @var int Hints the SDK if the plugin offers a trial period. If negative, no trial, if zero - has a trial but
151 * without a specified period, if positive - the number of trial days.
152 */
153 private $_trial_days = - 1;
154
155 /**
156 * @since 1.2.1.5
157 * @var bool Hints the SDK if the trial requires a payment method or not.
158 */
159 private $_is_trial_require_payment = false;
160
161 /**
162 * @since 1.0.7
163 * @var bool Hints the SDK if the plugin is WordPress.org compliant.
164 */
165 private $_is_org_compliant;
166
167 /**
168 * @since 1.0.7
169 * @var bool Hints the SDK if the plugin is has add-ons.
170 */
171 private $_has_addons;
172
173 /**
174 * @since 2.4.5
175 * @var string Navigation type: 'menu' or 'tabs'.
176 */
177 private $_navigation;
178
179 const NAVIGATION_MENU = 'menu';
180 const NAVIGATION_TABS = 'tabs';
181
182 /**
183 * @since 1.1.6
184 * @var string[]bool.
185 */
186 private $_permissions;
187
188 /**
189 * @var FS_Storage
190 */
191 private $_storage;
192
193 /**
194 * @since 1.2.2.7
195 * @var FS_Cache_Manager
196 */
197 private $_cache;
198
199 /**
200 * @since 1.0.0
201 *
202 * @var FS_Logger
203 */
204 private $_logger;
205 /**
206 * @since 1.0.4
207 *
208 * @var FS_Plugin
209 */
210 private $_plugin = false;
211 /**
212 * @since 1.0.4
213 *
214 * @var FS_Plugin|false
215 */
216 private $_parent_plugin = false;
217 /**
218 * @since 1.1.1
219 *
220 * @var Freemius
221 */
222 private $_parent = false;
223 /**
224 * @since 1.0.1
225 *
226 * @var FS_User
227 */
228 private $_user = false;
229 /**
230 * @since 1.0.1
231 *
232 * @var FS_Site
233 */
234 private $_site = false;
235 /**
236 * @since 1.0.1
237 *
238 * @var FS_Plugin_License
239 */
240 private $_license;
241 /**
242 * @since 1.0.2
243 *
244 * @var FS_Plugin_Plan[]
245 */
246 private $_plans = false;
247 /**
248 * @var FS_Plugin_License[]
249 * @since 1.0.5
250 */
251 private $_licenses = false;
252
253 /**
254 * @since 1.0.1
255 *
256 * @var FS_Admin_Menu_Manager
257 */
258 private $_menu;
259
260 /**
261 * @var FS_Admin_Notices
262 */
263 private $_admin_notices;
264
265 /**
266 * @since 1.1.6
267 *
268 * @var FS_Admin_Notices
269 */
270 private static $_global_admin_notices;
271
272 /**
273 * @var FS_Logger
274 * @since 1.0.0
275 */
276 private static $_static_logger;
277
278 /**
279 * @var FS_Options
280 * @since 1.0.2
281 */
282 private static $_accounts;
283
284 /**
285 * @since 1.2.2
286 *
287 * @var number
288 */
289 private $_module_id;
290
291 /**
292 * @var Freemius[]
293 */
294 private static $_instances = array();
295
296 /**
297 * @since 1.2.3
298 *
299 * @var FS_Affiliate
300 */
301 private $affiliate = null;
302
303 /**
304 * @since 1.2.3
305 *
306 * @var FS_AffiliateTerms
307 */
308 private $plugin_affiliate_terms = null;
309
310 /**
311 * @since 1.2.3
312 *
313 * @var FS_AffiliateTerms
314 */
315 private $custom_affiliate_terms = null;
316
317 /**
318 * @since 2.0.0
319 *
320 * @var bool
321 */
322 private $_is_multisite_integrated;
323
324 /**
325 * @since 2.0.0
326 *
327 * @var bool True if the current request is for a network admin screen and the plugin is network active.
328 */
329 private $_is_network_active;
330
331 /**
332 * @since 2.0.0
333 *
334 * @var int|null The original blog ID the plugin was loaded with.
335 */
336 private $_blog_id = null;
337
338 /**
339 * @since 2.0.0
340 *
341 * @var int|null The current execution context. When true, run on network context. When int, run on the specified blog context.
342 */
343 private $_context_is_network_or_blog_id = null;
344
345 /**
346 * @since 2.0.0
347 *
348 * @var string
349 */
350 private $_dynamically_added_top_level_page_hook_name = '';
351
352 /**
353 * @author Leo Fajardo (@leorw)
354 * @since 2.3.1
355 *
356 * @var bool
357 */
358 private $is_whitelabeled;
359
360 /**
361 * @author Leo Fajardo (@leorw)
362 * @since 2.4.0
363 *
364 * @var bool
365 */
366 private $_is_bundle_license_auto_activation_enabled = false;
367
368 #region Uninstall Reasons IDs
369
370 const REASON_NO_LONGER_NEEDED = 1;
371 const REASON_FOUND_A_BETTER_PLUGIN = 2;
372 const REASON_NEEDED_FOR_A_SHORT_PERIOD = 3;
373 const REASON_BROKE_MY_SITE = 4;
374 const REASON_SUDDENLY_STOPPED_WORKING = 5;
375 const REASON_CANT_PAY_ANYMORE = 6;
376 const REASON_OTHER = 7;
377 const REASON_DIDNT_WORK = 8;
378 const REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION = 9;
379 const REASON_COULDNT_MAKE_IT_WORK = 10;
380 const REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE = 11;
381 const REASON_NOT_WORKING = 12;
382 const REASON_NOT_WHAT_I_WAS_LOOKING_FOR = 13;
383 const REASON_DIDNT_WORK_AS_EXPECTED = 14;
384 const REASON_TEMPORARY_DEACTIVATION = 15;
385
386 #endregion
387
388 /**
389 * @author Leo Fajardo (@leorw)
390 * @since 2.4.2
391 *
392 * @var string|null
393 */
394 private $_pricing_js_path = null;
395
396 const VERSION_MAX_CHARS = 16;
397 const LANGUAGE_MAX_CHARS = 8;
398
399 /* Ctor
400 ------------------------------------------------------------------------------------------------------------------*/
401
402 /**
403 * Main singleton instance.
404 *
405 * @author Vova Feldman (@svovaf)
406 * @since 1.0.0
407 *
408 * @param number $module_id
409 * @param string|bool $slug
410 * @param bool $is_init Since 1.2.1 Is initiation sequence.
411 */
412 private function __construct( $module_id, $slug = false, $is_init = false ) {
413 $main_file = false;
414
415 if ( $is_init && is_numeric( $module_id ) && is_string( $slug ) ) {
416 $main_file = $this->store_id_slug_type_path_map( $module_id, $slug );
417 }
418
419 $this->_module_id = $module_id;
420 $this->_slug = $this->get_slug();
421 $this->_module_type = $this->get_module_type();
422
423 $this->_blog_id = is_multisite() ? get_current_blog_id() : null;
424
425 $this->_storage = FS_Storage::instance( $this->_module_type, $this->_slug );
426
427 // If not set or 24 hours have already passed from the last time it's set, set the last load timestamp to the current time.
428 if (
429 ! isset( $this->_storage->last_load_timestamp ) ||
430 $this->_storage->last_load_timestamp < ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC ) )
431 ) {
432 $this->_storage->last_load_timestamp = time();
433 }
434
435 $this->_cache = FS_Cache_Manager::get_manager( WP_FS___OPTION_PREFIX . "cache_{$module_id}" );
436
437 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $this->get_unique_affix(), WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
438
439 $this->_plugin_main_file_path = $this->_find_caller_plugin_file( $is_init, $main_file );
440 $this->_plugin_dir_path = plugin_dir_path( $this->_plugin_main_file_path );
441 $this->_plugin_basename = $this->get_plugin_basename();
442 $this->_free_plugin_basename = str_replace( '-premium/', '/', $this->_plugin_basename );
443
444 $this->_is_multisite_integrated = (
445 defined( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) &&
446 ( true === constant( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) )
447 );
448
449 $this->_is_network_active = (
450 is_multisite() &&
451 $this->_is_multisite_integrated &&
452 // Themes are always network activated, but the ACTUAL activation is per site.
453 $this->is_plugin() &&
454 (
455 is_plugin_active_for_network( $this->_plugin_basename ) ||
456 // Plugin network level activation or uninstall.
457 ( fs_is_network_admin() && is_plugin_inactive( $this->_plugin_basename ) )
458 )
459 );
460
461 $this->_storage->set_network_active(
462 $this->_is_network_active,
463 $this->is_delegated_connection()
464 );
465
466 if ( ! isset( $this->_storage->is_network_activated ) ) {
467 $this->_storage->is_network_activated = $this->_is_network_active;
468 }
469
470 if ( $this->_storage->is_network_activated != $this->_is_network_active ) {
471 // Update last activation level.
472 $this->_storage->is_network_activated = $this->_is_network_active;
473
474 $this->maybe_adjust_storage();
475 }
476
477 #region Migration
478
479 if ( is_multisite() ) {
480 /**
481 * If the install_timestamp exists on the site level but doesn't exist on the
482 * network level storage, it means that we need to process the storage with migration.
483 *
484 * The code in this `if` scope will only be executed once and only for the first site that will execute it because once we migrate the storage data, install_timestamp will be already set in the network level storage.
485 *
486 * @author Vova Feldman (@svovaf)
487 * @since 2.0.0
488 */
489 if ( false === $this->_storage->get( 'install_timestamp', false, true ) &&
490 false !== $this->_storage->get( 'install_timestamp', false, false )
491 ) {
492 // Initiate storage migration.
493 $this->_storage->migrate_to_network();
494
495 // Migrate module cache to network level storage.
496 $this->_cache->migrate_to_network();
497 }
498 }
499
500 #endregion
501
502 $base_name_split = explode( '/', $this->_plugin_basename );
503 $this->_plugin_dir_name = $base_name_split[0];
504
505 if ( $this->_logger->is_on() ) {
506 $this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
507 $this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
508 $this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
509 $this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
510 $this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
511 }
512
513 // Remember link between file to slug.
514 $this->store_file_slug_map();
515
516 // Store plugin's initial install timestamp.
517 if ( ! isset( $this->_storage->install_timestamp ) ) {
518 $this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
519 }
520
521 if ( ! is_object( $this->_plugin ) ) {
522 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
523 }
524
525 $this->_admin_notices = FS_Admin_Notices::instance(
526 $this->_slug . ( $this->is_theme() ? ':theme' : '' ),
527 /**
528 * Ensure that the admin notice will always have a title by using the stored plugin title if available and
529 * retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
530 *
531 * @author Leo Fajardo (@leorw)
532 * @since 1.2.2
533 */
534 ( is_object( $this->_plugin ) && isset( $this->_plugin->title ) ?
535 $this->_plugin->title :
536 $this->get_plugin_name()
537 ),
538 $this->get_unique_affix()
539 );
540
541 if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
542 fs_request_is_action( 'restart_freemius' )
543 ) {
544 FS_Api::clear_cache();
545 $this->_cache->clear();
546 }
547
548 $this->register_constructor_hooks();
549
550 /**
551 * Starting from version 2.0.0, `FS_Site` entities no longer have the `plan` property and have `plan_id`
552 * instead. This should be called before calling `_load_account()`, otherwise, `$this->_site` will not be
553 * loaded in `_load_account` for versions of SDK starting from 2.0.0.
554 *
555 * @author Leo Fajardo (@leorw)
556 */
557 self::migrate_install_plan_to_plan_id( $this->_storage );
558
559 $this->_load_account();
560
561 $this->_version_updates_handler();
562 }
563
564 /**
565 * @author Leo Fajardo (@leorw)
566 * @since 2.3.0
567 */
568 private function maybe_adjust_storage() {
569 $install_timestamp = null;
570 $prev_is_premium = null;
571
572 $options_to_update = array();
573
574 $is_network_admin = fs_is_network_admin();
575
576 $network_install_timestamp = $this->_storage->get( 'install_timestamp', null, true );
577
578 if ( ! $is_network_admin ) {
579 if ( is_null( $network_install_timestamp ) ) {
580 // Plugin was not network-activated before.
581 return;
582 }
583
584 if ( is_null( $this->_storage->get( 'install_timestamp', null, false ) ) ) {
585 // Set the `install_timestamp` only if it's not yet set.
586 $install_timestamp = $network_install_timestamp;
587 }
588
589 $prev_is_premium = $this->_storage->get( 'prev_is_premium', null, true );
590 } else {
591 $current_wp_user = self::_get_current_wp_user();
592 $current_fs_user = self::_get_user_by_email( $current_wp_user->user_email );
593 $network_user_info = array();
594
595 $skips_count = 0;
596
597 $sites = self::get_sites();
598 $sites_count = count( $sites );
599
600 $blog_id_2_install_map = array();
601
602 $is_first_non_ignored_blog = true;
603
604 foreach ( $sites as $site ) {
605 $blog_id = self::get_site_blog_id( $site );
606
607 $blog_install_timestamp = $this->_storage->get( 'install_timestamp', null, $blog_id );
608
609 if ( is_null( $blog_install_timestamp ) ) {
610 // Plugin has not been installed on this blog.
611 continue;
612 }
613
614 $is_earlier_install = (
615 ! is_null( $install_timestamp ) &&
616 $blog_install_timestamp < $install_timestamp
617 );
618
619 $install = $this->get_install_by_blog_id( $blog_id );
620
621 $update_network_user_info = false;
622
623 if ( ! is_object( $install ) ) {
624 if ( ! $this->_storage->get( 'is_anonymous', false, $blog_id ) ) {
625 // The opt-in decision (whether to skip or opt in) is yet to be made.
626 continue;
627 }
628
629 $skips_count ++;
630 } else {
631 $blog_id_2_install_map[ $blog_id ] = $install;
632
633 if ( empty( $network_user_info ) ) {
634 // Set the network user info for the 1st time. Choose any user information whether or not it is for the current WP user.
635 $update_network_user_info = true;
636 }
637
638 if ( ! $update_network_user_info &&
639 is_object( $current_fs_user ) &&
640 $network_user_info['user_id'] != $current_fs_user->id &&
641 $install->user_id == $current_fs_user->id
642 ) {
643 // If an install that is owned by the current WP user is found, use its user information instead.
644 $update_network_user_info = true;
645 }
646
647 if ( ! $update_network_user_info &&
648 $is_earlier_install &&
649 ( ! is_object( $current_fs_user ) || $current_fs_user->id == $install->user_id )
650 ) {
651 // Update to the earliest install info if there's no install found so far that is owned by the current WP user; OR only if the found install is owned by the current WP user.
652 $update_network_user_info = true;
653 }
654 }
655
656 if ( $update_network_user_info ) {
657 $network_user_info = array(
658 'user_id' => $install->user_id,
659 'blog_id' => $blog_id
660 );
661 }
662
663 $site_prev_is_premium = $this->_storage->get( 'prev_is_premium', null, $blog_id );
664
665 if ( $is_first_non_ignored_blog ) {
666 $prev_is_premium = $site_prev_is_premium;
667
668 if ( is_null( $network_install_timestamp ) ) {
669 $install_timestamp = $blog_install_timestamp;
670 }
671
672 $is_first_non_ignored_blog = false;
673
674 continue;
675 }
676
677 if ( ! is_null( $prev_is_premium ) && $prev_is_premium !== $site_prev_is_premium ) {
678 // If a different `$site_prev_is_premium` value is found, do not include the option in the collection of options to update.
679 $prev_is_premium = null;
680 }
681
682 if ( $is_earlier_install ) {
683 // If an earlier install timestamp is found.
684 $install_timestamp = $blog_install_timestamp;
685 }
686 }
687
688 $installs_count = count( $blog_id_2_install_map );
689
690 if ( $sites_count === ( $installs_count + $skips_count ) ) {
691 if ( ! empty( $network_user_info ) ) {
692 $options_to_update['network_user_id'] = $network_user_info['user_id'];
693 $options_to_update['network_install_blog_id'] = $network_user_info['blog_id'];
694
695 foreach ( $blog_id_2_install_map as $blog_id => $install ) {
696 if ( $install->user_id == $network_user_info['user_id'] ) {
697 continue;
698 }
699
700 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
701 }
702 }
703
704 if ( $sites_count === $skips_count ) {
705 /**
706 * Assume network-level skipping as the intended action if all actions identified were only
707 * skipping of the connection (i.e., no opt-ins and delegated connections so far).
708 */
709 $options_to_update['is_anonymous_ms'] = true;
710 } else if ( $sites_count === $installs_count ) {
711 /**
712 * Assume network-level opt-in as the intended action if all actions identified were only opt-ins
713 * (i.e., no delegation and skipping of the connections so far).
714 */
715 $options_to_update['is_network_connected'] = true;
716 }
717 }
718 }
719
720 if ( ! is_null( $install_timestamp ) ) {
721 $options_to_update['install_timestamp'] = $install_timestamp;
722 }
723
724 if ( ! is_null( $prev_is_premium ) ) {
725 $options_to_update['prev_is_premium'] = $prev_is_premium;
726 }
727
728 if ( ! empty( $options_to_update ) ) {
729 $this->adjust_storage( $options_to_update, $is_network_admin );
730 }
731 }
732
733 /**
734 * @author Leo Fajardo (@leorw)
735 * @since 2.3.0
736 *
737 * @param array $options
738 * @param bool $is_network_admin
739 */
740 private function adjust_storage( $options, $is_network_admin ) {
741 foreach ( $options as $name => $value ) {
742 $this->_storage->store( $name, $value, $is_network_admin ? true : null );
743 }
744 }
745
746 /**
747 * Checks whether this module has a settings menu.
748 *
749 * @author Leo Fajardo (@leorw)
750 * @since 1.2.2
751 *
752 * @return bool
753 */
754 function has_settings_menu() {
755 return ( $this->_is_network_active && fs_is_network_admin() ) ?
756 $this->_menu->has_network_menu() :
757 $this->_menu->has_menu();
758 }
759
760 /**
761 * If `true` the opt-in should be shown as a modal dialog box on the themes.php page. WordPress.org themes guidelines prohibit from redirecting the user from the themes.php page after activating a theme.
762 *
763 * @author Vova Feldman (@svovaf)
764 * @since 2.4.5
765 *
766 * @return bool
767 */
768 function show_opt_in_on_themes_page() {
769 if ( ! $this->is_free_wp_org_theme() ) {
770 return false;
771 }
772
773 if ( ! $this->has_settings_menu() ) {
774 return true;
775 }
776
777 return $this->show_settings_with_tabs();
778 }
779
780 /**
781 * If `true` the opt-in should be shown on the product's main setting page.
782 *
783 * @author Vova Feldman (@svovaf)
784 * @since 2.4.5
785 *
786 * @return bool
787 *
788 * @uses show_opt_in_on_themes_page();
789 */
790 function show_opt_in_on_setting_page() {
791 return ! $this->show_opt_in_on_themes_page();
792 }
793
794 /**
795 * If `true` the settings should be shown using tabs.
796 *
797 * @author Vova Feldman (@svovaf)
798 * @since 2.4.5
799 *
800 * @return bool
801 */
802 function show_settings_with_tabs() {
803 return ( self::NAVIGATION_TABS === $this->_navigation );
804 }
805
806 /**
807 * Check if the context module is free wp.org theme.
808 *
809 * This method is helpful because:
810 * 1. wp.org themes are limited to a single submenu item,
811 * and sub-submenu items are most likely not allowed (never verified).
812 * 2. wp.org themes are not allowed to redirect the user
813 * after the theme activation, therefore, the agreed UX
814 * is showing the opt-in as a modal dialog box after
815 * activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
816 *
817 * @author Vova Feldman (@svovaf)
818 * @since 1.2.2.7
819 *
820 * @return bool
821 */
822 function is_free_wp_org_theme() {
823 return (
824 $this->is_theme() &&
825 $this->is_org_repo_compliant() &&
826 ! $this->is_premium()
827 );
828 }
829
830 /**
831 * Checks whether this a submenu item is visible.
832 *
833 * @author Vova Feldman (@svovaf)
834 * @since 1.2.2.6
835 * @since 1.2.2.7 Even if the menu item was specified to be hidden, when it is the context page, then show the submenu item so the user will have the right context page.
836 *
837 * @param string $slug
838 * @param bool $is_tabs_visibility_check This is used to decide if the associated tab should be shown or hidden.
839 *
840 * @return bool
841 */
842 function is_submenu_item_visible( $slug, $is_tabs_visibility_check = false ) {
843 if ( $this->is_admin_page( $slug ) ) {
844 /**
845 * It is the current context page, so show the submenu item
846 * so the user will have the right context page, even if it
847 * was set to hidden.
848 */
849 return true;
850 }
851
852 if ( ! $this->has_settings_menu() ) {
853 // No menu settings at all.
854 return false;
855 }
856
857 if (
858 ! $is_tabs_visibility_check &&
859 $this->is_org_repo_compliant() &&
860 $this->show_settings_with_tabs()
861 ) {
862 /**
863 * wp.org themes are limited to a single submenu item, and
864 * sub-submenu items are most likely not allowed (never verified).
865 */
866 return false;
867 }
868
869 return $this->_menu->is_submenu_item_visible( $slug );
870 }
871
872 /**
873 * Check if a Freemius page should be accessible via the UI.
874 *
875 * @author Vova Feldman (@svovaf)
876 * @since 1.2.2.7
877 *
878 * @param string $slug
879 *
880 * @return bool
881 */
882 function is_page_visible( $slug ) {
883 if ( $this->is_admin_page( $slug ) ) {
884 return true;
885 }
886
887 return $this->_menu->is_submenu_item_visible( $slug, true, true );
888 }
889
890 /**
891 * @author Vova Feldman (@svovaf)
892 * @since 1.0.9
893 */
894 private function _version_updates_handler() {
895 if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
896 // Freemius version upgrade mode.
897 $this->_storage->sdk_last_version = $this->_storage->sdk_version;
898 $this->_storage->sdk_version = $this->version;
899
900 if ( empty( $this->_storage->sdk_last_version ) ||
901 version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
902 ) {
903 $this->_storage->sdk_upgrade_mode = true;
904 $this->_storage->sdk_downgrade_mode = false;
905 } else {
906 $this->_storage->sdk_downgrade_mode = true;
907 $this->_storage->sdk_upgrade_mode = false;
908
909 }
910
911 $this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
912 }
913
914 $plugin_version = $this->get_plugin_version();
915 if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
916 // Plugin version upgrade mode.
917 $this->_storage->plugin_last_version = $this->_storage->plugin_version;
918 $this->_storage->plugin_version = $plugin_version;
919
920 if ( empty( $this->_storage->plugin_last_version ) ||
921 version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
922 ) {
923 $this->_storage->plugin_upgrade_mode = true;
924 $this->_storage->plugin_downgrade_mode = false;
925 } else {
926 $this->_storage->plugin_downgrade_mode = true;
927 $this->_storage->plugin_upgrade_mode = false;
928 }
929
930 if ( ! empty( $this->_storage->plugin_last_version ) ) {
931 // Different version of the plugin was installed before, therefore it's an update.
932 $this->_storage->is_plugin_new_install = false;
933 }
934
935 $this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
936 }
937 }
938
939 #--------------------------------------------------------------------------------
940 #region Data Migration on SDK Update
941 #--------------------------------------------------------------------------------
942
943 /**
944 * @author Vova Feldman (@svovaf)
945 * @since 1.1.5
946 *
947 * @param string $sdk_prev_version
948 * @param string $sdk_version
949 */
950 function _sdk_version_update( $sdk_prev_version, $sdk_version ) {
951 if ( empty( $sdk_prev_version ) ) {
952 return;
953 }
954
955 if (
956 version_compare( $sdk_prev_version, '2.5.1', '<' ) &&
957 version_compare( $sdk_version, '2.5.1', '>=' )
958 ) {
959 if ( $this->is_registered( true ) ) {
960 /**
961 * Migrate to new permissions layer.
962 */
963 require_once WP_FS__DIR_INCLUDES . '/supplements/fs-migration-2.5.1.php';
964
965 $install_by_blog_id = is_multisite() ?
966 $this->get_blog_install_map() :
967 array( 0 => $this->_site );
968
969 fs_migrate_251( $this, $install_by_blog_id );
970 }
971 }
972 }
973
974 /**
975 * @author Leo Fajardo (@leorw)
976 * @since 2.0.0
977 *
978 * @param \FS_Storage $storage
979 * @param bool|int|null $blog_id
980 */
981 private static function migrate_install_plan_to_plan_id( FS_Storage $storage, $blog_id = null ) {
982 if ( empty( $storage->sdk_version ) ) {
983 // New installation of the plugin, no need to upgrade.
984 return;
985 }
986
987 if ( ! version_compare( $storage->sdk_version, '2.0.0', '<' ) ) {
988 // Previous version is >= 2.0.0, so no need to migrate.
989 return;
990 }
991
992 // Alias.
993 $module_type = $storage->get_module_type();
994 $module_slug = $storage->get_module_slug();
995
996 $installs = self::get_all_sites( $module_type, $blog_id );
997 $install = isset( $installs[ $module_slug ] ) ? $installs[ $module_slug ] : null;
998
999 if ( ! is_object( $install ) ) {
1000 return;
1001 }
1002
1003 if ( isset( $install->plan ) && is_object( $install->plan ) ) {
1004 if ( isset( $install->plan->id ) && ! empty( $install->plan->id ) ) {
1005 $install->plan_id = self::_decrypt( $install->plan->id );
1006 }
1007
1008 unset( $install->plan );
1009
1010 $installs[ $module_slug ] = clone $install;
1011
1012 self::set_account_option_by_module(
1013 $module_type,
1014 'sites',
1015 $installs,
1016 true,
1017 $blog_id
1018 );
1019 }
1020 }
1021
1022 /**
1023 * @author Vova Feldman (@svovaf)
1024 * @since 1.2.2.7
1025 *
1026 * @param string $plugin_prev_version
1027 * @param string $plugin_version
1028 */
1029 function _after_version_update( $plugin_prev_version, $plugin_version ) {
1030 if ( $this->is_theme() ) {
1031 // Expire the cache of the previous tabs since the theme may
1032 // have setting updates.
1033 $this->_cache->expire( 'tabs' );
1034 $this->_cache->expire( 'tabs_stylesheets' );
1035 }
1036 }
1037
1038 /**
1039 * A special migration logic for the $_accounts, executed for all the plugins in the system:
1040 * - Moves some data to the network level storage.
1041 * - If the plugin's connection was skipped for all sites, set the plugin as if it was network skipped.
1042 * - If the plugin's connection was ignored for all sites, don't do anything in terms of the network connection.
1043 * - If the plugin was connected to all sites by the same super-admin, set the plugin as if was network opted-in for all sites.
1044 * - If there's at least one site that was connected by a super-admin, find the "main super-admin" (the one that installed the majority of the plugin installs) and set the plugin as if was network activated with the main super-admin, set all the sites that were skipped or opted-in with a different user to delegated mode. Then, prompt the currently logged super-admin to choose what to do with the ignored sites.
1045 * - If there are any sites in the network which the connection decision was not yet taken for, set this plugin into network activation mode so a super-admin can choose what to do with the rest of the sites.
1046 *
1047 * @author Vova Feldman (@svovaf)
1048 * @since 2.0.0
1049 */
1050 private static function migrate_accounts_to_network() {
1051 $sites = self::get_sites();
1052 $sites_count = count( $sites );
1053 $connection_status = array();
1054 $plugin_slugs = array();
1055 foreach ( $sites as $site ) {
1056 $blog_id = self::get_site_blog_id( $site );
1057
1058 self::$_accounts->migrate_to_network( $blog_id );
1059
1060 /**
1061 * Build a list of all Freemius powered plugins slugs.
1062 */
1063 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array(), $blog_id );
1064 foreach ( $id_slug_type_path_map as $module_id => $data ) {
1065 if ( WP_FS__MODULE_TYPE_PLUGIN === $data['type'] ) {
1066 $plugin_slugs[ $data['slug'] ] = true;
1067 }
1068 }
1069
1070 $installs = self::get_account_option( 'sites', WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
1071
1072 if ( is_array( $installs ) ) {
1073 foreach ( $installs as $slug => $install ) {
1074 if ( ! isset( $connection_status[ $slug ] ) ) {
1075 $connection_status[ $slug ] = array();
1076 }
1077
1078 if ( is_object( $install ) &&
1079 FS_Site::is_valid_id( $install->id ) &&
1080 FS_User::is_valid_id( $install->user_id )
1081 ) {
1082 $connection_status[ $slug ][ $blog_id ] = $install->user_id;
1083 }
1084 }
1085 }
1086 }
1087
1088 foreach ( $plugin_slugs as $slug => $true ) {
1089 if ( ! isset( $connection_status[ $slug ] ) ) {
1090 $connection_status[ $slug ] = array();
1091 }
1092
1093 foreach ( $sites as $site ) {
1094 $blog_id = self::get_site_blog_id( $site );
1095
1096 if ( isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1097 continue;
1098 }
1099
1100 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1101
1102 $is_anonymous = $storage->get( 'is_anonymous', null, $blog_id );
1103
1104 if ( ! is_null( $is_anonymous ) ) {
1105 // Since 1.1.3 is_anonymous is an array.
1106 if ( is_array( $is_anonymous ) && isset( $is_anonymous['is'] ) ) {
1107 $is_anonymous = $is_anonymous['is'];
1108 }
1109
1110 if ( is_bool( $is_anonymous ) && true === $is_anonymous ) {
1111 $connection_status[ $slug ][ $blog_id ] = 'skipped';
1112 }
1113 }
1114
1115 if ( ! isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1116 $connection_status[ $slug ][ $blog_id ] = 'ignored';
1117 }
1118 }
1119 }
1120
1121 $super_admins = array();
1122
1123 foreach ( $connection_status as $slug => $blogs_status ) {
1124 $skips = 0;
1125 $ignores = 0;
1126 $connections = 0;
1127 $opted_in_users = array();
1128 $opted_in_super_admins = array();
1129
1130 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1131
1132 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1133 if ( 'skipped' === $status_or_user_id ) {
1134 $skips ++;
1135 } else if ( 'ignored' === $status_or_user_id ) {
1136 $ignores ++;
1137 } else if ( FS_User::is_valid_id( $status_or_user_id ) ) {
1138 $connections ++;
1139
1140 if ( ! isset( $opted_in_users[ $status_or_user_id ] ) ) {
1141 $opted_in_users[ $status_or_user_id ] = array();
1142 }
1143
1144 $opted_in_users[ $status_or_user_id ][] = $blog_id;
1145
1146 if ( isset( $super_admins[ $status_or_user_id ] ) ||
1147 self::is_super_admin( $status_or_user_id )
1148 ) {
1149 // Cache super-admin data.
1150 $super_admins[ $status_or_user_id ] = true;
1151
1152 // Remember opted-in super-admins for the plugin.
1153 $opted_in_super_admins[ $status_or_user_id ] = true;
1154 }
1155 }
1156 }
1157
1158 $main_super_admin_user_id = null;
1159 $all_migrated = false;
1160 if ( $sites_count == $skips ) {
1161 // All sites were skipped -> network skip by copying the anonymous mode from any of the sites.
1162 $storage->is_anonymous_ms = $storage->is_anonymous;
1163
1164 $all_migrated = true;
1165 } else if ( $sites_count == $ignores ) {
1166 // Don't do anything, still in activation mode.
1167
1168 $all_migrated = true;
1169 } else if ( 0 < count( $opted_in_super_admins ) ) {
1170 // Find the super-admin with the majority of installs.
1171 $max_installs_by_super_admin = 0;
1172 foreach ( $opted_in_super_admins as $user_id => $true ) {
1173 $installs_count = count( $opted_in_users[ $user_id ] );
1174
1175 if ( $installs_count > $max_installs_by_super_admin ) {
1176 $max_installs_by_super_admin = $installs_count;
1177 $main_super_admin_user_id = $user_id;
1178 }
1179 }
1180
1181 if ( $sites_count == $connections && 1 == count( $opted_in_super_admins ) ) {
1182 // Super-admin opted-in for all sites in the network.
1183 $storage->is_network_connected = true;
1184
1185 $all_migrated = true;
1186 }
1187
1188 // Store network user.
1189 $storage->network_user_id = $main_super_admin_user_id;
1190
1191 $storage->network_install_blog_id = ( $sites_count == $connections ) ?
1192 // Since all sites are opted-in, associating with the main site.
1193 get_current_blog_id() :
1194 // Associating with the 1st found opted-in site.
1195 $opted_in_users[ $main_super_admin_user_id ][0];
1196
1197 /**
1198 * Make sure we migrate the plan ID of the network install, otherwise, if after the migration
1199 * the 1st page that will be loaded is the network level WP Admin and $storage->network_install_blog_id
1200 * is different than the main site of the network, the $this->_site will not be set since the plan_id
1201 * will be empty.
1202 */
1203 $storage->migrate_to_network();
1204 self::migrate_install_plan_to_plan_id( $storage, $storage->network_install_blog_id );
1205 } else {
1206 // At least one opt-in. All the opt-in were created by a non-super-admin.
1207 if ( 0 == $ignores ) {
1208 // All sites were opted-in or skipped, all by non-super-admin. So delegate all.
1209 $storage->store( 'is_delegated_connection', true, true );
1210
1211 $all_migrated = true;
1212 }
1213 }
1214
1215 if ( ! $all_migrated ) {
1216 /**
1217 * Delegate all sites that were:
1218 * 1) Opted-in by a user that is NOT the main-super-admin.
1219 * 2) Skipped and non of the sites was opted-in by a super-admin. If any site was opted-in by a super-admin, there will be a main-super-admin, and we consider the skip as if it was done by that user.
1220 */
1221 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1222 if ( $status_or_user_id == $main_super_admin_user_id ) {
1223 continue;
1224 }
1225
1226 if ( FS_User::is_valid_id( $status_or_user_id ) ||
1227 ( 'skipped' === $status_or_user_id && is_null( $main_super_admin_user_id ) )
1228 ) {
1229 $storage->store( 'is_delegated_connection', true, $blog_id );
1230 }
1231 }
1232 }
1233
1234
1235 if ( ( $connections + $skips > 0 ) ) {
1236 if ( $ignores > 0 ) {
1237 /**
1238 * If admin already opted-in or skipped in any of the network sites, and also
1239 * have sites which the connection decision was not yet taken, set this plugin
1240 * into network activation mode so the super-admin can choose what to do with
1241 * the rest of the sites.
1242 */
1243 self::set_network_upgrade_mode( $storage );
1244 }
1245 }
1246 }
1247 }
1248
1249 /**
1250 * Set a module into network upgrade mode.
1251 *
1252 * @author Vova Feldman (@svovaf)
1253 * @since 2.0.0
1254 *
1255 * @param \FS_Storage $storage
1256 *
1257 * @return bool
1258 */
1259 public static function set_network_upgrade_mode( FS_Storage $storage ) {
1260 return $storage->is_network_activation = true;
1261 }
1262
1263 /**
1264 * Will return true after upgrading to the SDK with the network level integration,
1265 * when the super-admin involvement is required regarding the rest of the sites.
1266 *
1267 * @author Vova Feldman (@svovaf)
1268 * @since 2.0.0
1269 *
1270 * @return bool
1271 */
1272 function is_network_upgrade_mode() {
1273 return $this->_storage->get( 'is_network_activation' );
1274 }
1275
1276 /**
1277 * Clear flag after the upgrade mode completion.
1278 *
1279 * @author Vova Feldman (@svovaf)
1280 * @since 2.0.0
1281 *
1282 * @return bool True if network activation was on and now completed.
1283 */
1284 private function network_upgrade_mode_completed() {
1285 if ( fs_is_network_admin() && $this->is_network_upgrade_mode() ) {
1286 $this->_storage->remove( 'is_network_activation' );
1287
1288 return true;
1289 }
1290
1291 return false;
1292 }
1293
1294 #endregion
1295
1296 /**
1297 * This action is connected to the 'plugins_loaded' hook and helps to determine
1298 * if this is a new plugin installation or a plugin update.
1299 *
1300 * There are 3 different use-cases:
1301 * 1) New plugin installation right with Freemius:
1302 * 1.1 _activate_plugin_event_hook() will be executed first
1303 * 1.2 Since $this->_storage->is_plugin_new_install is not set,
1304 * and $this->_storage->plugin_last_version is not set,
1305 * $this->_storage->is_plugin_new_install will be set to TRUE.
1306 * 1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1307 * be already set to TRUE.
1308 *
1309 * 2) Plugin update, didn't have Freemius before, and now have the SDK:
1310 * 2.1 _activate_plugin_event_hook() will not be executed, because
1311 * the activation hook do NOT fires on updates since WP 3.1.
1312 * 2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1313 * be empty, therefore, it will be set to FALSE.
1314 *
1315 * 3) Plugin update, had Freemius in prev version as well:
1316 * 3.1 _version_updates_handler() will be executed 1st, since FS was installed
1317 * before, $this->_storage->plugin_last_version will NOT be empty,
1318 * therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
1319 * 3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
1320 * already set, therefore, it will not be modified.
1321 *
1322 * Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
1323 *
1324 * NOTE:
1325 * The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
1326 * and then, the next immediate PageView is the plugin's main settings page, it will not
1327 * show the opt-in right away. The reason it will happen is because Freemius execution
1328 * will be turned off till the plugin is fully loaded at least once
1329 * (till $this->_storage->was_plugin_loaded is TRUE).
1330 *
1331 * @author Vova Feldman (@svovaf)
1332 * @since 1.1.9
1333 *
1334 */
1335 function _plugins_loaded() {
1336 // Update flag that plugin was loaded with Freemius at least once.
1337 $this->_storage->was_plugin_loaded = true;
1338
1339 /**
1340 * Bug fix - only set to false when it's a plugin, due to the
1341 * execution sequence of the theme hooks and our methods, if
1342 * this will be set for themes, Freemius will always assume
1343 * it's a theme update.
1344 *
1345 * @author Vova Feldman (@svovaf)
1346 * @since 1.2.2.2
1347 */
1348 if ( $this->is_plugin() &&
1349 ! isset( $this->_storage->is_plugin_new_install )
1350 ) {
1351 $this->_storage->is_plugin_new_install = (
1352 ! is_plugin_active( $this->_plugin_basename ) &&
1353 empty( $this->_storage->plugin_last_version )
1354 );
1355 }
1356 }
1357
1358 function _run_garbage_collector() {
1359 if ( true !== fs_get_optional_constant( 'WP_FS__ENABLE_GARBAGE_COLLECTOR', true ) ) {
1360 return;
1361 }
1362
1363 if ( ! $this->is_user_in_admin() ) {
1364 return;
1365 }
1366
1367 require_once WP_FS__DIR_INCLUDES . '/class-fs-lock.php';
1368
1369 $lock = new FS_Lock( 'garbage_collection' );
1370
1371 if ( $lock->is_locked() ) {
1372 return;
1373 }
1374
1375 // Create a 1-day lock.
1376 $lock->lock( WP_FS__TIME_24_HOURS_IN_SEC );
1377
1378 FS_Garbage_Collector::instance()->clean();
1379 }
1380
1381 /**
1382 * Modifies all external links in the submenu by altering their href, and also opens them in new tab if needed.
1383 *
1384 * @author Vova Feldman (@svovaf)
1385 * @author Swashata Ghosh (@swashata)
1386 * @since 2.1.4
1387 */
1388 static function _handle_submenu_external_link() {
1389 ?>
1390 <script type="text/javascript">
1391 (function ( $ ) {
1392 $( '.fs-submenu-item' ).each( function () {
1393 var $this = $( this ),
1394 $parent = $this.parent(),
1395 externalLink = $this.data( 'fs-external-url' ),
1396 isOpensInNewTab = $this.data( 'fs-new-tab' );
1397
1398 if ( externalLink ) {
1399 $parent.attr( 'href', externalLink );
1400 }
1401
1402 if ( isOpensInNewTab ) {
1403 $parent.attr( { target: '_blank', rel: 'noopener noreferrer' } );
1404 }
1405 } );
1406 } )( jQuery );
1407 </script>
1408 <?php
1409 }
1410
1411 /**
1412 * @author Vova Feldman (@svovaf)
1413 * @since 1.0.9
1414 */
1415 private function register_constructor_hooks() {
1416 $this->_logger->entrance();
1417
1418 if ( is_admin() ) {
1419 add_action( 'admin_init', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
1420
1421 if ( $this->is_plugin() ) {
1422 if ( self::is_plugin_install_page() && true !== fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) {
1423 /**
1424 * Unless the `fs_allow_updater_and_dialog` URL param exists and its value is `true`, make
1425 * Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php)
1426 * so that they won't interfere with the .org plugins' functionalities on that page (e.g.
1427 * updating of a .org plugin).
1428 */
1429 add_filter( 'site_transient_update_plugins', array( 'Freemius', '_remove_fs_updates_from_plugin_install_page' ), 10, 2 );
1430 } else if ( self::is_plugins_page() || self::is_updates_page() ) {
1431 /**
1432 * On the "Plugins" and "Updates" admin pages, if there are premium or non–org-compliant plugins, modify their details dialog URLs (add a Freemius-specific param) so that the SDK can determine if the plugin information dialog should show information from Freemius.
1433 *
1434 * @author Leo Fajardo (@leorw)
1435 * @since 2.2.3
1436 */
1437 add_action( 'admin_footer', array( 'Freemius', '_prepend_fs_allow_updater_and_dialog_flag_url_param' ) );
1438 }
1439
1440 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
1441
1442 /**
1443 * @since 1.2.2
1444 *
1445 * Hook to both free and premium version activations to support
1446 * auto deactivation on the other version activation.
1447 */
1448 register_activation_hook(
1449 $plugin_dir . $this->_free_plugin_basename,
1450 array( &$this, '_activate_plugin_event_hook' )
1451 );
1452
1453 register_activation_hook(
1454 $plugin_dir . $this->premium_plugin_basename(),
1455 array( &$this, '_activate_plugin_event_hook' )
1456 );
1457 } else {
1458 add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
1459
1460 add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
1461 }
1462
1463 /**
1464 * Part of the mechanism to identify new plugin install vs. plugin update.
1465 *
1466 * @author Vova Feldman (@svovaf)
1467 * @since 1.1.9
1468 */
1469 if ( empty( $this->_storage->was_plugin_loaded ) ) {
1470 /**
1471 * During the plugin activation (not theme), 'plugins_loaded' will be already executed
1472 * when the logic gets here since the activation logic first add the activate plugins,
1473 * then triggers 'plugins_loaded', and only then include the code of the plugin that
1474 * is activated. Which means that _plugins_loaded() will NOT be executed during the
1475 * plugin activation, and that IS intentional.
1476 *
1477 * @author Vova Feldman (@svovaf)
1478 */
1479 if ( $this->is_plugin() &&
1480 $this->is_activation_mode( false ) &&
1481 0 == did_action( 'plugins_loaded' )
1482 ) {
1483 add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
1484 } else {
1485 // If was activated before, then it was already loaded before.
1486 $this->_plugins_loaded();
1487 }
1488 }
1489
1490 add_action( 'plugins_loaded', array( &$this, '_run_garbage_collector' ) );
1491
1492 if ( ! self::is_ajax() ) {
1493 if ( ! $this->is_addon() ) {
1494 add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
1495 }
1496 }
1497
1498 if ( $this->_storage->handle_gdpr_admin_notice ) {
1499 add_action( 'init', array( &$this, '_maybe_show_gdpr_admin_notice' ) );
1500 }
1501
1502 add_action( 'init', array( &$this, '_maybe_add_gdpr_optin_ajax_handler') );
1503 add_action( 'init', array( &$this, '_add_pricing_ajax_handler' ) );
1504 }
1505
1506 if ( $this->is_plugin() ) {
1507 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1508 add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
1509 } else {
1510 add_action( 'wp_initialize_site', array( $this, '_after_wp_initialize_site_callback' ), 11, 2 );
1511 }
1512
1513 register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
1514 }
1515
1516 if ( is_multisite() ) {
1517 add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
1518 add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) );
1519 add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) );
1520
1521 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1522 add_action( 'deleted_blog', array( $this, '_after_site_deleted_callback' ), 10, 2 );
1523 } else {
1524 add_action( 'wp_delete_site', array( $this, '_after_wpsite_deleted_callback' ) );
1525 }
1526
1527 add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) );
1528 add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) );
1529 add_action( 'make_ham_blog', array( &$this, '_after_site_reactivated_callback' ) );
1530 }
1531
1532 if ( $this->is_theme() &&
1533 self::is_customizer() &&
1534 $this->apply_filters( 'show_customizer_upsell', true )
1535 ) {
1536 // Register customizer upsell.
1537 add_action( 'customize_register', array( &$this, '_customizer_register' ) );
1538 }
1539
1540 add_action( 'admin_init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
1541
1542 if ( $this->is_theme() && ! $this->is_migration() ) {
1543 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1544 }
1545
1546 add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
1547 add_action( 'admin_init', array( &$this, '_add_premium_version_upgrade_selection' ) );
1548 add_action( 'admin_init', array( &$this, '_add_beta_mode_update_handler' ) );
1549 add_action( 'admin_init', array( &$this, '_add_user_change_option' ) );
1550 add_action( 'admin_init', array( &$this, '_add_email_address_update_option' ) );
1551
1552 $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
1553 $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
1554 $this->add_ajax_action( 'set_data_debug_mode', array( &$this, '_set_data_debug_mode' ) );
1555 $this->add_ajax_action( 'toggle_whitelabel_mode', array( &$this, '_toggle_whitelabel_mode_ajax_handler' ) );
1556
1557 if ( $this->_is_network_active && fs_is_network_admin() ) {
1558 $this->add_ajax_action( 'network_activate', array( &$this, '_network_activate_ajax_action' ) );
1559 }
1560
1561 $this->add_ajax_action( 'install_premium_version', array(
1562 &$this,
1563 '_install_premium_version_ajax_action'
1564 ) );
1565
1566 $this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
1567
1568 $this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
1569
1570 $this->add_action( 'sdk_version_update', array( &$this, '_sdk_version_update' ), WP_FS__DEFAULT_PRIORITY, 2 );
1571
1572 $this->add_action(
1573 'plugin_version_update',
1574 array( &$this, '_after_version_update' ),
1575 WP_FS__DEFAULT_PRIORITY,
1576 2
1577 );
1578 $this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
1579
1580 add_action( 'admin_init', array( &$this, '_add_trial_notice' ) ); // @phpstan-ignore-line
1581 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) ); // @phpstan-ignore-line
1582 add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) );
1583
1584 /**
1585 * Handle request to reset anonymous mode for `get_reconnect_url()` or reset the pending activation mode.
1586 *
1587 * @author Vova Feldman (@svovaf)
1588 * @since 1.2.1.5
1589 */
1590 if (
1591 (
1592 fs_request_is_action( 'reset_anonymous_mode' ) ||
1593 fs_request_is_action( 'reset_pending_activation_mode' )
1594 ) &&
1595 $this->get_unique_affix() === fs_request_get_raw( 'fs_unique_affix' )
1596 ) {
1597 add_action( 'admin_init', array( &$this, 'connect_again' ) );
1598 }
1599
1600 FS_DebugManager::register_hooks();
1601 }
1602
1603 /**
1604 * Register the required hooks right after the settings parse is completed.
1605 *
1606 * @author Vova Feldman (@svovaf)
1607 * @since 2.3.1
1608 */
1609 private function register_after_settings_parse_hooks() {
1610 if ( is_admin() &&
1611 $this->is_theme() &&
1612 $this->is_premium() &&
1613 ! $this->has_active_valid_license()
1614 ) {
1615 $this->add_ajax_action(
1616 'delete_theme_update_data',
1617 array( &$this, '_delete_theme_update_data_action' )
1618 );
1619 }
1620
1621 if ( $this->show_settings_with_tabs() ) {
1622 /**
1623 * Include the required hooks to capture the theme settings' page tabs
1624 * and cache them.
1625 *
1626 * @author Vova Feldman (@svovaf)
1627 * @since 1.2.2.7
1628 */
1629 if ( ! $this->_cache->has_valid( 'tabs' ) ) {
1630 add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
1631 // Add license activation AJAX callback.
1632 $this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
1633
1634 add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
1635 }
1636
1637 add_action(
1638 'admin_footer',
1639 array( &$this, '_add_freemius_tabs' ),
1640 /**
1641 * The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
1642 * That's why the priority is 11 while the tabs capture logic is added
1643 * with priority 10.
1644 *
1645 * @author Vova Feldman (@svovaf)
1646 */
1647 11
1648 );
1649 }
1650
1651 if ( ! self::is_ajax() ) {
1652 if ( ! $this->is_addon() || $this->is_only_premium() ) {
1653 add_action(
1654 ( $this->_is_network_active && fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu',
1655 array( &$this, '_prepare_admin_menu' ),
1656 WP_FS__LOWEST_PRIORITY
1657 );
1658 }
1659 }
1660
1661 if (
1662 $this->is_user_in_admin() &&
1663 $this->is_parallel_activation() &&
1664 $this->_premium_plugin_basename !== $this->_premium_plugin_basename_from_parallel_activation
1665 ) {
1666 $this->_premium_plugin_basename = $this->_premium_plugin_basename_from_parallel_activation;
1667
1668 register_activation_hook(
1669 dirname( $this->_plugin_dir_path ) . '/' . $this->_premium_plugin_basename,
1670 array( &$this, '_activate_plugin_event_hook' )
1671 );
1672 }
1673 }
1674
1675 /**
1676 * Determines if a plugin is running in parallel activation mode.
1677 *
1678 * @author Leo Fajardo (@leorw)
1679 * @since 2.9.1
1680 *
1681 * @return bool
1682 */
1683 private function is_parallel_activation() {
1684 return ! empty( $this->_premium_plugin_basename_from_parallel_activation );
1685 }
1686
1687 /**
1688 * Makes Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php) so that
1689 * they won't interfere with the .org plugins' functionalities on that page (e.g. updating of a .org plugin).
1690 *
1691 * @author Leo Fajardo (@leorw)
1692 * @since 2.2.3
1693 *
1694 * @param object $updates
1695 * @param string|null $transient
1696 *
1697 * @return object
1698 */
1699 static function _remove_fs_updates_from_plugin_install_page( $updates, $transient = null ) {
1700 if ( is_object( $updates ) && isset( $updates->response ) ) {
1701 foreach ( $updates->response as $file => $plugin ) {
1702 if ( isset( $plugin->package ) && false !== strpos( $plugin->package, 'api.freemius' ) ) {
1703 unset( $updates->response[ $file ] );
1704 }
1705 }
1706 }
1707
1708 return $updates;
1709 }
1710
1711 /**
1712 * Prepends the `fs_allow_updater_and_dialog` param to the plugin information URLs to tell the SDK to handle
1713 * the information that is shown on the plugin details dialog that is shown when the relevant link is clicked.
1714 *
1715 * @author Leo Fajardo (@leorw)
1716 * @since 2.2.3
1717 *
1718 * @return void
1719 */
1720 static function _prepend_fs_allow_updater_and_dialog_flag_url_param() {
1721 $slug_basename_map = array();
1722 foreach ( self::$_instances as $instance ) {
1723 if ( ! $instance->is_plugin() ) {
1724 continue;
1725 }
1726
1727 $slug_basename_map[ $instance->get_slug() ] = $instance->premium_plugin_basename();
1728 }
1729 ?>
1730 <script type="text/javascript">
1731 (function( $ ) {
1732 var slugBasenameMap = <?php echo json_encode( $slug_basename_map ) ?>;
1733 for ( var slug in slugBasenameMap ) {
1734 var basename = slugBasenameMap[ slug ];
1735
1736 // Try to get the plugin rows if on the "Plugins" page.
1737 var $pluginRows = $( '.wp-list-table.plugins tr[data-plugin="' + basename + '"]');
1738
1739 if ( 0 === $pluginRows.length ) {
1740 // Try to get the plugin rows if on the "Updates" page.
1741 var $pluginCheckbox = $( '#update-plugins-table input[type="checkbox"][value="' + basename + '"]' );
1742 if ( 0 !== $pluginCheckbox.length ) {
1743 $pluginRows = $pluginCheckbox.parents( 'tr:first' );
1744 }
1745 }
1746
1747 if ( 0 === $pluginRows.length ) {
1748 // No plugin rows found.
1749 continue;
1750 }
1751
1752 // Find the "View details" links and add the `fs_allow_updater_and_dialog` param to the URL.
1753 $pluginRows.find( 'a[href*="plugin-install.php?tab=plugin-information"]' ).each(function() {
1754 var $this = $( this ),
1755 href = $this.attr( 'href' ).replace( '?tab=', '?fs_allow_updater_and_dialog=true&tab=');
1756
1757 $this.attr( 'href', href );
1758 });
1759 }
1760 })( jQuery );
1761 </script>
1762 <?php
1763 }
1764
1765 /**
1766 * @author Leo Fajardo (@leorw)
1767 * @since 2.3.0
1768 */
1769 static function _maybe_add_beta_label_styles() {
1770 $has_any_beta_version = false;
1771
1772 foreach ( self::$_instances as $instance ) {
1773 if ( $instance->is_beta() ) {
1774 $has_any_beta_version = true;
1775 break;
1776 }
1777 }
1778
1779 if ( $has_any_beta_version ) {
1780 fs_enqueue_local_style( 'fs_plugins', '/admin/plugins.css' );
1781 }
1782 }
1783
1784 /**
1785 * @author Leo Fajardo (@leorw)
1786 * @since 2.3.0
1787 */
1788 static function _maybe_add_beta_label_to_plugins_and_handle_confirmation() {
1789 $beta_data = array();
1790
1791 foreach ( self::$_instances as $instance ) {
1792 if ( ! $instance->is_premium() ) {
1793 continue;
1794 }
1795
1796 /**
1797 * If there's an available beta version update, a confirmation message will be shown when the
1798 * "Update now" link on the "Plugins" or "Themes" page is clicked.
1799 */
1800 $has_beta_update = $instance->has_beta_update();
1801
1802 $is_beta = (
1803 // The "Beta" label is added separately for themes.
1804 $instance->is_plugin() &&
1805 $instance->is_beta()
1806 );
1807
1808 if ( ! $is_beta && ! $has_beta_update ) {
1809 continue;
1810 }
1811
1812 $beta_data[ $instance->get_plugin_basename() ] = array( 'is_installed_version_beta' => $is_beta );
1813
1814 if ( ! $has_beta_update ) {
1815 continue;
1816 }
1817
1818 $beta_data[ $instance->get_plugin_basename() ]['beta_version_update_confirmation_message'] = sprintf(
1819 '%s %s',
1820 sprintf(
1821 fs_esc_attr_inline(
1822 'An update to a Beta version will replace your installed version of %s with the latest Beta release - use with caution, and not on production sites. You have been warned.',
1823 'beta-version-update-caution',
1824 $instance->get_slug()
1825 ),
1826 $instance->get_plugin_title()
1827 ),
1828 fs_esc_attr_inline( 'Would you like to proceed with the update?', 'update-confirmation', $instance->get_slug() )
1829 );
1830 }
1831
1832 if ( empty( $beta_data ) ) {
1833 return;
1834 }
1835 ?>
1836 <script type="text/javascript">
1837 ( function( $ ) {
1838 var betaData = <?php echo json_encode( $beta_data ) ?>;
1839
1840 for ( var pluginBasename in betaData ) {
1841 if ( ! betaData.hasOwnProperty( pluginBasename ) ) {
1842 continue;
1843 }
1844
1845 if ( ! betaData[ pluginBasename ].is_installed_version_beta ) {
1846 continue;
1847 }
1848
1849 var $parentContainer = $( '.wp-list-table.plugins tr[data-plugin="' + pluginBasename + '"]' );
1850 if ( 0 === $parentContainer.length ) {
1851 continue;
1852 }
1853
1854 $parentContainer.find( '.plugin-title > strong:first-child').append(
1855 '<span class="fs-tag fs-info"><?php fs_esc_js_echo_inline( 'Beta', 'beta' ) ?></span>'
1856 );
1857 }
1858
1859 setTimeout( function() {
1860 // Wait a little bit before adding the event handler, otherwise, it will be overridden by the core WP logic.
1861 $( '.plugins .update-message .update-link, .themes .theme .update-message' ).on( 'click', function() {
1862 var $parentContainer = $( this ).parents( 'tr:first' );
1863 pluginBasename = ( 0 !== $parentContainer.length ) ?
1864 $parentContainer.data( 'plugin' ) :
1865 $( this ).parents( '.theme:first' ).data( 'slug' );
1866
1867 if (
1868 betaData[ pluginBasename ] &&
1869 betaData[ pluginBasename ].beta_version_update_confirmation_message &&
1870 ! confirm( betaData[ pluginBasename ].beta_version_update_confirmation_message )
1871 ) {
1872 return false;
1873 }
1874 } );
1875 }, 20 );
1876 } )( jQuery );
1877 </script>
1878 <?php
1879 }
1880
1881 /**
1882 * Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
1883 * could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
1884 * plugin will trigger inclusion of the free or premium version and if one of them is active during the
1885 * uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
1886 *
1887 * @author Leo Fajardo (@leorw)
1888 *
1889 * @since 1.2.0
1890 */
1891 private function unregister_uninstall_hook() {
1892 $uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
1893 unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
1894 unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
1895
1896 update_option( 'uninstall_plugins', $uninstallable_plugins );
1897 }
1898
1899 /**
1900 * @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
1901 *
1902 * @param bool $store_prev_path
1903 */
1904 private function clear_module_main_file_cache( $store_prev_path = true ) {
1905 if ( ! isset( $this->_storage->plugin_main_file ) ||
1906 empty( $this->_storage->plugin_main_file->path )
1907 ) {
1908 return;
1909 }
1910
1911 if ( ! $store_prev_path ) {
1912 /**
1913 * Storing the previous path is not needed when clearing the cache after an SDK version update since
1914 * the main purpose of the cache clearing in that event is to correct a wrong plugin main file path
1915 * which causes data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
1916 *
1917 * @author Leo Fajardo (@leorw)
1918 * @since 2.2.1
1919 */
1920 unset( $this->_storage->plugin_main_file->path );
1921 } else {
1922 $plugin_main_file = clone $this->_storage->plugin_main_file;
1923
1924 // Store cached path (2nd layer cache).
1925 $plugin_main_file->prev_path = $plugin_main_file->path;
1926
1927 // Clear cached path.
1928 unset( $plugin_main_file->path );
1929
1930 $this->_storage->plugin_main_file = $plugin_main_file;
1931 }
1932
1933 /**
1934 * Clear global cached path.
1935 *
1936 * @author Leo Fajardo (@leorw)
1937 * @since 1.2.2
1938 */
1939 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
1940 unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
1941 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
1942 }
1943
1944 /**
1945 * @author Leo Fajardo (@leorw)
1946 * @since 2.0.0
1947 */
1948 function _hook_action_links_and_register_account_hooks() {
1949 if ( $this->is_migration() ) {
1950 return;
1951 }
1952
1953 if (
1954 ( self::is_plugins_page() && $this->is_plugin() ) ||
1955 ( self::is_themes_page() && $this->is_theme() ) ||
1956 fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' )
1957 ) {
1958 $this->_add_tracking_links();
1959 }
1960
1961 if ( self::is_plugins_page() && $this->is_plugin() ) {
1962 $this->hook_plugin_action_links();
1963 }
1964
1965 $this->_register_account_hooks();
1966 }
1967
1968 /**
1969 * @author Vova Feldman (@svovaf)
1970 * @since 1.0.9
1971 */
1972 private function _register_account_hooks() {
1973 if ( ! is_admin() ) {
1974 return;
1975 }
1976
1977 /**
1978 * Always show the deactivation feedback form since we added
1979 * automatic free version deactivation upon premium code activation.
1980 *
1981 * @since 1.2.1.6
1982 */
1983 $this->add_ajax_action(
1984 'submit_uninstall_reason',
1985 array( &$this, '_submit_uninstall_reason_action' )
1986 );
1987
1988 $this->add_ajax_action(
1989 'cancel_subscription_or_trial',
1990 array( &$this, 'cancel_subscription_or_trial_ajax_action' )
1991 );
1992
1993 if ( ! $this->is_addon() || $this->is_parent_plugin_installed() ) {
1994 if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
1995 ( $this->is_theme() && self::is_themes_page() )
1996 ) {
1997 add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
1998 }
1999 }
2000 }
2001
2002 /**
2003 * Leverage backtrace to find caller plugin file path.
2004 *
2005 * @param bool $is_init Is initiation sequence.
2006 * @param string $main_file Since 2.5.0 expects the module's main file path to potentially purge the cached path.
2007 *
2008 * @return string
2009 * @since 1.0.6
2010 *
2011 * @author Vova Feldman (@svovaf)
2012 */
2013 private function _find_caller_plugin_file( $is_init = false, $main_file = '' ) {
2014 // Try to load the cached value of the file path.
2015 if ( isset( $this->_storage->plugin_main_file ) ) {
2016 $plugin_main_file = $this->_storage->plugin_main_file;
2017 if ( ! empty( $plugin_main_file->path ) ) {
2018 $absolute_path = $this->get_absolute_path( $plugin_main_file->path );
2019 if ( file_exists( $absolute_path ) ) {
2020 if ( $is_init && $absolute_path !== $this->get_absolute_path( $main_file ) ) {
2021 // Update cached path if not matching the actual path.
2022 $plugin_main_file->path = $main_file;
2023 $this->_storage->plugin_main_file = $plugin_main_file;
2024 }
2025
2026 return $absolute_path;
2027 }
2028 }
2029 }
2030
2031 /**
2032 * @since 1.2.1
2033 *
2034 * `clear_module_main_file_cache()` is clearing the plugin's cached path on
2035 * deactivation. Therefore, if any plugin/theme was initiating `Freemius`
2036 * with that plugin's slug, it was overriding the empty plugin path with a wrong path.
2037 *
2038 * So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
2039 * when the class instantiator isn't the module.
2040 */
2041 if ( ! $is_init ) {
2042 // Fetch prev path cache.
2043 if ( isset( $this->_storage->plugin_main_file ) &&
2044 ! empty( $this->_storage->plugin_main_file->prev_path )
2045 ) {
2046 $absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
2047 if ( file_exists( $absolute_path ) ) {
2048 return $absolute_path;
2049 }
2050 }
2051
2052 wp_die(
2053 $this->get_text_inline( 'Freemius SDK couldn\'t find the plugin\'s main file. Please contact 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 ] = $network_plugins[ $basename ];
8843 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
8844 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8845 ! isset( $site_active_plugins[ $basename ] )
8846 ) {
8847 // Plugin was site level deactivated.
8848 unset( $site_active_plugins_cache->plugins[ $basename ] );
8849 }
8850
8851 $prev_version = $data['version'];
8852 $current_version = $network_plugins[ $basename ]['Version'];
8853
8854 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
8855 // Plugin activated or deactivated, or version changed.
8856
8857 if ( $was_active !== $is_active ) {
8858 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
8859 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
8860 }
8861 }
8862
8863 if ( $prev_version !== $current_version ) {
8864 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
8865 }
8866
8867 $updated_plugin_data = $data;
8868 $updated_plugin_data['is_active'] = $is_active;
8869 $updated_plugin_data['version'] = $current_version;
8870 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
8871 $plugins_update_data[] = $updated_plugin_data;
8872 }
8873 }
8874
8875 // Find new plugins that weren't yet seen before.
8876 foreach ( $network_plugins as $basename => $data ) {
8877 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
8878 // New plugin.
8879 $new_plugin = array(
8880 'slug' => $data['slug'],
8881 'version' => $data['Version'],
8882 'title' => $data['Name'],
8883 'is_active' => $data['is_active'],
8884 'is_uninstalled' => false,
8885 );
8886
8887 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
8888
8889 $is_site_level_active = (
8890 isset( $site_active_plugins[ $basename ] ) &&
8891 $site_active_plugins[ $basename ]['is_active']
8892 );
8893
8894 /**
8895 * If not network active, set the activity status based on the site-level plugin status.
8896 */
8897 if ( ! $new_plugin['is_active'] ) {
8898 $new_plugin['is_active'] = $is_site_level_active;
8899 }
8900
8901 $plugins_update_data[] = $new_plugin;
8902
8903 if ( isset( $site_active_plugins[ $basename ] ) ) {
8904 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
8905 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
8906 }
8907 }
8908 }
8909
8910 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
8911 $site_active_plugins_cache->timestamp = $time;
8912 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8913
8914 $network_plugins_cache->md5 = $network_plugins_thumbprint;
8915 $network_plugins_cache->timestamp = $time;
8916 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
8917
8918 return $plugins_update_data;
8919 }
8920
8921 /**
8922 * Return a list of modified themes since the last sync.
8923 *
8924 * Note:
8925 * There's no point to store a themes counter since even if the number of
8926 * themes didn't change, we still need to check if the versions are all the
8927 * same and the activity state is similar.
8928 *
8929 * @author Vova Feldman (@svovaf)
8930 * @since 1.1.8
8931 *
8932 * @return array|false
8933 */
8934 private function get_themes_data_for_api() {
8935 // Alias.
8936 $option_name = 'all_themes';
8937
8938 $all_cached_themes = self::$_accounts->get_option( $option_name );
8939
8940 if ( ! is_object( $all_cached_themes ) ) {
8941 $all_cached_themes = (object) array(
8942 'timestamp' => '',
8943 'md5' => '',
8944 'themes' => array(),
8945 );
8946 }
8947
8948 $time = time();
8949
8950 if ( ! empty( $all_cached_themes->timestamp ) &&
8951 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8952 ) {
8953 // Don't send theme updates if last update was in the past 5 min.
8954 return false;
8955 }
8956
8957 // Write timestamp to lock the logic.
8958 $all_cached_themes->timestamp = $time;
8959 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
8960
8961 // Reload options from DB.
8962 self::$_accounts->load( true );
8963 $all_cached_themes = self::$_accounts->get_option( $option_name );
8964
8965 if ( $time != $all_cached_themes->timestamp ) {
8966 // If timestamp is different, then another thread captured the lock.
8967 return false;
8968 }
8969
8970 // Get active theme.
8971 $active_theme = wp_get_theme();
8972 $active_theme_stylesheet = $active_theme->get_stylesheet();
8973
8974 // Check if there's a change in themes.
8975 $all_themes = wp_get_themes();
8976
8977 // Check if themes changed.
8978 ksort( $all_themes );
8979
8980 $themes_signature = '';
8981 foreach ( $all_themes as $slug => $data ) {
8982 $is_active = ( $slug === $active_theme_stylesheet );
8983 $themes_signature .= $slug . ',' .
8984 $data->version . ',' .
8985 ( $is_active ? '1' : '0' ) . ';';
8986 }
8987
8988 // Check if themes status changed (version or active/inactive).
8989 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
8990
8991 $themes_update_data = array();
8992
8993 if ( $themes_changed ) {
8994 // Change in themes, report changes.
8995
8996 // Update existing themes info.
8997 foreach ( $all_cached_themes->themes as $slug => $data ) {
8998 $is_active = ( $slug === $active_theme_stylesheet );
8999
9000 if ( ! isset( $all_themes[ $slug ] ) ) {
9001 // Plugin uninstalled.
9002 $uninstalled_theme_data = $data;
9003 $uninstalled_theme_data['is_active'] = false;
9004 $uninstalled_theme_data['is_uninstalled'] = true;
9005 $themes_update_data[] = $uninstalled_theme_data;
9006
9007 unset( $all_themes[ $slug ] );
9008 unset( $all_cached_themes->themes[ $slug ] );
9009 } else if ( $data['is_active'] !== $is_active ||
9010 $data['version'] !== $all_themes[ $slug ]->version
9011 ) {
9012 // Plugin activated or deactivated, or version changed.
9013
9014 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
9015 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
9016
9017 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
9018 }
9019 }
9020
9021 // Find new themes that weren't yet seen before.
9022 foreach ( $all_themes as $slug => $data ) {
9023 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
9024 $is_active = ( $slug === $active_theme_stylesheet );
9025
9026 // New plugin.
9027 $new_plugin = array(
9028 'slug' => $slug,
9029 'version' => $data->version,
9030 'title' => $data->name,
9031 'is_active' => $is_active,
9032 'is_uninstalled' => false,
9033 );
9034
9035 $themes_update_data[] = $new_plugin;
9036 $all_cached_themes->themes[ $slug ] = $new_plugin;
9037 }
9038 }
9039
9040 $all_cached_themes->md5 = md5( $themes_signature );
9041 $all_cached_themes->timestamp = time();
9042 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9043 }
9044
9045 return $themes_update_data;
9046 }
9047
9048 /**
9049 * Get site data for API install request.
9050 *
9051 * @author Vova Feldman (@svovaf)
9052 * @since 1.1.2
9053 *
9054 * @param string[] $override
9055 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9056 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9057 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, title, and URL).
9058 *
9059 * @return array
9060 */
9061 private function get_install_data_for_api(
9062 array $override,
9063 $include_plugins = true,
9064 $include_themes = true,
9065 $include_blog_data = true
9066 ) {
9067 // Alias.
9068 $permissions = FS_Permission_Manager::instance( $this );
9069
9070 if ( $permissions->is_extensions_tracking_allowed() ) {
9071 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
9072 /**
9073 * @since 1.1.8 Also send plugin updates.
9074 */
9075 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9076 $plugins = $this->get_plugins_data_for_api();
9077 if ( ! empty( $plugins ) ) {
9078 $override['plugins'] = $plugins;
9079 }
9080 }
9081 }
9082
9083 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
9084 /**
9085 * @since 1.1.8 Also send themes updates.
9086 */
9087 if ( $include_themes && ! isset( $override['themes'] ) ) {
9088 $themes = $this->get_themes_data_for_api();
9089 if ( ! empty( $themes ) ) {
9090 $override['themes'] = $themes;
9091 }
9092 }
9093 }
9094 }
9095
9096 $versions = $this->get_versions();
9097
9098 $blog_data = array();
9099 if ( $include_blog_data ) {
9100 $blog_data['url'] = self::get_unfiltered_site_url();
9101
9102 if ( $permissions->is_diagnostic_tracking_allowed() ) {
9103 $blog_data = array_merge( $blog_data, array(
9104 'language' => self::get_sanitized_language(),
9105 'title' => get_bloginfo( 'name' ),
9106 ) );
9107 }
9108 }
9109
9110 return array_merge( $versions, $blog_data, array(
9111 'version' => $this->get_plugin_version(),
9112 'is_premium' => $this->is_premium(),
9113 // Special params.
9114 'is_active' => true,
9115 'is_uninstalled' => false,
9116 ), $override );
9117 }
9118
9119 /**
9120 * Update installs details.
9121 *
9122 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
9123 *
9124 * @author Vova Feldman (@svovaf)
9125 * @since 2.0.0
9126 *
9127 * @param string[] string $override
9128 * @param bool $only_diff
9129 * @param bool $is_keepalive
9130 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9131 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9132 *
9133 * @return array
9134 */
9135 private function get_installs_data_for_api(
9136 array $override,
9137 $only_diff = false,
9138 $is_keepalive = false,
9139 $include_plugins = true,
9140 $include_themes = true
9141 ) {
9142 /**
9143 * @since 1.1.8 Also send plugin updates.
9144 */
9145 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9146 // $plugins = $this->get_plugins_data_for_api();
9147 // if ( ! empty( $plugins ) ) {
9148 // $override['plugins'] = $plugins;
9149 // }
9150 // }
9151 /**
9152 * @since 1.1.8 Also send themes updates.
9153 */
9154 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9155 // $themes = $this->get_themes_data_for_api();
9156 // if ( ! empty( $themes ) ) {
9157 // $override['themes'] = $themes;
9158 // }
9159 // }
9160
9161 // Common properties.
9162 $versions = $this->get_versions();
9163 $common = array_merge( $versions, array(
9164 'version' => $this->get_plugin_version(),
9165 'is_premium' => $this->is_premium(),
9166 ), $override );
9167
9168
9169 $is_common_diff_for_any_site = false;
9170 $common_diff_union = array();
9171
9172 $installs_data = array();
9173
9174 $sites = self::get_sites();
9175
9176 $subsite_data_for_api_by_install_id = array();
9177 $install_url_by_install_id = array();
9178 $subsite_registration_date_by_install_id = array();
9179
9180 foreach ( $sites as $site ) {
9181 $blog_id = self::get_site_blog_id( $site );
9182
9183 $install = $this->get_install_by_blog_id( $blog_id );
9184
9185 if ( is_object( $install ) ) {
9186 if ( $install->user_id != $this->_user->id ) {
9187 // Install belongs to a different owner.
9188 continue;
9189 }
9190
9191 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
9192 // Don't send updates regarding opted-out installs.
9193 continue;
9194 }
9195
9196 $install_data = $this->get_site_info( $site, true );
9197
9198 if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $install_data['blog_id'] ) ) {
9199 continue;
9200 }
9201
9202 $uid = $install_data['uid'];
9203 $url = $install_data['url'];
9204 $registration_date = $install_data['registration_date'];
9205
9206 if ( isset( $subsite_data_for_api_by_install_id[ $install->id ] ) ) {
9207 $clone_subsite_data = $subsite_data_for_api_by_install_id[ $install->id ];
9208 $clone_install_url = $install_url_by_install_id[ $install->id ];
9209 $clone_subsite_registration_date = $subsite_registration_date_by_install_id[ $install->id ];
9210
9211 $skip = false;
9212
9213 if (
9214 ! empty( $install_data['registration_date'] ) &&
9215 ! empty( $clone_subsite_registration_date )
9216 ) {
9217 /**
9218 * 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.
9219 *
9220 * @author Leo Fajardo (@leorw)
9221 * @since 2.5.1
9222 */
9223 $skip = ( strtotime( $install_data['registration_date'] ) > strtotime( $clone_subsite_registration_date ) );
9224 } else if (
9225 /**
9226 * 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.
9227 *
9228 * @author Leo Fajardo (@leorw)
9229 * @since 2.5.0
9230 */
9231 fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) ||
9232 fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $url ) )
9233 ) {
9234 $skip = true;
9235 }
9236
9237 if ( $skip ) {
9238 // 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.
9239 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
9240 continue;
9241 }
9242 }
9243
9244 unset( $install_data['blog_id'] );
9245 unset( $install_data['uid'] );
9246 unset( $install_data['url'] );
9247 unset( $install_data['registration_date'] );
9248
9249 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9250 $install_data['is_uninstalled'] = $install->is_uninstalled;
9251
9252 $common_diff = null;
9253 $is_common_diff = false;
9254 if ( $only_diff ) {
9255 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9256 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9257
9258 $is_common_diff = ! empty( $common_diff );
9259
9260 if ( $is_common_diff ) {
9261 foreach ( $common_diff as $k => $v ) {
9262 if ( ! isset( $common_diff_union[ $k ] ) ) {
9263 $common_diff_union[ $k ] = $v;
9264 }
9265 }
9266 }
9267
9268 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9269 }
9270
9271 if ( ! empty( $install_data ) || $is_common_diff || $is_keepalive ) {
9272 // Add install ID and site unique ID.
9273 $install_data['id'] = $install->id;
9274 $install_data['uid'] = $uid;
9275 $install_data['url'] = $url;
9276
9277 $subsite_data_for_api_by_install_id[ $install->id ] = $install_data;
9278 $install_url_by_install_id[ $install->id ] = $install->url;
9279 $subsite_registration_date_by_install_id[ $install->id ] = $registration_date;
9280 }
9281 }
9282 }
9283
9284 restore_current_blog();
9285
9286 $installs_data = array_merge(
9287 $installs_data,
9288 array_values( $subsite_data_for_api_by_install_id )
9289 );
9290
9291 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9292 if ( ! $only_diff ) {
9293 $installs_data[] = $common;
9294 } else if ( ! empty( $common_diff_union ) ) {
9295 $installs_data[] = $common_diff_union;
9296 }
9297 }
9298
9299 foreach ( $installs_data as &$data ) {
9300 $data = (object) $data;
9301 }
9302
9303 return $installs_data;
9304 }
9305
9306 /**
9307 * Compare site actual data to the stored install data and return the differences for an API data sync.
9308 *
9309 * @author Vova Feldman (@svovaf)
9310 * @since 2.0.0
9311 *
9312 * @param array $site
9313 * @param FS_Site $install
9314 * @param string[] string $override
9315 *
9316 * @return array
9317 */
9318 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9319 $diff = array();
9320 $special = array();
9321 $special_override = false;
9322
9323 foreach ( $site as $p => $v ) {
9324 if ( property_exists( $install, $p ) ) {
9325 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9326 $install->{$p} != $v
9327 ) {
9328 $val = self::get_api_sanitized_property( $p, $v );
9329
9330 if ( $install->{$p} != $val ) {
9331 $install->{$p} = $val;
9332 $diff[ $p ] = $val;
9333 }
9334 }
9335 } else {
9336 $special[ $p ] = $v;
9337
9338 if ( isset( $override[ $p ] ) ||
9339 'plugins' === $p ||
9340 'themes' === $p
9341 ) {
9342 $special_override = true;
9343 }
9344 }
9345 }
9346
9347 if ( $special_override || 0 < count( $diff ) ) {
9348 // Add special params only if has at least one
9349 // standard param, or if explicitly requested to
9350 // override a special param or a param which is not exist
9351 // in the install object.
9352 $diff = array_merge( $diff, $special );
9353 }
9354
9355 return $diff;
9356 }
9357
9358 /**
9359 * @author Leo Fajardo (@leorw)
9360 * @since 2.5.1
9361 */
9362 private function send_pending_clone_update_once() {
9363 $this->_logger->entrance();
9364
9365 if ( ! empty( $this->_storage->clone_id ) ) {
9366 return;
9367 }
9368
9369 $install_clone = $this->get_api_site_scope()->call(
9370 '/clones',
9371 'post',
9372 array( 'site_url' => self::get_unfiltered_site_url() )
9373 );
9374
9375 if ( $this->is_api_result_entity( $install_clone ) ) {
9376 $this->_storage->clone_id = $install_clone->id;
9377 }
9378 }
9379
9380 /**
9381 * @author Leo Fajardo (@leorw)
9382 * @since 2.5.1
9383 *
9384 * @param string $resolution_type
9385 * @param FS_Site $clone_context_install
9386 */
9387 function send_clone_resolution_update( $resolution_type, $clone_context_install ) {
9388 $this->_logger->entrance();
9389
9390 if ( empty( $this->_storage->clone_id ) ) {
9391 return;
9392 }
9393
9394 $new_install_id = null;
9395 $current_site = null;
9396
9397 $flush = false;
9398
9399 /**
9400 * 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.
9401 */
9402 if ( $clone_context_install->id != $this->_site->id ) {
9403 $new_install_id = $this->_site->id;
9404 $current_site = $this->_site;
9405 $this->_site = $clone_context_install;
9406
9407 $flush = true;
9408 }
9409
9410 $this->get_api_site_scope( $flush )->call(
9411 "/clones/{$this->_storage->clone_id}",
9412 'put',
9413 array(
9414 'resolution' => $resolution_type,
9415 'new_install_id' => $new_install_id,
9416 )
9417 );
9418
9419 if ( is_object( $current_site ) ) {
9420 /**
9421 * Ensure that the install scope entity is updated back to the previous install entity.
9422 */
9423 $this->_site = $current_site;
9424
9425 // Restore the previous install scope entity of the API.
9426 $this->get_api_site_scope( true );
9427 }
9428 }
9429
9430 /**
9431 * Update install only if changed.
9432 *
9433 * @author Vova Feldman (@svovaf)
9434 * @since 1.0.9
9435 *
9436 * @param string[] string $override
9437 * @param bool $flush
9438 * @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.
9439 *
9440 * @return false|object|string
9441 */
9442 private function send_install_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9443 $this->_logger->entrance();
9444
9445 $check_properties = $this->get_install_data_for_api( $override );
9446
9447 if ( $flush ) {
9448 $params = $check_properties;
9449 } else {
9450 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9451 }
9452
9453 if ( empty( $params ) ) {
9454 $keepalive_only_update = $this->should_send_keepalive_update();
9455
9456 if ( ! $keepalive_only_update ) {
9457 /**
9458 * There are no updates to send including keepalive.
9459 *
9460 * @author Leo Fajardo (@leorw)
9461 * @since 2.2.3
9462 */
9463 return false;
9464 }
9465 }
9466
9467 if ( $is_two_way_sync ) {
9468 /**
9469 * Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9470 *
9471 * @author Leo Fajardo (@leorw)
9472 * @since 2.2.3
9473 */
9474 if ( ! is_multisite() ) {
9475 // Update last install sync timestamp.
9476 $this->set_cron_execution_timestamp( 'install_sync' );
9477 }
9478
9479 $params['uid'] = $this->get_anonymous_id();
9480 }
9481
9482 $this->set_keepalive_timestamp();
9483
9484 // Send updated values to FS.
9485 $site = $this->api_site_call( '/', 'put', $params, true );
9486
9487 if ( $is_two_way_sync && $this->is_api_result_entity( $site ) ) {
9488 /**
9489 * Clear scheduled install sync after a two-way sync call.
9490 *
9491 * @author Leo Fajardo (@leorw)
9492 * @since 2.2.3
9493 */
9494 if ( ! is_multisite() ) {
9495 // I successfully sent install update, clear scheduled sync if exist.
9496 $this->clear_install_sync_cron();
9497 }
9498 }
9499
9500 return $site;
9501 }
9502
9503 /**
9504 * Update installs only if changed.
9505 *
9506 * @author Vova Feldman (@svovaf)
9507 * @since 2.0.0
9508 *
9509 * @param string[] string $override
9510 * @param bool $flush
9511 * @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.
9512 *
9513 * @return false|object|string
9514 */
9515 private function send_installs_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9516 $this->_logger->entrance();
9517
9518 /**
9519 * Pass `true` to use the network level storage since the update is for many installs.
9520 *
9521 * @author Leo Fajardo (@leorw)
9522 * @since 2.2.3
9523 */
9524 $should_send_keepalive = $this->should_send_keepalive_update( true );
9525
9526 $installs_data = $this->get_installs_data_for_api( $override, ! $flush, $should_send_keepalive );
9527
9528 if ( empty( $installs_data ) ) {
9529 return false;
9530 }
9531
9532 if ( $is_two_way_sync ) {
9533 // Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9534 $this->set_cron_execution_timestamp( 'install_sync' );
9535 }
9536
9537 /**
9538 * Pass `true` to use the network level storage since the update is for many installs.
9539 *
9540 * @author Leo Fajardo (@leorw)
9541 * @since 2.2.3
9542 */
9543 $this->set_keepalive_timestamp( true );
9544
9545 // Send updated values to FS.
9546 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9547
9548 if ( $is_two_way_sync && $this->is_api_result_object( $result, 'installs' ) ) {
9549 // I successfully sent a two-way installs update, clear the scheduled install sync if it exists.
9550 $this->clear_install_sync_cron();
9551 }
9552
9553 return $result;
9554 }
9555
9556 /**
9557 * @author Leo Fajardo (@leorw)
9558 *
9559 * @param bool|null $use_network_level_storage
9560 *
9561 * @return bool
9562 */
9563 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9564 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9565
9566 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9567 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9568 return true;
9569 } else {
9570 // 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.
9571 return ( 7 == rand( 1, 7 ) );
9572 }
9573 }
9574
9575 /**
9576 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9577 *
9578 * @author Leo Fajardo (@leorw)
9579 * @since 2.3.2
9580 */
9581 private function maybe_sync_install_user() {
9582 if ( $this->_user->id == $this->_site->user_id ) {
9583 return;
9584 }
9585
9586 // Fetch user data and store if found.
9587 $this->sync_user_by_current_install();
9588 }
9589
9590 /**
9591 * Update install only if changed.
9592 *
9593 * @author Vova Feldman (@svovaf)
9594 * @since 1.0.9
9595 *
9596 * @param string[] string $override
9597 * @param bool $flush
9598 */
9599 function sync_install( $override = array(), $flush = false ) {
9600 $this->_logger->entrance();
9601
9602 $site = $this->send_install_update( $override, $flush, true );
9603
9604 if ( false === $site ) {
9605 // No sync required.
9606 return;
9607 }
9608
9609 if ( ! $this->is_api_result_entity( $site ) ) {
9610 // Failed to sync, don't update locally.
9611 return;
9612 }
9613
9614 $this->_site = new FS_Site( $site );
9615
9616 $this->_store_site( true );
9617 }
9618
9619 /**
9620 * Update install only if changed.
9621 *
9622 * @author Vova Feldman (@svovaf)
9623 * @since 1.0.9
9624 *
9625 * @param string[] string $override
9626 * @param bool $flush
9627 */
9628 private function sync_installs( $override = array(), $flush = false ) {
9629 $this->_logger->entrance();
9630
9631 $result = $this->send_installs_update( $override, $flush, true );
9632
9633 if ( false === $result ) {
9634 // No sync required.
9635 return;
9636 }
9637
9638 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9639 // Failed to sync, don't update locally.
9640 return;
9641 }
9642
9643 $address_to_blog_map = $this->get_address_to_blog_map();
9644
9645 foreach ( $result->installs as $install ) {
9646 $this->_site = new FS_Site( $install );
9647
9648 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9649 $blog_id = $address_to_blog_map[ $address ];
9650
9651 $this->_store_site( true, $blog_id );
9652 }
9653 }
9654
9655 /**
9656 * Track install's custom event.
9657 *
9658 * IMPORTANT:
9659 * Custom event tracking is currently only supported for specific clients.
9660 * If you are not one of them, please don't use this method. If you will,
9661 * the API will simply ignore your request based on the plugin ID.
9662 *
9663 * Need custom tracking for your plugin or theme?
9664 * If you are interested in custom event tracking please contact yo@freemius.com
9665 * for further details.
9666 *
9667 * @author Vova Feldman (@svovaf)
9668 * @since 1.2.1
9669 *
9670 * @param string $name Event name.
9671 * @param array $properties Associative key/value array with primitive values only
9672 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9673 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9674 *
9675 * @return object|false Event data or FALSE on failure.
9676 *
9677 * @throws \Freemius_InvalidArgumentException
9678 */
9679 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9680 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9681
9682 if ( ! $this->is_registered() ) {
9683 return false;
9684 }
9685
9686 $event = array( 'type' => $name );
9687
9688 if ( is_numeric( $process_at ) && $process_at > time() ) {
9689 $event['process_at'] = $process_at;
9690 }
9691
9692 if ( $once ) {
9693 $event['once'] = true;
9694 }
9695
9696 if ( ! empty( $properties ) ) {
9697 // Verify associative array values are primitive.
9698 foreach ( $properties as $k => $v ) {
9699 if ( ! is_scalar( $v ) ) {
9700 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9701 }
9702 }
9703
9704 $event['properties'] = $properties;
9705 }
9706
9707 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9708
9709 return $this->is_api_error( $result ) ?
9710 false :
9711 $result;
9712 }
9713
9714 /**
9715 * Track install's custom event only once, but it still triggers the API call.
9716 *
9717 * IMPORTANT:
9718 * Custom event tracking is currently only supported for specific clients.
9719 * If you are not one of them, please don't use this method. If you will,
9720 * the API will simply ignore your request based on the plugin ID.
9721 *
9722 * Need custom tracking for your plugin or theme?
9723 * If you are interested in custom event tracking please contact yo@freemius.com
9724 * for further details.
9725 *
9726 * @author Vova Feldman (@svovaf)
9727 * @since 1.2.1
9728 *
9729 * @param string $name Event name.
9730 * @param array $properties Associative key/value array with primitive values only
9731 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9732 *
9733 * @return object|false Event data or FALSE on failure.
9734 *
9735 * @throws \Freemius_InvalidArgumentException
9736 *
9737 * @user Freemius::track_event()
9738 */
9739 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9740 return $this->track_event( $name, $properties, $process_at, true );
9741 }
9742
9743 /**
9744 * Plugin uninstall hook.
9745 *
9746 * @author Vova Feldman (@svovaf)
9747 * @since 1.0.1
9748 *
9749 * @param bool $check_user Enforce checking if user have plugins activation privileges.
9750 */
9751 function _uninstall_plugin_event( $check_user = true ) {
9752 $this->_logger->entrance( 'slug = ' . $this->_slug );
9753
9754 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
9755 return;
9756 }
9757
9758 $params = array();
9759 $uninstall_reason = null;
9760 if ( isset( $this->_storage->uninstall_reason ) ) {
9761 $uninstall_reason = $this->_storage->uninstall_reason;
9762 $params['reason_id'] = $uninstall_reason->id;
9763 $params['reason_info'] = $uninstall_reason->info;
9764 }
9765
9766 if ( ! $this->is_registered() ) {
9767 // Send anonymous uninstall event only if user submitted a feedback.
9768 if ( isset( $uninstall_reason ) ) {
9769 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
9770 $this->opt_in( false, false, false, false, true );
9771 } else {
9772 $params['uid'] = $this->get_anonymous_id();
9773 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
9774 }
9775 }
9776 } else {
9777 $params = array_merge( $params, array(
9778 'is_active' => false,
9779 'is_uninstalled' => true,
9780 ) );
9781
9782 if ( $this->_is_network_active ) {
9783 // Send uninstall event.
9784 $this->send_installs_update( $params );
9785 } else {
9786 // Send uninstall event and handle the result.
9787 $this->sync_install( $params );
9788 }
9789 }
9790
9791 // @todo Decide if we want to delete plugin information from db.
9792 }
9793
9794 /**
9795 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
9796 *
9797 * @author Vova Feldman (@svovaf)
9798 * @since 2.2.1
9799 *
9800 * @param string $is_premium
9801 * @param string $caller
9802 *
9803 * @return void
9804 */
9805 function set_basename( $is_premium, $caller ) {
9806 $basename = plugin_basename( $caller );
9807
9808 $current_basename = $is_premium ?
9809 $this->_premium_plugin_basename :
9810 $this->_free_plugin_basename;
9811
9812 if ( $current_basename == $basename ) {
9813 // Basename value set correctly.
9814 return;
9815 }
9816
9817 if ( $is_premium ) {
9818 $this->_premium_plugin_basename = $basename;
9819 } else {
9820 $this->_free_plugin_basename = $basename;
9821 }
9822
9823 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
9824
9825 register_activation_hook(
9826 $plugin_dir . $basename,
9827 array( &$this, '_activate_plugin_event_hook' )
9828 );
9829 }
9830
9831 /**
9832 * @author Vova Feldman (@svovaf)
9833 * @since 1.1.1
9834 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
9835 *
9836 * @return string
9837 */
9838 function premium_plugin_basename() {
9839 if ( ! isset( $this->_premium_plugin_basename ) ) {
9840 $this->_premium_plugin_basename = $this->is_premium() ?
9841 // The product is premium, so use the current basename.
9842 $this->_plugin_basename :
9843 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
9844 }
9845
9846 return $this->_premium_plugin_basename;
9847 }
9848
9849 /**
9850 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
9851 *
9852 * @author Vova Feldman (@svovaf)
9853 * @since 1.0.2
9854 */
9855 public static function _uninstall_plugin_hook() {
9856 self::_load_required_static();
9857
9858 self::$_static_logger->entrance();
9859
9860 if ( ! current_user_can( 'activate_plugins' ) ) {
9861 return;
9862 }
9863
9864 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
9865
9866 self::$_static_logger->info( 'plugin = ' . $plugin_file );
9867
9868 define( 'WP_FS__UNINSTALL_MODE', true );
9869
9870 $fs = self::get_instance_by_file( $plugin_file );
9871
9872 if ( is_object( $fs ) ) {
9873 $fs->remove_sdk_reference();
9874
9875 self::require_plugin_essentials();
9876
9877 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
9878 is_plugin_active( $fs->premium_plugin_basename() )
9879 ) {
9880 // Deleting Free or Premium plugin version while the other version still installed.
9881 return;
9882 }
9883
9884 if (
9885 ! $fs->is_clone() &&
9886 /**
9887 * 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).
9888 *
9889 * @author Leo Fajardo
9890 */
9891 ( ! is_object( $fs->_site ) || $fs->is_registered() )
9892 ) {
9893 $fs->_uninstall_plugin_event();
9894 }
9895
9896 $fs->do_action( 'after_uninstall' );
9897 }
9898 }
9899
9900 #----------------------------------------------------------------------------------
9901 #region Plugin Information
9902 #----------------------------------------------------------------------------------
9903
9904 /**
9905 * Load WordPress core plugin.php essential module.
9906 *
9907 * @author Vova Feldman (@svovaf)
9908 * @since 1.1.1
9909 */
9910 private static function require_plugin_essentials() {
9911 if ( ! function_exists( 'get_plugins' ) ) {
9912 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
9913
9914 require_once ABSPATH . 'wp-admin/includes/plugin.php';
9915 }
9916 }
9917
9918 /**
9919 * Load WordPress core pluggable.php module.
9920 *
9921 * @author Vova Feldman (@svovaf)
9922 * @since 1.1.2
9923 */
9924 private static function require_pluggable_essentials() {
9925 if ( ! function_exists( 'wp_get_current_user' ) ) {
9926 require_once ABSPATH . 'wp-includes/pluggable.php';
9927 }
9928 }
9929
9930 /**
9931 * Return plugin data.
9932 *
9933 * @author Vova Feldman (@svovaf)
9934 * @since 1.0.1
9935 *
9936 * @param bool $reparse_plugin_metadata
9937 *
9938 * @return array
9939 */
9940 function get_plugin_data( $reparse_plugin_metadata = false ) {
9941 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
9942 self::require_plugin_essentials();
9943
9944 if ( $this->is_plugin() ) {
9945 /**
9946 * @author Vova Feldman (@svovaf)
9947 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
9948 *
9949 * @link https://github.com/Freemius/wordpress-sdk/issues/77
9950 */
9951 $plugin_data = get_plugin_data(
9952 $this->_plugin_main_file_path,
9953 false,
9954 false
9955 );
9956 } else {
9957 $theme_data = wp_get_theme();
9958
9959 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
9960 $parent_theme = $theme_data->parent();
9961
9962 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
9963 $theme_data = $parent_theme;
9964 }
9965 }
9966
9967 $plugin_data = array(
9968 'Name' => $theme_data->get( 'Name' ),
9969 'Version' => $theme_data->get( 'Version' ),
9970 'Author' => $theme_data->get( 'Author' ),
9971 'Description' => $theme_data->get( 'Description' ),
9972 'PluginURI' => $theme_data->get( 'ThemeURI' ),
9973 );
9974 }
9975
9976 $this->_plugin_data = $plugin_data;
9977 }
9978
9979 return $this->_plugin_data;
9980 }
9981
9982 /**
9983 * @author Vova Feldman (@svovaf)
9984 * @since 1.0.1
9985 * @since 1.2.2.5 If slug not set load slug by module ID.
9986 *
9987 * @return string Plugin slug.
9988 */
9989 function get_slug() {
9990 if ( ! isset( $this->_slug ) ) {
9991 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
9992 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
9993 }
9994
9995 return $this->_slug;
9996 }
9997
9998 /**
9999 * @author Leo Fajardo (@leorw)
10000 * @since 2.2.1
10001 *
10002 * @return string
10003 */
10004 function get_premium_slug() {
10005 return ( is_object( $this->_plugin ) && ! empty( $this->_plugin->premium_slug ) ) ?
10006 $this->_plugin->premium_slug :
10007 "{$this->_slug}-premium";
10008 }
10009
10010 /**
10011 * Retrieve the desired folder name for the product.
10012 *
10013 * @author Vova Feldman (@svovaf)
10014 * @since 1.2.1.7
10015 *
10016 * @return string Plugin slug.
10017 */
10018 function get_target_folder_name() {
10019 return $this->can_use_premium_code() ?
10020 $this->_plugin->premium_slug :
10021 $this->_slug;
10022 }
10023
10024 /**
10025 * @author Vova Feldman (@svovaf)
10026 * @since 1.0.1
10027 *
10028 * @return number Plugin ID.
10029 */
10030 function get_id() {
10031 return $this->_plugin->id;
10032 }
10033
10034 /**
10035 * @author Leo Fajardo (@leorw)
10036 * @since 2.2.4
10037 *
10038 * @return number|null Bundle ID.
10039 */
10040 function get_bundle_id() {
10041 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
10042 $this->_plugin->bundle_id :
10043 null;
10044 }
10045
10046 /**
10047 * @author Vova Feldman (@svovaf)
10048 * @since 2.3.1
10049 *
10050 * @return string|null Bundle public key.
10051 */
10052 function get_bundle_public_key() {
10053 return isset( $this->_plugin->bundle_public_key ) ?
10054 $this->_plugin->bundle_public_key :
10055 null;
10056 }
10057
10058 /**
10059 * Get whether the SDK has been initiated in the context of a Bundle.
10060 *
10061 * This will return true, if `bundle_id` is present in the SDK init parameters.
10062 *
10063 * ```php
10064 * $my_fs = fs_dynamic_init( array(
10065 * // ...
10066 * 'bundle_id' => 'XXXX', // Will return true since we have bundle id.
10067 * 'bundle_public_key' => 'pk_XXXX',
10068 * ) );
10069 * ```
10070 *
10071 * @author Swashata Ghosh (@swashata)
10072 * @since 2.5.0
10073 *
10074 * @return bool True if we are running in bundle context, false otherwise.
10075 */
10076 private function has_bundle_context() {
10077 return ! is_null( $this->get_bundle_id() );
10078 }
10079
10080 /**
10081 * @author Vova Feldman (@svovaf)
10082 * @since 1.2.1.5
10083 *
10084 * @return string Freemius SDK version
10085 */
10086 function get_sdk_version() {
10087 return $this->version;
10088 }
10089
10090 /**
10091 * @author Vova Feldman (@svovaf)
10092 * @since 1.2.1.5
10093 *
10094 * @return number Parent plugin ID (if parent exist).
10095 */
10096 function get_parent_id() {
10097 return $this->is_addon() ?
10098 $this->get_parent_instance()->get_id() :
10099 $this->_plugin->id;
10100 }
10101
10102 /**
10103 * @author Vova Feldman (@svovaf)
10104 * @since 2.3.1
10105 *
10106 * @return string
10107 */
10108 function get_usage_tracking_terms_url() {
10109 return $this->apply_filters(
10110 'usage_tracking_terms_url',
10111 "https://freemius.com/product/opt-in/{$this->_plugin->id}/{$this->_slug}/"
10112 );
10113 }
10114
10115 /**
10116 * @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.
10117 * @since 2.5.8
10118 *
10119 * @return string
10120 */
10121 function get_license_activation_terms_url() {
10122 return $this->apply_filters(
10123 'license_activation_terms_url',
10124 "https://freemius.com/product/license-activation/{$this->_plugin->id}/{$this->_slug}/"
10125 );
10126 }
10127
10128 /**
10129 * @author Vova Feldman (@svovaf)
10130 * @since 2.3.1
10131 *
10132 * @return string
10133 */
10134 function get_eula_url() {
10135 return $this->apply_filters(
10136 'eula_url',
10137 "https://freemius.com/product/{$this->_plugin->id}/{$this->_slug}/legal/eula/"
10138 );
10139 }
10140
10141 /**
10142 * @author Vova Feldman (@svovaf)
10143 * @since 1.0.1
10144 *
10145 * @return string Plugin public key.
10146 */
10147 function get_public_key() {
10148 return $this->_plugin->public_key;
10149 }
10150
10151 /**
10152 * Will be available only on sandbox mode.
10153 *
10154 * @author Vova Feldman (@svovaf)
10155 * @since 1.0.4
10156 *
10157 * @return mixed Plugin secret key.
10158 */
10159 function get_secret_key() {
10160 return $this->_plugin->secret_key;
10161 }
10162
10163 /**
10164 * @author Vova Feldman (@svovaf)
10165 * @since 1.1.1
10166 *
10167 * @return bool
10168 */
10169 function has_secret_key() {
10170 return ! empty( $this->_plugin->secret_key );
10171 }
10172
10173 /**
10174 * @author Vova Feldman (@svovaf)
10175 * @since 1.0.9
10176 *
10177 * @param string|bool $premium_suffix
10178 *
10179 * @return string
10180 */
10181 function get_plugin_name( $premium_suffix = false ) {
10182 $this->_logger->entrance();
10183
10184 /**
10185 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10186 *
10187 * @author Vova Feldman
10188 */
10189 if ( ! isset( $this->_plugin_name ) ) {
10190 // Name is not yet set.
10191 $this->set_name( $premium_suffix );
10192 } else if (
10193 ! empty( $premium_suffix ) &&
10194 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10195 ) {
10196 // Name is already set, but there's a change in the premium suffix.
10197 $this->set_name( $premium_suffix );
10198 }
10199
10200 return $this->_plugin_name;
10201 }
10202
10203 /**
10204 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10205 *
10206 * @author Vova Feldman (@svovaf)
10207 * @since 2.2.1
10208 *
10209 * @param string $premium_suffix
10210 */
10211 private function set_name( $premium_suffix = '' ) {
10212 $plugin_data = $this->get_plugin_data();
10213
10214 // Get name.
10215 $this->_plugin_name = $plugin_data['Name'];
10216
10217 if ( is_string( $premium_suffix ) ) {
10218 $premium_suffix = trim( $premium_suffix );
10219
10220 if ( ! empty( $premium_suffix ) ) {
10221 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10222 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10223 $suffix_len = strlen( $suffix );
10224
10225 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10226 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10227 ) {
10228 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10229 }
10230 }
10231 }
10232
10233 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10234 }
10235
10236 /**
10237 * @author Vova Feldman (@svovaf)
10238 * @since 1.0.0
10239 *
10240 * @param bool $reparse_plugin_metadata
10241 *
10242 * @return string
10243 */
10244 function get_plugin_version( $reparse_plugin_metadata = false ) {
10245 $this->_logger->entrance();
10246
10247 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10248
10249 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10250
10251 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10252 }
10253
10254 /**
10255 * @author Vova Feldman (@svovaf)
10256 * @since 1.2.1.7
10257 *
10258 * @return string
10259 */
10260 function get_plugin_title() {
10261 $this->_logger->entrance();
10262
10263 $title = $this->_plugin->title;
10264
10265 return $this->apply_filters( 'plugin_title', $title );
10266 }
10267
10268 /**
10269 * @author Vova Feldman (@svovaf)
10270 * @since 1.2.2.7
10271 *
10272 * @param bool $lowercase
10273 *
10274 * @return string
10275 */
10276 function get_module_label( $lowercase = false ) {
10277 $label = $this->is_addon() ?
10278 $this->get_text_inline( 'Add-On', 'addon' ) :
10279 ( $this->is_plugin() ?
10280 $this->get_text_inline( 'Plugin', 'plugin' ) :
10281 $this->get_text_inline( 'Theme', 'theme' ) );
10282
10283 if ( $lowercase ) {
10284 $label = strtolower( $label );
10285 }
10286
10287 return $label;
10288 }
10289
10290 /**
10291 * @author Vova Feldman (@svovaf)
10292 * @since 1.0.4
10293 *
10294 * @return string
10295 */
10296 function get_plugin_basename() {
10297 if ( ! isset( $this->_plugin_basename ) ) {
10298 if ( $this->is_plugin() ) {
10299 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10300 } else {
10301 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10302 }
10303 }
10304
10305 return $this->_plugin_basename;
10306 }
10307
10308 function get_plugin_folder_name() {
10309 $this->_logger->entrance();
10310
10311 $plugin_folder = $this->_plugin_basename;
10312
10313 while ( '.' !== dirname( $plugin_folder ) ) {
10314 $plugin_folder = dirname( $plugin_folder );
10315 }
10316
10317 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10318
10319 return $plugin_folder;
10320 }
10321
10322 #endregion ------------------------------------------------------------------
10323
10324 /* Account
10325 ------------------------------------------------------------------------------------------------------------------*/
10326
10327 /**
10328 * Find plugin's slug by plugin's basename.
10329 *
10330 * @author Vova Feldman (@svovaf)
10331 * @since 1.0.9
10332 *
10333 * @param string $plugin_base_name
10334 *
10335 * @return false|string
10336 */
10337 private static function find_slug_by_basename( $plugin_base_name ) {
10338 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10339
10340 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10341 return false;
10342 }
10343
10344 return $file_slug_map[ $plugin_base_name ];
10345 }
10346
10347 /**
10348 * Store the map between the plugin's basename to the slug.
10349 *
10350 * @author Vova Feldman (@svovaf)
10351 * @since 1.0.9
10352 */
10353 private function store_file_slug_map() {
10354 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10355
10356 if ( ! array( $file_slug_map ) ) {
10357 $file_slug_map = array();
10358 }
10359
10360 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10361 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10362 ) {
10363 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10364 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10365 }
10366 }
10367
10368 /**
10369 * @return array[number]FS_User
10370 */
10371 static function get_all_users() {
10372 $users = self::maybe_get_entities_account_option( 'users', array() );
10373
10374 if ( ! is_array( $users ) ) {
10375 $users = array();
10376 }
10377
10378 return $users;
10379 }
10380
10381 /**
10382 * @param string $module_type
10383 * @param null|int $blog_id Since 2.0.0
10384 *
10385 * @return array[string]FS_Site
10386 */
10387 public static function get_all_sites(
10388 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10389 $blog_id = null,
10390 $is_backup = false
10391 ) {
10392 $sites = self::get_account_option(
10393 ( $is_backup ? 'prev_' : '' ) . 'sites',
10394 $module_type,
10395 $blog_id
10396 );
10397
10398 if ( ! is_array( $sites ) ) {
10399 $sites = array();
10400 }
10401
10402 return $sites;
10403 }
10404
10405 /**
10406 * @author Leo Fajardo (@leorw)
10407 *
10408 * @since 1.2.2
10409 *
10410 * @param string $option_name
10411 * @param string $module_type
10412 * @param null|int $network_level_or_blog_id Since 2.0.0
10413 *
10414 * @return mixed
10415 */
10416 public static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10417 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10418 $option_name = $module_type . '_' . $option_name;
10419 }
10420
10421 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10422 }
10423
10424 /**
10425 * @author Leo Fajardo (@leorw)
10426 *
10427 * @since 1.2.2
10428 *
10429 * @param string $option_name
10430 * @param mixed $option_value
10431 * @param bool $store
10432 * @param null|int $network_level_or_blog_id Since 2.0.0
10433 */
10434 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10435 self::set_account_option_by_module(
10436 $this->_module_type,
10437 $option_name,
10438 $option_value,
10439 $store,
10440 $network_level_or_blog_id
10441 );
10442 }
10443
10444 /**
10445 * @author Vova Feldman (@svovaf)
10446 *
10447 * @since 1.2.2.7
10448 *
10449 * @param string $module_type
10450 * @param string $option_name
10451 * @param mixed $option_value
10452 * @param bool $store
10453 * @param null|int $network_level_or_blog_id Since 2.0.0
10454 */
10455 private static function set_account_option_by_module(
10456 $module_type,
10457 $option_name,
10458 $option_value,
10459 $store,
10460 $network_level_or_blog_id = null
10461 ) {
10462 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10463 $option_name = $module_type . '_' . $option_name;
10464 }
10465
10466 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10467 }
10468
10469 /**
10470 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10471 *
10472 * @author Leo Fajardo (@leorw)
10473 * @since 2.3.1
10474 *
10475 * @param string $option_name
10476 * @param mixed $default
10477 * @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).
10478 *
10479 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10480 */
10481 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10482 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10483
10484 $class_name = '';
10485
10486 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10487 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10488 }
10489
10490 switch ( $option_name ) {
10491 case 'plugins':
10492 case 'themes':
10493 case 'addons':
10494 $class_name = FS_Plugin::get_class_name();
10495 break;
10496 case 'users':
10497 $class_name = FS_User::get_class_name();
10498 break;
10499 case 'sites':
10500 $class_name = FS_Site::get_class_name();
10501 break;
10502 case 'licenses':
10503 case 'all_licenses':
10504 $class_name = FS_Plugin_License::get_class_name();
10505 break;
10506 case 'plans':
10507 $class_name = FS_Plugin_Plan::get_class_name();
10508 break;
10509 case 'updates':
10510 $class_name = FS_Plugin_Tag::get_class_name();
10511 break;
10512 }
10513
10514 if ( empty( $class_name ) ) {
10515 return $option;
10516 }
10517
10518 return fs_get_entities( $option, $class_name );
10519 }
10520
10521 /**
10522 * @author Vova Feldman (@svovaf)
10523 * @since 1.0.6
10524 *
10525 * @param number|null $module_id
10526 *
10527 * @return FS_Plugin_License[]
10528 */
10529 private static function get_all_licenses( $module_id = null ) {
10530 $licenses = self::get_account_option( 'all_licenses' );
10531
10532 if ( ! is_array( $licenses ) ) {
10533 $licenses = array();
10534 }
10535
10536 if ( is_null( $module_id ) ) {
10537 return $licenses;
10538 }
10539
10540 $licenses = isset( $licenses[ $module_id ] ) ?
10541 $licenses[ $module_id ] :
10542 array();
10543
10544 return $licenses;
10545 }
10546
10547 /**
10548 * @author Leo Fajardo (@leorw)
10549 * @since 2.0.0
10550 *
10551 * @param number $module_id
10552 * @param number|null $user_id
10553 *
10554 * @return array
10555 */
10556 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10557 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10558
10559 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10560 $all_modules_user_id_license_ids_map = array();
10561 }
10562
10563 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10564 $all_modules_user_id_license_ids_map[ $module_id ] :
10565 array();
10566
10567 if ( FS_User::is_valid_id( $user_id ) ) {
10568 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10569 $user_id_license_ids_map[ $user_id ] :
10570 array();
10571 }
10572
10573 return $user_id_license_ids_map;
10574 }
10575
10576 /**
10577 * @author Leo Fajardo (@leorw)
10578 * @since 2.0.0
10579 *
10580 * @param array $new_user_id_license_ids_map
10581 * @param number $module_id
10582 * @param number|null $user_id
10583 */
10584 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10585 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10586 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10587 $all_modules_user_id_license_ids_map = array();
10588 }
10589
10590 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10591 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10592 }
10593
10594 if ( FS_User::is_valid_id( $user_id ) ) {
10595 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10596 } else {
10597 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10598 }
10599
10600 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10601 }
10602
10603 /**
10604 * Get a collection of the user's linked license IDs.
10605 *
10606 * @author Vova Feldman (@svovaf)
10607 * @since 2.0.0
10608 *
10609 * @param number $user_id
10610 *
10611 * @return number[]
10612 */
10613 private function get_user_linked_license_ids( $user_id ) {
10614 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10615 }
10616
10617 /**
10618 * Override the user's linked license IDs with a new IDs collection.
10619 *
10620 * @author Vova Feldman (@svovaf)
10621 * @since 2.0.0
10622 *
10623 * @param number $user_id
10624 * @param number[] $license_ids
10625 */
10626 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10627 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10628 }
10629
10630 /**
10631 * Link a specified license ID to a given user.
10632 *
10633 * @author Vova Feldman (@svovaf)
10634 * @since 2.0.0
10635 *
10636 * @param number $license_id
10637 * @param number $user_id
10638 */
10639 private function link_license_2_user( $license_id, $user_id ) {
10640 $license_ids = $this->get_user_linked_license_ids( $user_id );
10641
10642 if ( in_array( $license_id, $license_ids ) ) {
10643 // License already linked.
10644 return;
10645 }
10646
10647 $license_ids[] = $license_id;
10648
10649 $this->set_user_linked_license_ids( $user_id, $license_ids );
10650 }
10651
10652 /**
10653 * @param string|bool $module_type
10654 *
10655 * @return FS_Plugin_Plan[]
10656 */
10657 private static function get_all_plans( $module_type = false ) {
10658 $plans = self::get_account_option( 'plans', $module_type );
10659
10660 if ( ! is_array( $plans ) ) {
10661 $plans = array();
10662 }
10663
10664 return $plans;
10665 }
10666
10667 /**
10668 * @author Vova Feldman (@svovaf)
10669 * @since 1.0.4
10670 *
10671 * @return FS_Plugin_Tag[]
10672 */
10673 private static function get_all_updates() {
10674 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10675
10676 if ( ! is_array( $updates ) ) {
10677 $updates = array();
10678 }
10679
10680 return $updates;
10681 }
10682
10683 /**
10684 * @author Vova Feldman (@svovaf)
10685 * @since 1.0.6
10686 *
10687 * @return array<number,FS_Plugin[]>|false
10688 */
10689 public static function get_all_addons() {
10690 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10691
10692 if ( ! is_array( $addons ) ) {
10693 $addons = array();
10694 }
10695
10696 return $addons;
10697 }
10698
10699 /**
10700 * @author Vova Feldman (@svovaf)
10701 * @since 1.0.6
10702 *
10703 * @return number[]|false
10704 */
10705 public static function get_all_account_addons() {
10706 $addons = self::$_accounts->get_option( 'account_addons', array() );
10707
10708 if ( ! is_array( $addons ) ) {
10709 $addons = array();
10710 }
10711
10712 return $addons;
10713 }
10714
10715 /**
10716 * Check if user has connected his account (opted-in).
10717 *
10718 * Note:
10719 * If the user opted-in and opted-out on a later stage,
10720 * this will still return true. If you want to check if the
10721 * user is currently opted-in, use:
10722 * `$fs->is_registered() && $fs->is_tracking_allowed()`
10723 *
10724 * @author Vova Feldman (@svovaf)
10725 * @since 1.0.1
10726 *
10727 * @param bool $ignore_anonymous_state Since 2.5.1
10728 *
10729 * @return bool
10730 */
10731 function is_registered( $ignore_anonymous_state = false ) {
10732 return (
10733 is_object( $this->_user ) &&
10734 (
10735 $this->is_premium() ||
10736 $ignore_anonymous_state ||
10737 ! $this->is_anonymous()
10738 )
10739 );
10740 }
10741
10742 /**
10743 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
10744 *
10745 * @author Leo Fajardo (@leorw)
10746 * @since 1.2.1.5
10747 *
10748 * @return bool
10749 */
10750 function is_tracking_allowed( $blog_id = null, $install = null ) {
10751 if ( is_null( $install ) ) {
10752 $install = is_null( $blog_id ) ?
10753 $this->_site :
10754 $this->get_install_by_blog_id( $blog_id );
10755 }
10756
10757 return (
10758 is_object( $install ) &&
10759 FS_Permission_Manager::instance( $this )->is_homepage_url_tracking_allowed( $blog_id )
10760 );
10761 }
10762
10763 /**
10764 * Returns TRUE if the user never opted-in or manually opted-out.
10765 *
10766 * @author Vova Feldman (@svovaf)
10767 * @since 1.2.1.5
10768 *
10769 * @param int|null $blog_id
10770 *
10771 * @return bool
10772 */
10773 function is_tracking_prohibited( $blog_id = null ) {
10774 return (
10775 ! $this->is_registered( true ) ||
10776 ! $this->is_tracking_allowed( $blog_id )
10777 );
10778 }
10779
10780 /**
10781 * @author Leo Fajardo (@leorw)
10782 * @since 2.4.0
10783 *
10784 * @return bool
10785 */
10786 function is_bundle_license_auto_activation_enabled() {
10787 return $this->is_addon() ?
10788 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
10789 $this->_is_bundle_license_auto_activation_enabled;
10790 }
10791
10792 /**
10793 * @author Vova Feldman (@svovaf)
10794 * @since 1.0.4
10795 *
10796 * @return FS_Plugin
10797 */
10798 function get_plugin() {
10799 return $this->_plugin;
10800 }
10801
10802 /**
10803 * @author Vova Feldman (@svovaf)
10804 * @since 1.0.3
10805 *
10806 * @return FS_User
10807 */
10808 function get_user() {
10809 return $this->_user;
10810 }
10811
10812 /**
10813 * @author Vova Feldman (@svovaf)
10814 * @since 1.0.3
10815 *
10816 * @return FS_Site
10817 */
10818 function get_site() {
10819 return $this->_site;
10820 }
10821
10822 /**
10823 * @author Daniele Alessandra (@danielealessandra)
10824 * @return FS_Storage
10825 * @since 2.6.2
10826 *
10827 */
10828 public function get_storage() {
10829 return $this->_storage;
10830 }
10831
10832 /**
10833 * @author Leo Fajardo (@leorw)
10834 * @since 2.5.0
10835 */
10836 function store_site( $site ) {
10837 $this->_site = $site;
10838 $this->_store_site( true );
10839 }
10840
10841 /**
10842 * 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).
10843 *
10844 * @author Leo Fajardo (@leorw)
10845 * @since 2.5.0
10846 */
10847 function delete_current_install( $back_up ) {
10848 // Back up and delete the unique ID.
10849 if ( $back_up ) {
10850 self::$_accounts->set_option( 'prev_unique_id', $this->get_anonymous_id() );
10851 }
10852
10853 self::$_accounts->set_option( 'unique_id', null );
10854
10855 if ( $back_up ) {
10856 // 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).
10857 $this->back_up_site();
10858 }
10859
10860 $this->_delete_site();
10861 $this->_site = null;
10862 }
10863
10864 /**
10865 * @author Leo Fajardo (@leorw)
10866 * @since 2.5.0
10867 */
10868 function restore_backup_site() {
10869 self::$_accounts->set_option(
10870 'unique_id',
10871 self::$_accounts->get_option( 'prev_unique_id' )
10872 );
10873
10874 $sites = self::get_all_sites( $this->_module_type, null, true );
10875 $this->store_site( clone $sites[ $this->_slug ] );
10876 }
10877
10878 /**
10879 * Get plugin add-ons.
10880 *
10881 * @author Vova Feldman (@svovaf)
10882 * @since 1.0.6
10883 *
10884 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
10885 *
10886 * @param bool $flush
10887 *
10888 * @return FS_Plugin[]|false
10889 */
10890 function get_addons( $flush = false ) {
10891 $this->_logger->entrance();
10892
10893 if ( ! $this->_has_addons ) {
10894 return false;
10895 }
10896
10897 $addons = $this->sync_addons( $flush );
10898
10899 return ( ! is_array( $addons ) || empty( $addons ) ) ?
10900 false :
10901 $addons;
10902 }
10903
10904 /**
10905 * @author Vova Feldman (@svovaf)
10906 * @since 1.0.6
10907 *
10908 * @return number[]|false
10909 */
10910 function get_account_addons() {
10911 $this->_logger->entrance();
10912
10913 $addons = self::get_all_account_addons();
10914
10915 if ( ! is_array( $addons ) ||
10916 ! isset( $addons[ $this->_plugin->id ] ) ||
10917 ! is_array( $addons[ $this->_plugin->id ] ) ||
10918 0 === count( $addons[ $this->_plugin->id ] )
10919 ) {
10920 return false;
10921 }
10922
10923 return $addons[ $this->_plugin->id ];
10924 }
10925
10926 /**
10927 * Check if user has any
10928 *
10929 * @author Vova Feldman (@svovaf)
10930 * @since 1.1.6
10931 *
10932 * @return bool
10933 */
10934 function has_account_addons() {
10935 $addons = $this->get_account_addons();
10936
10937 return is_array( $addons ) && ( 0 < count( $addons ) );
10938 }
10939
10940
10941 /**
10942 * Get add-on by ID (from local data).
10943 *
10944 * @author Vova Feldman (@svovaf)
10945 * @since 1.0.6
10946 *
10947 * @param number $id
10948 *
10949 * @return FS_Plugin|false
10950 */
10951 function get_addon( $id ) {
10952 $this->_logger->entrance();
10953
10954 $addons = $this->get_addons();
10955
10956 if ( is_array( $addons ) ) {
10957 foreach ( $addons as $addon ) {
10958 if ( $id == $addon->id ) {
10959 return $addon;
10960 }
10961 }
10962 }
10963
10964 return false;
10965 }
10966
10967 /**
10968 * Get add-on by slug (from local data).
10969 *
10970 * @author Vova Feldman (@svovaf)
10971 * @since 1.0.6
10972 *
10973 * @param string $slug
10974 *
10975 * @param bool $flush
10976 *
10977 * @return FS_Plugin|false
10978 */
10979 function get_addon_by_slug( $slug, $flush = false ) {
10980 $this->_logger->entrance();
10981
10982 $addons = $this->get_addons( $flush );
10983
10984 if ( is_array( $addons ) ) {
10985 foreach ( $addons as $addon ) {
10986 if ( $slug === $addon->slug ) {
10987 return $addon;
10988 }
10989 }
10990 }
10991
10992 return false;
10993 }
10994
10995 /**
10996 * @var array<number,object[]> {
10997 * @key number Add-on ID.
10998 * @val object[] The add-on's plans and prices object.
10999 * }
11000 */
11001 private $plans_and_pricing_by_addon_id;
11002
11003 /**
11004 * @author Leo Fajardo (@leorw)
11005 * @since 2.3.0
11006 *
11007 * @return array<number,object[]> {
11008 * @key number Add-on ID.
11009 * @val object[] The add-on's plans and prices object.
11010 * }
11011 */
11012 function _get_addons_plans_and_pricing_map_by_id() {
11013 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
11014 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
11015
11016 $plans_and_pricing_by_addon_id = array();
11017 if ( $this->is_api_result_object( $result, 'addons' ) ) {
11018 foreach ( $result->addons as $addon ) {
11019 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
11020 }
11021 }
11022
11023 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
11024 }
11025
11026 return $this->plans_and_pricing_by_addon_id;
11027 }
11028
11029 /**
11030 * @author Leo Fajardo (@leorw)
11031 * @since 2.3.0
11032 *
11033 * @param number $addon_id
11034 * @param bool $is_installed
11035 *
11036 * @return array
11037 */
11038 function _get_addon_info( $addon_id, $is_installed ) {
11039 $addon = $this->get_addon( $addon_id );
11040
11041 if ( ! is_object( $addon ) ) {
11042 // Unexpected call.
11043 return array();
11044 }
11045
11046 $slug = $addon->slug;
11047
11048 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
11049
11050 if ( ! fs_is_network_admin() ) {
11051 // Get blog-level activated installations.
11052 $sites = self::maybe_get_entities_account_option( 'sites', array() );
11053 } else {
11054 $sites = null;
11055
11056 if ( $this->is_addon_activated( $addon_id ) &&
11057 $this->get_addon_instance( $addon_id )->is_network_active()
11058 ) {
11059 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
11060 // Get network-level activated installations.
11061 $sites = self::maybe_get_entities_account_option(
11062 'sites',
11063 array(),
11064 $addon_storage->network_install_blog_id
11065 );
11066 }
11067 }
11068 }
11069
11070 $addon_info = array(
11071 'is_connected' => false,
11072 'slug' => $slug,
11073 'title' => $addon->title,
11074 'is_whitelabeled' => $addon_storage->is_whitelabeled
11075 );
11076
11077 if ( ! $is_installed ) {
11078 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
11079
11080 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
11081 $has_paid_plan = false;
11082 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
11083
11084 if ( is_array( $plans ) && count( $plans ) > 0 ) {
11085 foreach ( $plans as $plan ) {
11086 if ( isset( $plan->pricing ) &&
11087 is_array( $plan->pricing ) &&
11088 count( $plan->pricing ) > 0
11089 ) {
11090 $has_paid_plan = true;
11091 break;
11092 }
11093 }
11094 }
11095
11096 $addon_info['has_paid_plan'] = $has_paid_plan;
11097 }
11098 }
11099
11100 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
11101 return $addon_info;
11102 }
11103
11104 $site = $sites[ $slug ];
11105
11106 $addon_info['is_connected'] = (
11107 ( $addon->parent_plugin_id == $this->get_id() ) &&
11108 is_object( $site ) &&
11109 FS_Site::is_valid_id( $site->id ) &&
11110 FS_User::is_valid_id( $site->user_id ) &&
11111 FS_Plugin_Plan::is_valid_id( $site->plan_id )
11112 );
11113
11114 if ( $addon_info['is_connected'] && $is_installed ) {
11115 return $addon_info;
11116 }
11117
11118 $addon_info['site'] = $site;
11119
11120 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
11121 if ( isset( $plugins_data[ $slug ] ) ) {
11122 $plugin_data = $plugins_data[ $slug ];
11123
11124 $addon_info['version'] = $plugin_data->version;
11125 }
11126
11127 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
11128 if ( isset( $all_plans[ $slug ] ) ) {
11129 $plans = $all_plans[ $slug ];
11130
11131 foreach ( $plans as $plan ) {
11132 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
11133 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
11134 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
11135 break;
11136 }
11137 }
11138 }
11139
11140 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
11141 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
11142 foreach ( $licenses[ $addon_id ] as $license ) {
11143 if ( $license->id == $site->license_id ) {
11144 $addon_info['license'] = $license;
11145 break;
11146 }
11147 }
11148 }
11149
11150 if ( isset( $addon_info['license'] ) ) {
11151 if ( isset( $addon_storage->subscriptions ) &&
11152 ! empty( $addon_storage->subscriptions )
11153 ) {
11154 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11155
11156 foreach ( $addon_subscriptions as $subscription ) {
11157 if ( $subscription->license_id == $site->license_id ) {
11158 $addon_info['subscription'] = $subscription;
11159 break;
11160 }
11161 }
11162 }
11163 }
11164
11165 return $addon_info;
11166 }
11167
11168 /**
11169 * @author Vova Feldman (@svovaf)
11170 * @since 2.0.0
11171 *
11172 * @param number $user_id
11173 *
11174 * @return FS_User
11175 */
11176 static function _get_user_by_id( $user_id ) {
11177 self::$_static_logger->entrance( "user_id = {$user_id}" );
11178
11179 $users = self::get_all_users();
11180
11181 if ( is_array( $users ) ) {
11182 if ( isset( $users[ $user_id ] ) &&
11183 $users[ $user_id ] instanceof FS_User &&
11184 $user_id == $users[ $user_id ]->id
11185 ) {
11186 return $users[ $user_id ];
11187 }
11188
11189 // If user wasn't found by the key, iterate over all the users collection.
11190 foreach ( $users as $user ) {
11191 /**
11192 * @var FS_User $user
11193 */
11194 if ( $user_id == $user->id ) {
11195 return $user;
11196 }
11197 }
11198 }
11199
11200 return null;
11201 }
11202
11203 /**
11204 * Checks if a Freemius user_id is associated with a super-admin.
11205 *
11206 * @author Vova Feldman (@svovaf)
11207 * @since 2.0.0
11208 *
11209 * @param number $user_id
11210 *
11211 * @return bool
11212 */
11213 private static function is_super_admin( $user_id ) {
11214 $is_super_admin = false;
11215
11216 $user = self::_get_user_by_id( $user_id );
11217
11218 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11219 self::require_pluggable_essentials();
11220
11221 $wp_user = get_user_by( 'email', $user->email );
11222
11223 if ( $wp_user instanceof WP_User ) {
11224 $super_admins = get_super_admins();
11225 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11226 }
11227 }
11228
11229 return $is_super_admin;
11230 }
11231
11232 #----------------------------------------------------------------------------------
11233 #region Plans & Licensing
11234 #----------------------------------------------------------------------------------
11235
11236 /**
11237 * Check if running premium plugin code.
11238 *
11239 * @author Vova Feldman (@svovaf)
11240 * @since 1.0.5
11241 *
11242 * @return bool
11243 */
11244 function is_premium() {
11245 /**
11246 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11247 * `register_constructor_hooks` method.
11248 *
11249 * @author Leo Fajardo (@leorw)
11250 * @since 2.2.3
11251 */
11252 return is_object( $this->_plugin ) ?
11253 $this->_plugin->is_premium :
11254 false;
11255 }
11256
11257 /**
11258 * Get site's plan ID.
11259 *
11260 * @author Vova Feldman (@svovaf)
11261 * @since 1.0.2
11262 *
11263 * @return number
11264 */
11265 function get_plan_id() {
11266 return $this->_site->plan_id;
11267 }
11268
11269 /**
11270 * Get site's plan title.
11271 *
11272 * @author Vova Feldman (@svovaf)
11273 * @since 1.0.2
11274 *
11275 * @return string
11276 */
11277 function get_plan_title() {
11278 $plan = $this->get_plan();
11279
11280 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11281 }
11282
11283 /**
11284 * Get site's plan name.
11285 *
11286 * @author Vova Feldman (@svovaf)
11287 * @since 2.0.0
11288 *
11289 * @return string
11290 */
11291 function get_plan_name() {
11292 $plan = $this->get_plan();
11293
11294 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11295 }
11296
11297 /**
11298 * @author Vova Feldman (@svovaf)
11299 * @since 1.0.9
11300 *
11301 * @return FS_Plugin_Plan|false
11302 */
11303 function get_plan() {
11304 if ( ! is_object( $this->_site ) ) {
11305 return false;
11306 }
11307
11308 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11309 $this->_get_plan_by_id( $this->_site->plan_id ) :
11310 false;
11311 }
11312
11313 /**
11314 * @author Vova Feldman (@svovaf)
11315 * @since 1.0.3
11316 *
11317 * @return bool
11318 */
11319 function is_trial() {
11320 $this->_logger->entrance();
11321
11322 if ( ! $this->is_registered( true ) || ! is_object( $this->_site ) ) {
11323 return false;
11324 }
11325
11326 return $this->_site->is_trial();
11327 }
11328
11329 /**
11330 * Check if currently in a trial with payment method (credit card or paypal).
11331 *
11332 * @author Vova Feldman (@svovaf)
11333 * @since 1.1.7
11334 *
11335 * @return bool
11336 */
11337 function is_paid_trial() {
11338 $this->_logger->entrance();
11339
11340 if ( ! $this->is_trial() ) {
11341 return false;
11342 }
11343
11344 if ( ! $this->has_active_valid_license() ) {
11345 return false;
11346 }
11347
11348 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11349 return false;
11350 }
11351
11352 /**
11353 * @var FS_Subscription $subscription
11354 */
11355 $subscription = $this->_get_subscription( $this->_license->id );
11356
11357 return ( is_object( $subscription ) && $subscription->is_active() );
11358 }
11359
11360 /**
11361 * Check if trial already utilized.
11362 *
11363 * @since 1.0.9
11364 *
11365 * @return bool
11366 */
11367 function is_trial_utilized() {
11368 $this->_logger->entrance();
11369
11370 if ( ! $this->is_registered() ) {
11371 return false;
11372 }
11373
11374 return $this->_site->is_trial_utilized();
11375 }
11376
11377 /**
11378 * Get trial plan information (if in trial).
11379 *
11380 * @author Vova Feldman (@svovaf)
11381 * @since 1.0.9
11382 *
11383 * @return bool|FS_Plugin_Plan
11384 */
11385 function get_trial_plan() {
11386 $this->_logger->entrance();
11387
11388 if ( ! $this->is_trial() ) {
11389 return false;
11390 }
11391
11392 // Try to load plan from local cache.
11393 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11394
11395 if ( ! is_object( $trial_plan ) ) {
11396 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11397
11398 /**
11399 * If managed to fetch the plan, add it to the plans collection.
11400 */
11401 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11402 if ( ! is_array( $this->_plans ) ) {
11403 $this->_plans = array();
11404 }
11405
11406 $this->_plans[] = $trial_plan;
11407 $this->_store_plans();
11408 }
11409 }
11410
11411 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11412 return $trial_plan;
11413 }
11414
11415 /**
11416 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11417 */
11418 $trial_plan = new FS_Plugin_Plan();
11419 $trial_plan->id = $this->_site->trial_plan_id;
11420 $trial_plan->name = 'pro';
11421 $trial_plan->title = 'Pro';
11422
11423 return $trial_plan;
11424 }
11425
11426 /**
11427 * Check if the user has an activate, non-expired license on current plugin's install.
11428 *
11429 * @since 1.0.9
11430 *
11431 * @return bool
11432 */
11433 function is_paying() {
11434 $this->_logger->entrance();
11435
11436 if ( ! $this->is_registered( true ) ) {
11437 return false;
11438 }
11439
11440 if ( ! $this->has_paid_plan() ) {
11441 return false;
11442 }
11443
11444 return (
11445 ! $this->is_trial() &&
11446 'free' !== $this->get_plan_name() &&
11447 $this->has_active_valid_license()
11448 );
11449 }
11450
11451 /**
11452 * @author Vova Feldman (@svovaf)
11453 * @since 1.0.4
11454 *
11455 * @return bool
11456 */
11457 function is_free_plan() {
11458 if ( ! $this->is_registered() ) {
11459 return true;
11460 }
11461
11462 if ( ! $this->has_paid_plan() ) {
11463 return true;
11464 }
11465
11466 return (
11467 'free' === $this->get_plan_name() ||
11468 ! $this->has_features_enabled_license()
11469 );
11470 }
11471
11472 /**
11473 * @author Vova Feldman (@svovaf)
11474 * @since 1.0.5
11475 *
11476 * @return bool
11477 */
11478 function _has_premium_license() {
11479 $this->_logger->entrance();
11480
11481 $premium_license = $this->_get_available_premium_license();
11482
11483 return ( false !== $premium_license );
11484 }
11485
11486 /**
11487 * Check if user has any licenses associated with the plugin (including expired or blocking).
11488 *
11489 * @author Vova Feldman (@svovaf)
11490 * @since 1.1.7.3
11491 *
11492 * @param bool $including_foreign
11493 *
11494 * @return bool
11495 */
11496 function has_any_license( $including_foreign = true ) {
11497 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11498 return false;
11499 }
11500
11501 if ( $including_foreign ) {
11502 return true;
11503 }
11504
11505 foreach ( $this->_licenses as $license ) {
11506 if ( $this->_user->id == $license->user_id ) {
11507 return true;
11508 }
11509 }
11510
11511 return false;
11512 }
11513
11514 /**
11515 * @author Vova Feldman (@svovaf)
11516 * @since 1.0.5
11517 *
11518 * @param bool|null $is_localhost
11519 *
11520 * @return FS_Plugin_License|false
11521 */
11522 function _get_available_premium_license( $is_localhost = null ) {
11523 $this->_logger->entrance();
11524
11525 $licenses = $this->get_available_premium_licenses( $is_localhost );
11526 if ( ! empty( $licenses ) ) {
11527 return $licenses[0];
11528 }
11529
11530 return false;
11531 }
11532
11533 /**
11534 * @author Vova Feldman (@svovaf)
11535 * @since 1.0.5
11536 *
11537 * @param bool|null $is_localhost
11538 *
11539 * @return FS_Plugin_License[]
11540 */
11541 function get_available_premium_licenses( $is_localhost = null ) {
11542 $this->_logger->entrance();
11543
11544 $licenses = array();
11545 if ( ! $this->has_paid_plan() ) {
11546 return $licenses;
11547 }
11548
11549 if ( is_array( $this->_licenses ) ) {
11550 foreach ( $this->_licenses as $license ) {
11551 if ( ! $license->can_activate( $is_localhost ) ) {
11552 continue;
11553 }
11554
11555 $licenses[] = $license;
11556 }
11557 }
11558
11559 return $licenses;
11560 }
11561
11562 /**
11563 * Sync local plugin plans with remote server.
11564 *
11565 * 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).
11566 *
11567 * @author Vova Feldman (@svovaf)
11568 * @since 1.0.5
11569 *
11570 * @return FS_Plugin_Plan[]|object
11571 */
11572 function _sync_plans() {
11573 $plans = $this->_fetch_plugin_plans();
11574
11575 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11576 $plans_map = array();
11577 foreach ( $plans as $plan ) {
11578 $plans_map[ $plan->id ] = true;
11579 }
11580
11581 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11582
11583 foreach ( $plans_ids_to_keep as $plan_id ) {
11584 if ( isset( $plans_map[ $plan_id ] ) ) {
11585 continue;
11586 }
11587
11588 $missing_plan = self::_get_plan_by_id( $plan_id, false );
11589
11590 if ( is_object( $missing_plan ) ) {
11591 $plans[] = $missing_plan;
11592 }
11593 }
11594
11595 $this->_plans = $plans;
11596 $this->_store_plans();
11597 }
11598
11599 $this->do_action( 'after_plans_sync', $plans );
11600
11601 return $this->_plans;
11602 }
11603
11604 /**
11605 * Check if specified plan exists locally. If not, fetch it and store it.
11606 *
11607 * @author Vova Feldman (@svovaf)
11608 * @since 2.0.0
11609 *
11610 * @param number $plan_id
11611 *
11612 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11613 */
11614 private function sync_plan_if_not_exist( $plan_id ) {
11615 $plan = self::_get_plan_by_id( $plan_id );
11616
11617 if ( is_object( $plan ) ) {
11618 // Plan already exists.
11619 return $plan;
11620 }
11621
11622 $plan = $this->fetch_plan_by_id( $plan_id );
11623
11624 if ( $plan instanceof FS_Plugin_Plan ) {
11625 $this->_plans[] = $plan;
11626 $this->_store_plans();
11627
11628 return $plan;
11629 }
11630
11631 return $plan;
11632 }
11633
11634 /**
11635 * Check if specified license exists locally. If not, fetch it and store it.
11636 *
11637 * @author Vova Feldman (@svovaf)
11638 * @since 2.0.0
11639 *
11640 * @param number $license_id
11641 * @param string $license_key
11642 *
11643 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11644 */
11645 private function sync_license_if_not_exist( $license_id, $license_key ) {
11646 $license = $this->_get_license_by_id( $license_id );
11647
11648 if ( is_object( $license ) ) {
11649 // License already exists.
11650 return $license;
11651 }
11652
11653 $license = $this->fetch_license_by_key( $license_id, $license_key );
11654
11655 if ( $license instanceof FS_Plugin_License ) {
11656 $this->_licenses[] = $license;
11657
11658 $this->set_license( $license );
11659
11660 $this->_store_licenses();
11661
11662 return $license;
11663 }
11664
11665 return $license;
11666 }
11667
11668 /**
11669 * Get a collection of unique plan IDs that are associated with any installs in the network.
11670 *
11671 * @author Leo Fajardo (@leorw)
11672 * @since 2.0.0
11673 *
11674 * @return number[]
11675 */
11676 private function get_plans_ids_associated_with_installs() {
11677 if ( ! is_multisite() ) {
11678 if ( ! is_object( $this->_site ) ||
11679 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11680 ) {
11681 return array();
11682 }
11683
11684 return array( $this->_site->plan_id );
11685 }
11686
11687 $plan_ids = array();
11688 $sites = self::get_sites();
11689 foreach ( $sites as $site ) {
11690 $blog_id = self::get_site_blog_id( $site );
11691 $install = $this->get_install_by_blog_id( $blog_id );
11692
11693 if ( ! is_object( $install ) ||
11694 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11695 ) {
11696 continue;
11697 }
11698
11699 $plan_ids[ $install->plan_id ] = true;
11700 }
11701
11702 return array_keys( $plan_ids );
11703 }
11704
11705 /**
11706 * Get a collection of unique license IDs that are associated with any installs in the network.
11707 *
11708 * @author Leo Fajardo (@leorw)
11709 * @since 2.0.0
11710 *
11711 * @return number[]
11712 */
11713 private function get_license_ids_associated_with_installs() {
11714 if ( ! $this->_is_network_active ) {
11715 if ( ! is_object( $this->_site ) ||
11716 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11717 ) {
11718 return array();
11719 }
11720
11721 return array( $this->_site->license_id );
11722 }
11723
11724 $license_ids = array();
11725 $sites = self::get_sites();
11726 foreach ( $sites as $site ) {
11727 $blog_id = self::get_site_blog_id( $site );
11728 $install = $this->get_install_by_blog_id( $blog_id );
11729
11730 if ( ! is_object( $install ) ||
11731 ! FS_Plugin_License::is_valid_id( $install->license_id )
11732 ) {
11733 continue;
11734 }
11735
11736 $license_ids[ $install->license_id ] = true;
11737 }
11738
11739 return array_keys( $license_ids );
11740 }
11741
11742 /**
11743 * @author Vova Feldman (@svovaf)
11744 * @since 1.0.5
11745 *
11746 * @param number $id
11747 *
11748 * @return FS_Plugin_Plan|false
11749 */
11750 function _get_plan_by_id( $id, $allow_sync = true ) {
11751 $this->_logger->entrance();
11752
11753 if ( $allow_sync && ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) ) {
11754 $this->_sync_plans();
11755 }
11756
11757 foreach ( $this->_plans as $plan ) {
11758 if ( $id == $plan->id ) {
11759 return $plan;
11760 }
11761 }
11762
11763 return false;
11764 }
11765
11766 /**
11767 * @author Vova Feldman (@svovaf)
11768 * @since 1.1.8.1
11769 *
11770 * @param string $name
11771 *
11772 * @return FS_Plugin_Plan|false
11773 */
11774 private function get_plan_by_name( $name ) {
11775 $this->_logger->entrance();
11776
11777 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11778 $this->_sync_plans();
11779 }
11780
11781 foreach ( $this->_plans as $plan ) {
11782 if ( $name == $plan->name ) {
11783 return $plan;
11784 }
11785 }
11786
11787 return false;
11788 }
11789
11790 /**
11791 * Sync local licenses with remote server.
11792 *
11793 * @author Vova Feldman (@svovaf)
11794 * @since 1.0.6
11795 *
11796 * @param number|bool $site_license_id
11797 * @param number|null $blog_id
11798 *
11799 * @return FS_Plugin_License[]|object
11800 */
11801 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
11802 $this->_logger->entrance();
11803
11804 $is_network_admin = fs_is_network_admin();
11805
11806 if ( $is_network_admin && is_null( $blog_id ) ) {
11807 $all_licenses = self::get_all_licenses( $this->_module_id );
11808 } else {
11809 $all_licenses = $this->get_user_licenses( $this->_user->id );
11810 }
11811
11812 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
11813
11814 $all_licenses_map = array();
11815 foreach ( $all_licenses as $license ) {
11816 $all_licenses_map[ $license->id ] = true;
11817 }
11818
11819 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
11820
11821 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
11822 $licenses_map = array();
11823 foreach ( $licenses as $license ) {
11824 $licenses_map[ $license->id ] = true;
11825 }
11826
11827 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
11828 // foreach ( $license_ids_to_keep as $license_id ) {
11829 // if ( isset( $licenses_map[ $license_id ] ) ) {
11830 // continue;
11831 // }
11832 //
11833 // $missing_license = self::_get_license_by_id( $license_id, false );
11834 // if ( is_object( $missing_license ) ) {
11835 // $licenses[] = $missing_license;
11836 // $licenses_map[ $missing_license->id ] = true;
11837 // }
11838 // }
11839
11840 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
11841
11842 foreach ( $user_license_ids as $key => $license_id ) {
11843 if ( ! isset( $licenses_map[ $license_id ] ) ) {
11844 // Remove access to licenses that no longer exist.
11845 unset( $user_license_ids[ $key ] );
11846 }
11847 }
11848
11849 if ( ! empty( $user_license_ids ) ) {
11850 foreach ( $licenses_map as $license_id => $value ) {
11851 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
11852 // Associate new licenses with the user who triggered the license syncing.
11853 $user_license_ids[] = $license_id;
11854 }
11855 }
11856
11857 $user_license_ids = array_unique( $user_license_ids );
11858 } else {
11859 $user_license_ids = array_keys( $licenses_map );
11860 }
11861
11862 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
11863 $user_licenses = array();
11864 foreach ( $licenses as $license ) {
11865 if ( ! in_array( $license->id, $user_license_ids ) ) {
11866 continue;
11867 }
11868
11869 $user_licenses[] = $license;
11870 }
11871
11872 $this->_licenses = $user_licenses;
11873 } else {
11874 $this->_licenses = $licenses;
11875 }
11876
11877 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
11878
11879 $this->_store_licenses( true, $this->_module_id, $licenses );
11880 }
11881
11882 // Update current license.
11883 if ( is_object( $this->_license ) ) {
11884 $license = $this->_get_license_by_id( $this->_license->id );
11885
11886 if ( is_object( $license ) ) {
11887 /**
11888 * `$license` can be `false` in case a user change action has just been completed and this method
11889 * has synced the `$this->_licenses` collection for the new user. In this case, the
11890 * `$this->_licenses` collection may have only the newly activated license that is associated with
11891 * the new user. `set_license` will eventually be called in the same request by the logic that
11892 * follows outside this method which will detect that the install's license has been updated, and
11893 * then `_update_site_license` will be called which in turn will call `set_license`.
11894 *
11895 * @author Leo Fajardo (@leorw)
11896 * @since 2.3.2
11897 */
11898 $this->set_license( $license );
11899 }
11900 }
11901
11902 return $this->_licenses;
11903 }
11904
11905 /**
11906 * @author Vova Feldman (@svovaf)
11907 * @since 1.0.5
11908 *
11909 * @param number $id
11910 * @param bool $sync_licenses
11911 *
11912 * @return FS_Plugin_License|false
11913 */
11914 function _get_license_by_id( $id, $sync_licenses = true ) {
11915 $this->_logger->entrance();
11916
11917 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
11918 return false;
11919 }
11920
11921 /**
11922 * When running from the network level admin and opted-in from the network,
11923 * check if the license exists in the network user licenses collection.
11924 *
11925 * @author Vova Feldman (@svovaf)
11926 * @since 2.0.0
11927 */
11928 if ( fs_is_network_admin() &&
11929 $this->is_network_registered() &&
11930 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
11931 ) {
11932 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
11933
11934 foreach ( $licenses as $license ) {
11935 if ( $id == $license->id ) {
11936 return $license;
11937 }
11938 }
11939 }
11940
11941 if ( ! $this->has_any_license() && $sync_licenses ) {
11942 $this->_sync_licenses( $id );
11943 }
11944
11945 if ( is_array( $this->_licenses ) ) {
11946 foreach ( $this->_licenses as $license ) {
11947 if ( $id == $license->id ) {
11948 return $license;
11949 }
11950 }
11951 }
11952
11953 return false;
11954 }
11955
11956 /**
11957 * 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.
11958 *
11959 * @author Vova Feldman (@svovaf)
11960 * @since 2.0.0
11961 *
11962 * @param number $id
11963 *
11964 * @return FS_Plugin_License
11965 */
11966 private function get_license_by_id( $id ) {
11967 $licenses = self::get_all_licenses( $this->_module_id );
11968
11969 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
11970 foreach ( $licenses as $license ) {
11971 if ( $id == $license->id ) {
11972 return $license;
11973 }
11974 }
11975 }
11976
11977 return null;
11978 }
11979
11980 /**
11981 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
11982 *
11983 * @author Vova Feldman (@svovaf)
11984 * @since 2.0.0
11985 *
11986 * @return \FS_Plugin_License|mixed
11987 */
11988 private function sync_site_license() {
11989 $api = $this->get_api_user_scope();
11990
11991 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
11992
11993 if ( ! $this->is_api_result_entity( $result ) ) {
11994 return $result;
11995 }
11996
11997 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
11998 $this->_store_licenses();
11999
12000 return $license;
12001 }
12002
12003 /**
12004 * Get all user's available licenses for the current module.
12005 *
12006 * @author Vova Feldman (@svovaf)
12007 * @since 2.0.0
12008 *
12009 * @param number $user_id
12010 *
12011 * @return FS_Plugin_License[]
12012 */
12013 private function get_user_licenses( $user_id ) {
12014 $all_licenses = self::get_all_licenses( $this->_module_id );
12015 if ( empty( $all_licenses ) ) {
12016 return array();
12017 }
12018
12019 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
12020 if ( empty( $user_license_ids ) ) {
12021 return array();
12022 }
12023
12024 $licenses = array();
12025 foreach ( $all_licenses as $license ) {
12026 if ( in_array( $license->id, $user_license_ids ) ) {
12027 $licenses[] = $license;
12028 }
12029 }
12030
12031 return $licenses;
12032 }
12033
12034 /**
12035 * Checks if the context license is network activated except on the given blog ID.
12036 *
12037 * @author Vova Feldman (@svovaf)
12038 * @since 2.0.0
12039 *
12040 * @param int $except_blog_id
12041 *
12042 * @return bool
12043 */
12044 private function is_license_network_active( $except_blog_id = 0 ) {
12045 $this->_logger->entrance();
12046
12047 if ( ! is_object( $this->_license ) ) {
12048 return false;
12049 }
12050
12051 $sites = self::get_sites();
12052
12053 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
12054 // There are more sites than the number of activations, so license cannot be network activated.
12055 return false;
12056 }
12057
12058 foreach ( $sites as $site ) {
12059 $blog_id = self::get_site_blog_id( $site );
12060
12061 if ( $except_blog_id == $blog_id ) {
12062 // Skip excluded blog.
12063 continue;
12064 }
12065
12066 $install = $this->get_install_by_blog_id( $blog_id );
12067
12068 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
12069 return false;
12070 }
12071 }
12072
12073 return true;
12074 }
12075
12076 /**
12077 * 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.
12078 *
12079 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
12080 *
12081 * @author Vova Feldman (@svovaf)
12082 * @since 2.0.0
12083 *
12084 * @param \FS_User $user
12085 * @param \FS_Plugin_License $license
12086 *
12087 * @return bool
12088 */
12089 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
12090 $this->_logger->entrance();
12091
12092 $result = $this->can_activate_license_on_network( $license );
12093
12094 if ( false === $result ) {
12095 return false;
12096 }
12097
12098 $installs_without_license = $result['installs'];
12099 if ( ! empty( $installs_without_license ) ) {
12100 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
12101 }
12102
12103 $disconnected_site_ids = $result['sites'];
12104 if ( ! empty( $disconnected_site_ids ) ) {
12105 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
12106 }
12107
12108 $this->link_license_2_user( $license->id, $user->id );
12109
12110 // Sync license after activations.
12111 $license->activated += $result['production_count'];
12112 $license->activated_local += $result['localhost_count'];
12113
12114 // $this->_store_licenses()
12115
12116 return true;
12117 }
12118
12119 /**
12120 * Checks if the given license can be activated on the whole network.
12121 *
12122 * @author Vova Feldman (@svovaf)
12123 * @since 2.0.0
12124 *
12125 * @param \FS_Plugin_License $license
12126 *
12127 * @return false|array {
12128 * @type array[int]FS_Site $installs Blog ID to install map.
12129 * @type int[] $sites Non-connected blog IDs.
12130 * @type int $production_count Production sites count.
12131 * @type int $localhost_count Production sites count.
12132 * }
12133 */
12134 private function can_activate_license_on_network( FS_Plugin_License $license ) {
12135 $sites = self::get_sites();
12136
12137 $production_count = 0;
12138 $localhost_count = 0;
12139
12140 $installs_without_license = array();
12141 $disconnected_site_ids = array();
12142
12143 foreach ( $sites as $site ) {
12144 $blog_id = self::get_site_blog_id( $site );
12145 $install = $this->get_install_by_blog_id( $blog_id );
12146
12147 if ( is_object( $install ) ) {
12148 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
12149 // License already activated on the install.
12150 continue;
12151 }
12152
12153 $url = $install->url;
12154
12155 $installs_without_license[ $blog_id ] = $install;
12156 } else {
12157 $url = is_object( $site ) ?
12158 $site->siteurl :
12159 self::get_unfiltered_site_url( $blog_id );
12160
12161 $disconnected_site_ids[] = $blog_id;
12162 }
12163
12164 if ( FS_Site::is_localhost_by_address( $url ) ) {
12165 $localhost_count ++;
12166 } else {
12167 $production_count ++;
12168 }
12169 }
12170
12171 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12172 return false;
12173 }
12174
12175 return array(
12176 'installs' => $installs_without_license,
12177 'sites' => $disconnected_site_ids,
12178 'production_count' => $production_count,
12179 'localhost_count' => $localhost_count,
12180 );
12181 }
12182
12183 /**
12184 * Activate a given license on a collection of installs.
12185 *
12186 * @author Vova Feldman (@svovaf)
12187 * @since 2.0.0
12188 *
12189 * @param \FS_User $user
12190 * @param string $license_key
12191 * @param array $blog_2_install_map {
12192 * @key int Blog ID.
12193 * @value FS_Site Blog's associated install.
12194 * }
12195 *
12196 * @return mixed|true
12197 */
12198 private function activate_license_on_many_installs(
12199 FS_User $user,
12200 $license_key,
12201 array $blog_2_install_map
12202 ) {
12203 $params = array(
12204 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12205 );
12206
12207 $install_2_blog_map = array();
12208 foreach ( $blog_2_install_map as $blog_id => $install ) {
12209 $params[] = array( 'id' => $install->id, 'url' => $install->url );
12210
12211 $install_2_blog_map[ $install->id ] = $blog_id;
12212 }
12213
12214 $result = $this->get_api_user_scope_by_user( $user )->call(
12215 "plugins/{$this->_plugin->id}/installs.json",
12216 'PUT',
12217 $params
12218 );
12219
12220 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12221 return $result;
12222 }
12223
12224 foreach ( $result->installs as $r_install ) {
12225 $install = new FS_Site( $r_install );
12226 $install->is_disconnected = false;
12227
12228 // Update install.
12229 $this->_store_site(
12230 true,
12231 $install_2_blog_map[ $r_install->id ],
12232 $install
12233 );
12234 }
12235
12236 return true;
12237 }
12238
12239 /**
12240 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12241 *
12242 * @author Vova Feldman (@svovaf)
12243 * @since 2.3.1
12244 *
12245 * @param \FS_User $user
12246 * @param string $license_key
12247 *
12248 * @return true|mixed True if successful, otherwise, the API result.
12249 */
12250 private function activate_license_on_site( FS_User $user, $license_key ) {
12251 return $this->activate_license_on_many_sites( $user, $license_key );
12252 }
12253
12254 /**
12255 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12256 *
12257 * @author Vova Feldman (@svovaf)
12258 * @since 2.0.0
12259 *
12260 * @param \FS_User $user
12261 * @param string $license_key
12262 * @param int[] $site_ids
12263 *
12264 * @return true|mixed True if successful, otherwise, the API result.
12265 */
12266 private function activate_license_on_many_sites(
12267 FS_User $user,
12268 $license_key,
12269 array $site_ids = array()
12270 ) {
12271 $sites = array();
12272 foreach ( $site_ids as $site_id ) {
12273 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12274 }
12275
12276 // Install the plugin.
12277 $result = $this->create_installs_with_user(
12278 $user,
12279 $license_key,
12280 false,
12281 $sites,
12282 false,
12283 true
12284 );
12285
12286 if ( ! $this->is_api_result_entity( $result ) &&
12287 ! $this->is_api_result_object( $result, 'installs' )
12288 ) {
12289 return $result;
12290 }
12291
12292 $installs = array();
12293
12294 if ( $this->is_api_result_entity( $result ) ) {
12295 $install = new FS_Site( $result );
12296
12297 $this->_user = $user;
12298
12299 $this->_store_site( true, null, $install );
12300
12301 $this->_site = $install;
12302
12303 $this->reset_anonymous_mode();
12304 } else {
12305 foreach ( $result->installs as $install ) {
12306 $installs[] = new FS_Site( $install );
12307 }
12308
12309 // Map site addresses to their blog IDs.
12310 $address_to_blog_map = $this->get_address_to_blog_map();
12311
12312 $first_blog_id = null;
12313
12314 foreach ( $installs as $install ) {
12315 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12316 $blog_id = $address_to_blog_map[ $address ];
12317
12318 $this->_store_site( true, $blog_id, $install );
12319
12320 $this->reset_anonymous_mode( $blog_id );
12321
12322 if ( is_null( $first_blog_id ) ) {
12323 $first_blog_id = $blog_id;
12324 }
12325 }
12326
12327 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12328 $this->_storage->network_install_blog_id = $first_blog_id;
12329 }
12330 }
12331
12332 return true;
12333 }
12334
12335 /**
12336 * Sync site's license with user licenses.
12337 *
12338 * @author Vova Feldman (@svovaf)
12339 * @since 1.0.6
12340 *
12341 * @param FS_Plugin_License|null $new_license
12342 *
12343 * @return FS_Plugin_License|null
12344 */
12345 function _update_site_license( $new_license ) {
12346 $this->_logger->entrance();
12347
12348 /**
12349 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12350 * accordingly so that it will also handle the case when an ownership change is done via license
12351 * activation.
12352 *
12353 * @author Leo Fajardo (@leorw)
12354 * @since 2.3.2
12355 */
12356 $this->set_license( $new_license );
12357
12358 if ( ! is_object( $new_license ) ) {
12359 $this->_site->license_id = null;
12360 $this->_sync_site_subscription( null );
12361
12362 return $this->_license;
12363 }
12364
12365 $this->_site->license_id = $this->_license->id;
12366
12367 if ( ! is_array( $this->_licenses ) ) {
12368 $this->_licenses = array();
12369 }
12370
12371 $is_license_found = false;
12372 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12373 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12374 $this->_licenses[ $i ] = $new_license;
12375
12376 $is_license_found = true;
12377 break;
12378 }
12379 }
12380
12381 // If new license just append.
12382 if ( ! $is_license_found ) {
12383 $this->_licenses[] = $new_license;
12384 }
12385
12386 $this->_sync_site_subscription( $new_license );
12387
12388 return $this->_license;
12389 }
12390
12391 /**
12392 * @author Vova Feldman (@svovaf)
12393 * @since 2.3.1
12394 *
12395 * @param \FS_Plugin_License $license
12396 */
12397 private function set_license( $license = null ) {
12398 $this->_license = $license;
12399
12400 $this->maybe_update_whitelabel_flag( $license );
12401 }
12402
12403 /**
12404 * @author Leo Fajardo (@leorw)
12405 * @since 2.3.1
12406 *
12407 * @param FS_Plugin_License $license
12408 */
12409 private function maybe_update_whitelabel_flag( $license ) {
12410 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12411 $this->_storage->is_whitelabeled :
12412 false;
12413
12414 if ( is_object( $license ) ) {
12415 $license_user = self::_get_user_by_id( $license->user_id );
12416
12417 if ( ! is_object( $license_user ) ) {
12418 // If foreign license, do not update the `is_whitelabeled` flag.
12419 return;
12420 }
12421
12422 if ( $this->is_addon() ) {
12423 /**
12424 * Store the last license data to the parent's storage since it's needed only when showing the
12425 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12426 * iterate over the add-ons just to get the last license data.
12427 */
12428 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12429 } else {
12430 $this->store_last_activated_license_data( $license );
12431 }
12432
12433 if ( $license->is_whitelabeled ) {
12434 // Activated a developer license, data should be hidden.
12435 $is_whitelabeled = true;
12436 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12437 // The account owner activated a regular license key, no need to hide the data.
12438 $is_whitelabeled = false;
12439 }
12440 }
12441
12442 $this->_storage->is_whitelabeled = $is_whitelabeled;
12443
12444 // Reset the whitelabeled status after update.
12445 $this->is_whitelabeled = null;
12446 if ( $this->is_addon() ) {
12447 $parent_fs = $this->get_parent_instance();
12448
12449 if ( is_object( $parent_fs ) ) {
12450 $parent_fs->is_whitelabeled = null;
12451 }
12452 }
12453 }
12454
12455 /**
12456 * @author Leo Fajardo (@leorw)
12457 * @since 2.3.1
12458 *
12459 * @param FS_Plugin_License $license
12460 * @param FS_User $license_user
12461 */
12462 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12463 if ( ! is_object( $license_user ) ) {
12464 $this->_storage->last_license_key = md5( $license->secret_key );
12465 $this->_storage->last_license_user_id = null;
12466 } else {
12467 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12468 $this->_storage->last_license_user_id = $license_user->id;
12469 }
12470 }
12471
12472 /**
12473 * @author Leo Fajardo (@leorw)
12474 * @since 2.3.1
12475 *
12476 * @param bool $ignore_data_debug_mode
12477 *
12478 * @return bool
12479 */
12480 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12481 if ( true !== $this->_storage->is_whitelabeled ) {
12482 return false;
12483 } else if ( $ignore_data_debug_mode ) {
12484 return true;
12485 }
12486
12487 $fs = $this->is_addon() ?
12488 $this->get_parent_instance() :
12489 $this;
12490
12491 return ! $fs->is_data_debug_mode();
12492 }
12493
12494 /**
12495 * @author Leo Fajardo (@leorw)
12496 * @since 2.3.1
12497 *
12498 * @return number
12499 */
12500 function get_last_license_user_id() {
12501 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12502 $this->_storage->last_license_user_id :
12503 null;
12504 }
12505
12506 /**
12507 * @author Leo Fajardo (@leorw)
12508 * @since 2.3.1
12509 *
12510 * @param int $blog_id
12511 * @param bool $ignore_data_debug_mode
12512 *
12513 * @return bool
12514 */
12515 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12516 if ( ! is_null( $blog_id ) ) {
12517 $this->switch_to_blog( $blog_id );
12518 }
12519
12520 if ( ! is_null( $this->is_whitelabeled ) ) {
12521 $is_whitelabeled = $this->is_whitelabeled;
12522 } else {
12523 $is_whitelabeled = false;
12524
12525 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12526
12527 if ( ! $this->has_addons() ) {
12528 $is_whitelabeled = $is_whitelabeled_flag;
12529 } else if ( $is_whitelabeled_flag ) {
12530 $is_whitelabeled = true;
12531 } else {
12532 if ( $this->is_registered() || $this->is_premium() ) {
12533 $addon_ids = $this->get_updated_account_addons();
12534 } else {
12535 $addons = self::get_all_addons();
12536
12537 $plugin_addons = isset( $addons[ $this->_plugin->id ] ) ?
12538 $addons[ $this->_plugin->id ] :
12539 array();
12540
12541 $addon_ids = array();
12542 foreach ( $plugin_addons as $addon ) {
12543 $addon_ids[] = $addon->id;
12544 }
12545 }
12546
12547 $installed_addons = $this->get_installed_addons();
12548 foreach ( $installed_addons as $fs_addon ) {
12549 $addon_ids[] = $fs_addon->get_id();
12550 }
12551
12552 if ( ! empty( $addon_ids ) ) {
12553 $addon_ids = array_unique( $addon_ids );
12554
12555 $is_network_level = (
12556 fs_is_network_admin() &&
12557 $this->is_network_active()
12558 );
12559
12560 foreach ( $addon_ids as $addon_id ) {
12561 $addon = $this->get_addon( $addon_id );
12562
12563 if ( ! is_object( $addon ) ) {
12564 continue;
12565 }
12566
12567 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12568 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12569 self::get_addon_instance( $addon_id ) :
12570 null;
12571
12572 $was_addon_network_activated = false;
12573
12574 if ( is_object( $fs_addon ) ) {
12575 $was_addon_network_activated = $fs_addon->is_network_active();
12576 } else if ( $is_network_level ) {
12577 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12578 }
12579
12580 $network_delegated_connection = (
12581 $was_addon_network_activated &&
12582 $addon_storage->get( 'is_delegated_connection', false, true )
12583 );
12584
12585 if (
12586 $is_network_level &&
12587 ( ! $was_addon_network_activated || $network_delegated_connection )
12588 ) {
12589 $sites = self::get_sites();
12590
12591 /**
12592 * If in network admin area and the add-on was not network-activated or network-activated
12593 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12594 */
12595 foreach ( $sites as $site ) {
12596 $site_info = $this->get_site_info( $site );
12597
12598 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12599 $is_whitelabeled = true;
12600 break;
12601 }
12602 }
12603
12604 if ( $is_whitelabeled ) {
12605 break;
12606 }
12607 } else {
12608 /**
12609 * This will be executed when any of the following is met:
12610 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12611 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12612 * 3. Add-on was not network-activated and in site admin area.
12613 */
12614 if ( true === $addon_storage->is_whitelabeled ) {
12615 $is_whitelabeled = true;
12616 break;
12617 }
12618 }
12619 }
12620 }
12621 }
12622
12623 $this->is_whitelabeled = $is_whitelabeled;
12624
12625 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12626 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12627 }
12628
12629 if ( ! is_null( $blog_id ) ) {
12630 $this->restore_current_blog();
12631 }
12632 }
12633
12634 return (
12635 $is_whitelabeled &&
12636 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12637 );
12638 }
12639
12640 /**
12641 * Sync site's subscription.
12642 *
12643 * @author Vova Feldman (@svovaf)
12644 * @since 1.0.9
12645 *
12646 * @param FS_Plugin_License|null $license
12647 *
12648 * @return bool|\FS_Subscription
12649 */
12650 private function _sync_site_subscription( $license ) {
12651 if ( ! is_object( $license ) ) {
12652 $this->delete_unused_subscriptions();
12653
12654 return false;
12655 }
12656
12657 // Load subscription details if not lifetime.
12658 $subscription = $license->is_lifetime() ?
12659 false :
12660 $this->_fetch_site_license_subscription();
12661
12662 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12663 $this->store_subscription( $subscription );
12664 } else {
12665 $this->delete_unused_subscriptions();
12666 }
12667
12668 return $subscription;
12669 }
12670
12671 /**
12672 * @author Vova Feldman (@svovaf)
12673 * @since 1.0.6
12674 *
12675 * @return bool|\FS_Plugin_License
12676 */
12677 function _get_license() {
12678 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12679 return $this->_license;
12680 }
12681
12682 return $this->_get_available_premium_license();
12683 }
12684
12685 /**
12686 * @param number $license_id
12687 *
12688 * @return null|\FS_Subscription
12689 */
12690 function _get_subscription( $license_id ) {
12691 if ( ! isset( $this->_storage->subscriptions ) ||
12692 empty( $this->_storage->subscriptions )
12693 ) {
12694 return null;
12695 }
12696
12697 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12698 if ( $subscription->license_id == $license_id ) {
12699 return $subscription;
12700 }
12701 }
12702
12703 return null;
12704 }
12705
12706 /**
12707 * @author Leo Fajardo (@leorw)
12708 * @since 2.0.0
12709 *
12710 * @param FS_Subscription $subscription
12711 */
12712 function store_subscription( FS_Subscription $subscription ) {
12713 if ( ! isset( $this->_storage->subscriptions ) ) {
12714 $this->_storage->subscriptions = array();
12715 }
12716
12717 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12718 $this->_storage->subscriptions = array( $subscription );
12719
12720 return;
12721 }
12722
12723 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
12724
12725 $updated_subscription = false;
12726 foreach ( $subscriptions as $key => $existing_subscription ) {
12727 if ( $existing_subscription->id == $subscription->id ) {
12728 $subscriptions[ $key ] = $subscription;
12729 $updated_subscription = true;
12730 break;
12731 }
12732 }
12733
12734 if ( ! $updated_subscription ) {
12735 $subscriptions[] = $subscription;
12736 }
12737
12738 $this->_storage->subscriptions = $subscriptions;
12739 }
12740
12741 /**
12742 * @author Leo Fajardo (@leorw)
12743 * @since 2.0.0
12744 */
12745 function delete_unused_subscriptions() {
12746 if ( ! isset( $this->_storage->subscriptions ) ||
12747 empty( $this->_storage->subscriptions ) ||
12748 // Clean up only if there are already at least 3 subscriptions.
12749 ( count( $this->_storage->subscriptions ) < 3 )
12750 ) {
12751 return;
12752 }
12753
12754 if ( ! is_multisite() ) {
12755 // If not multisite, there should only be 1 subscription, so just clear the array.
12756 $this->_storage->subscriptions = array();
12757
12758 return;
12759 }
12760
12761 $subscriptions_to_keep_by_license_id_map = array();
12762 $sites = self::get_sites();
12763 foreach ( $sites as $site ) {
12764 $blog_id = self::get_site_blog_id( $site );
12765 $install = $this->get_install_by_blog_id( $blog_id );
12766
12767 if ( ! is_object( $install ) ||
12768 ! FS_Plugin_License::is_valid_id( $install->license_id )
12769 ) {
12770 continue;
12771 }
12772
12773 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
12774 }
12775
12776 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
12777 $this->_storage->subscriptions = array();
12778
12779 return;
12780 }
12781
12782 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
12783 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
12784 unset( $this->_storage->subscriptions[ $key ] );
12785 }
12786 }
12787 }
12788
12789 /**
12790 * @author Vova Feldman (@svovaf)
12791 * @since 1.0.2
12792 *
12793 * @param string $plan Plan name
12794 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12795 *
12796 * @return bool
12797 */
12798 function is_plan( $plan, $exact = false ) {
12799 $this->_logger->entrance();
12800
12801 if ( ! $this->is_registered() ) {
12802 return false;
12803 }
12804
12805 $plan = strtolower( $plan );
12806
12807 $current_plan_name = $this->get_plan_name();
12808
12809 if ( $current_plan_name === $plan ) {
12810 // Exact plan.
12811 return true;
12812 } else if ( $exact ) {
12813 // Required exact, but plans are different.
12814 return false;
12815 }
12816
12817 $current_plan_order = - 1;
12818 $required_plan_order = PHP_INT_MAX;
12819 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12820 if ( $plan === $this->_plans[ $i ]->name ) {
12821 $required_plan_order = $i;
12822 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
12823 $current_plan_order = $i;
12824 }
12825 }
12826
12827 return ( $current_plan_order > $required_plan_order );
12828 }
12829
12830 /**
12831 * Check if module has only one plan.
12832 *
12833 * @author Vova Feldman (@svovaf)
12834 * @since 1.2.1.7
12835 *
12836 * @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.
12837 *
12838 * @return bool
12839 */
12840 function is_single_plan( $double_check = false ) {
12841 $this->_logger->entrance();
12842
12843 if ( ! $this->is_registered() ||
12844 ! is_array( $this->_plans ) ||
12845 0 === count( $this->_plans )
12846 ) {
12847 return true;
12848 }
12849
12850 $has_free_plan = $this->has_free_plan();
12851
12852 if ( ! $has_free_plan && $double_check ) {
12853 foreach ( $this->_plans as $plan ) {
12854 if ( $plan->is_free() ) {
12855 $has_free_plan = true;
12856 break;
12857 }
12858 }
12859 }
12860
12861 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
12862 }
12863
12864 /**
12865 * Check if plan based on trial. If not in trial mode, should return false.
12866 *
12867 * @since 1.0.9
12868 *
12869 * @param string $plan Plan name
12870 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12871 *
12872 * @return bool
12873 */
12874 function is_trial_plan( $plan, $exact = false ) {
12875 $this->_logger->entrance();
12876
12877 if ( ! $this->is_registered() ) {
12878 return false;
12879 }
12880
12881 if ( ! $this->is_trial() ) {
12882 return false;
12883 }
12884
12885 $trial_plan = $this->get_trial_plan();
12886
12887 if ( $trial_plan->name === $plan ) {
12888 // Exact plan.
12889 return true;
12890 } else if ( $exact ) {
12891 // Required exact, but plans are different.
12892 return false;
12893 }
12894
12895 $current_plan_order = - 1;
12896 $required_plan_order = - 1;
12897 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12898 if ( $plan === $this->_plans[ $i ]->name ) {
12899 $required_plan_order = $i;
12900 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
12901 $current_plan_order = $i;
12902 }
12903 }
12904
12905 return ( $current_plan_order > $required_plan_order );
12906 }
12907
12908 /**
12909 * Check if plugin has any paid plans.
12910 *
12911 * @author Vova Feldman (@svovaf)
12912 * @since 1.0.7
12913 *
12914 * @return bool
12915 */
12916 function has_paid_plan() {
12917 return $this->_has_paid_plans ||
12918 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
12919 }
12920
12921 /**
12922 * Check if plugin has any plan with a trail.
12923 *
12924 * @author Vova Feldman (@svovaf)
12925 * @since 1.0.9
12926 *
12927 * @return bool
12928 */
12929 function has_trial_plan() {
12930 /**
12931 * @author Vova Feldman(@svovaf)
12932 * @since 1.2.1.5
12933 *
12934 * Allow setting a trial from the SDK without calling the API.
12935 * But, if the user did opt-in, continue using the real data from the API.
12936 */
12937 if ( $this->_trial_days >= 0 ) {
12938 return true;
12939 }
12940
12941 return $this->_storage->get( 'has_trial_plan', false );
12942 }
12943
12944 /**
12945 * Check if plugin has any free plan, or is it premium only.
12946 *
12947 * Note: If no plans configured, assume plugin is free.
12948 *
12949 * @author Vova Feldman (@svovaf)
12950 * @since 1.0.7
12951 *
12952 * @return bool
12953 */
12954 function has_free_plan() {
12955 return ! $this->is_only_premium();
12956 }
12957
12958 /**
12959 * Displays a license activation dialog box when the user clicks on the "Activate License"
12960 * or "Change License" link on the plugins
12961 * page.
12962 *
12963 * @author Leo Fajardo (@leorw)
12964 * @since 1.1.9
12965 */
12966 function _add_license_activation_dialog_box() {
12967 $vars = array(
12968 'id' => $this->_module_id,
12969 );
12970
12971 fs_require_template( 'forms/license-activation.php', $vars );
12972 fs_require_template( 'forms/resend-key.php', $vars );
12973 }
12974
12975 /**
12976 * Displays an email address update dialog box when the user clicks on the email address "Edit" button on the "Account" page.
12977 *
12978 * @author Leo Fajardo (@leorw)
12979 * @since 2.5.0
12980 */
12981 function _add_email_address_update_dialog_box() {
12982 $vars = array( 'id' => $this->_module_id );
12983
12984 fs_require_template( 'forms/email-address-update.php', $vars );
12985 }
12986
12987 /**
12988 * @author Leo Fajardo (@leorw)
12989 * @since 2.5.0
12990 */
12991 function _add_email_address_update_option() {
12992 if ( ! $this->should_handle_user_change() ) {
12993 return;
12994 }
12995
12996 // Add email address update AJAX handler.
12997 $this->add_ajax_action( 'update_email_address', array( &$this, '_email_address_update_ajax_handler' ) );
12998 }
12999
13000 /**
13001 * @author Leo Fajardo (@leorw)
13002 * @since 2.5.0
13003 */
13004 function _email_address_update_ajax_handler() {
13005 $this->check_ajax_referer( 'update_email_address' );
13006
13007 $new_email_address = fs_request_get( 'email_address' );
13008 $transfer_type = fs_request_get( 'transfer_type' );
13009
13010 $result = $this->update_email( $new_email_address );
13011
13012 if ( ! FS_Api::is_api_error( $result ) ) {
13013 self::shoot_ajax_success();
13014 }
13015
13016 $error = '';
13017
13018 if ( FS_Api::is_api_error_object( $result ) ) {
13019 switch ( $result->error->code ) {
13020 case 'user_exist':
13021 case 'account_verification_required':
13022 $error = array(
13023 'code' => 'change_ownership',
13024 'url' => $this->get_account_url( 'change_owner', array(
13025 'state' => 'init',
13026 'candidate_email' => $new_email_address,
13027 'transfer_type' => $transfer_type,
13028 ) ),
13029 );
13030
13031 break;
13032 }
13033 }
13034
13035 if ( empty( $error ) ) {
13036 $error = is_object( $result ) ?
13037 var_export( $result->error, true ) :
13038 $result;
13039 }
13040
13041 self::shoot_ajax_failure( $error );
13042 }
13043
13044 /**
13045 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
13046 *
13047 * @author Leo Fajardo (@leorw)
13048 * @since 2.3.2
13049 *
13050 * @return number[]
13051 */
13052 function get_installs_ids_with_foreign_licenses() {
13053 $installs = array();
13054
13055 if (
13056 is_object( $this->_license ) &&
13057 $this->_site->user_id != $this->_license->user_id
13058 ) {
13059 $installs[] = $this->_site->id;
13060 }
13061
13062 /**
13063 * Also try to get foreign licenses for the context product's add-ons.
13064 */
13065 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
13066
13067 foreach ( $installs_by_slug_map as $slug => $install_info ) {
13068 if ( $slug == $this->get_slug() ) {
13069 continue;
13070 }
13071
13072 $install = $install_info['install'];
13073 $license = $install_info['license'];
13074
13075 if (
13076 is_object( $license ) &&
13077 $install->user_id != $license->user_id
13078 ) {
13079 $installs[] = $install->id;
13080 }
13081 }
13082
13083 return $installs;
13084 }
13085
13086 /**
13087 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
13088 *
13089 * @author Leo Fajardo (@leorw)
13090 * @since 2.3.2
13091 *
13092 * @param number[] $install_ids
13093 */
13094 function _add_user_change_dialog_box( $install_ids ) {
13095 $vars = array(
13096 'id' => $this->_module_id,
13097 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
13098 );
13099
13100 fs_require_template( 'forms/user-change.php', $vars );
13101 }
13102
13103 /**
13104 * @author Leo Fajardo (@leorw)
13105 * @since 2.3.1
13106 */
13107 function _add_data_debug_mode_dialog_box() {
13108 $vars = array(
13109 'id' => $this->_module_id,
13110 );
13111
13112 fs_require_template( 'forms/data-debug-mode.php', $vars );
13113 }
13114
13115 /**
13116 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
13117 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
13118 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
13119 * is only activated on a single production site.
13120 *
13121 * @author Leo Fajardo (@leorw)
13122 * @since 2.2.1
13123 *
13124 * @param bool $is_license_deactivation
13125 *
13126 * @return array
13127 */
13128 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
13129 if ( fs_is_network_admin() ) {
13130 // Subscription cancellation dialog box is currently not supported for multisite networks.
13131 return array();
13132 }
13133
13134 if ( $this->is_whitelabeled() ) {
13135 return array();
13136 }
13137
13138 $license = $this->_get_license();
13139
13140 /**
13141 * 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.
13142 *
13143 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
13144 * @since 2.2.1
13145 */
13146 if ( ! is_object( $license ) ||
13147 $license->is_lifetime() ||
13148 ( ! $license->is_single_site() && $license->activated > 1 )
13149 ) {
13150 return array();
13151 }
13152
13153 /**
13154 * @var FS_Subscription $subscription
13155 */
13156 $subscription = $this->_get_subscription( $license->id );
13157 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
13158 return array();
13159 }
13160
13161 return array(
13162 'id' => $this->_module_id,
13163 'license' => $license,
13164 'has_trial' => $this->is_paid_trial(),
13165 'is_license_deactivation' => $is_license_deactivation,
13166 );
13167 }
13168
13169 /**
13170 * @author Leo Fajardo (@leorw)
13171 * @since 2.0.2
13172 */
13173 function _add_premium_version_upgrade_selection_dialog_box() {
13174 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
13175 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
13176 return;
13177 }
13178
13179 $vars = array(
13180 'id' => $this->_module_id,
13181 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
13182 $modules_update->response[ $this->_plugin_basename ]->new_version :
13183 $modules_update->response[ $this->_plugin_basename ]['new_version']
13184 );
13185
13186 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13187 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13188 }
13189
13190 /**
13191 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13192 * page.
13193 *
13194 * @author Leo Fajardo (@leorw)
13195 * @since 1.2.1.5
13196 */
13197 function _add_optout_dialog() {
13198 if ( $this->is_theme() ) {
13199 $vars = null;
13200 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13201 }
13202
13203 $vars = array( 'id' => $this->_module_id );
13204 fs_require_template( 'forms/optout.php', $vars );
13205 }
13206
13207 /**
13208 * Prepare page to include all required UI and logic for the license activation dialog.
13209 *
13210 * @author Vova Feldman (@svovaf)
13211 * @since 1.2.0
13212 */
13213 function _add_license_activation() {
13214 if ( $this->is_migration() ) {
13215 return;
13216 }
13217
13218 if ( ! $this->is_user_admin() ) {
13219 // Only admins can activate a license.
13220 return;
13221 }
13222
13223 if ( ! $this->has_paid_plan() ) {
13224 // Module doesn't have any paid plans.
13225 return;
13226 }
13227
13228 if (
13229 $this->has_premium_version() &&
13230 ! $this->is_premium() &&
13231 /**
13232 * Also handle the case when an upgrade was made using the free version.
13233 *
13234 * @author Leo Fajardo (@leorw)
13235 * @since 2.3.2
13236 */
13237 ! is_object( $this->_get_license() )
13238 ) {
13239 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13240 return;
13241 }
13242
13243 // Add license activation link and AJAX request handler.
13244 if ( self::is_plugins_page() ) {
13245 $is_network_admin = fs_is_network_admin();
13246
13247 if (
13248 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13249 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13250 ) {
13251 if (
13252 $this->is_premium() ||
13253 ( $this->has_paid_plan() && ! $this->has_premium_version() )
13254 ) {
13255 /**
13256 * @since 1.2.0 Add license action link only on plugins page.
13257 */
13258 $this->_add_license_action_link();
13259 }
13260 }
13261 }
13262
13263 // Add license activation AJAX callback.
13264 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13265
13266 // Add resend license AJAX callback.
13267 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13268 }
13269
13270 /**
13271 * Prepares page to include all required UI and logic for the "Change User" dialog.
13272 *
13273 * @author Leo Fajardo (@leorw)
13274 * @since 2.3.2
13275 */
13276 function _add_user_change_option() {
13277 if ( ! $this->should_handle_user_change() ) {
13278 return;
13279 }
13280
13281 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13282
13283 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13284 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13285 return;
13286 }
13287
13288 // Add user change AJAX handler.
13289 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13290 }
13291
13292 /**
13293 * @author Leo Fajardo (@leorw)
13294 * @since 2.3.2
13295 */
13296 function should_handle_user_change() {
13297 if ( ! $this->is_user_admin() ) {
13298 // Only admins can change user.
13299 return false;
13300 }
13301
13302 if ( $this->is_addon() ) {
13303 return false;
13304 }
13305
13306 if ( ! $this->is_registered() ) {
13307 return false;
13308 }
13309
13310 if (
13311 $this->is_network_active() &&
13312 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13313 ) {
13314 // Handle only on site-level "Account" section for now.
13315 return false;
13316 }
13317
13318 return true;
13319 }
13320
13321 /**
13322 * @author Leo Fajardo (@leorw)
13323 * @since 2.0.2
13324 */
13325 function _add_premium_version_upgrade_selection() {
13326 if ( ! $this->is_user_admin() ) {
13327 return;
13328 }
13329
13330 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13331 // This is relevant only to the free versions and premium versions without an active license.
13332 return;
13333 }
13334
13335 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13336 $this->_add_premium_version_upgrade_selection_action();
13337 }
13338 }
13339
13340 /**
13341 * @author Edgar Melkonyan
13342 * @since 2.4.1
13343 *
13344 * @throws Freemius_Exception
13345 */
13346 function _toggle_whitelabel_mode_ajax_handler() {
13347 $this->_logger->entrance();
13348
13349 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13350
13351 if ( ! $this->is_user_admin() ) {
13352 // Only for admins.
13353 self::shoot_ajax_failure();
13354 }
13355
13356 $license = $this->get_api_user_scope()->call(
13357 "/licenses/{$this->_site->license_id}.json",
13358 'put',
13359 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13360 );
13361
13362 if ( ! $this->is_api_result_entity( $license ) ) {
13363 self::shoot_ajax_failure(
13364 FS_Api::is_api_error_object( $license ) ?
13365 $license->error->message :
13366 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13367 );
13368 }
13369
13370 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13371 $this->_store_licenses();
13372
13373 $this->_sync_license();
13374
13375 if ( ! $license->is_whitelabeled ) {
13376 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13377 } else {
13378 $this->_admin_notices->add_sticky(
13379 sprintf(
13380 $this->get_text_inline(
13381 '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.',
13382 'license_whitelabeled'
13383 ),
13384 "<strong>{$this->get_plugin_title()}</strong>",
13385 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13386 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13387 ),
13388 'license_whitelabeled'
13389 );
13390 }
13391
13392 self::shoot_ajax_response( array( 'success' => true ) );
13393 }
13394
13395 /**
13396 * @author Leo Fajardo (@leorw)
13397 * @since 2.3.0
13398 */
13399 function _add_beta_mode_update_handler() {
13400 if ( ! $this->is_user_admin() ) {
13401 return;
13402 }
13403
13404 if ( ! $this->is_premium() ) {
13405 return;
13406 }
13407
13408 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13409 }
13410
13411 /**
13412 * @author Leo Fajardo (@leorw)
13413 * @since 2.3.0
13414 */
13415 function _set_beta_mode_ajax_handler() {
13416 $this->_logger->entrance();
13417
13418 $this->check_ajax_referer( 'set_beta_mode' );
13419
13420 if ( ! $this->is_user_admin() ) {
13421 // Only for admins.
13422 self::shoot_ajax_failure();
13423 }
13424
13425 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13426
13427 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13428 self::shoot_ajax_failure();
13429 }
13430
13431 $site = $this->api_site_call(
13432 '',
13433 'put',
13434 array(
13435 'is_beta' => ( 'true' == $is_beta ),
13436 'fields' => 'is_beta'
13437 )
13438 );
13439
13440 if ( ! $this->is_api_result_entity( $site ) ) {
13441 self::shoot_ajax_failure(
13442 FS_Api::is_api_error_object( $site ) ?
13443 $site->error->message :
13444 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13445 );
13446 }
13447
13448 $this->_site->is_beta = $site->is_beta;
13449 $this->_store_site();
13450
13451 self::shoot_ajax_response( array( 'success' => true ) );
13452 }
13453
13454 /**
13455 * License activation WP AJAX handler.
13456 *
13457 * @author Leo Fajardo (@leorw)
13458 * @since 1.1.9
13459 *
13460 * @uses Freemius::activate_license()
13461 */
13462 function _activate_license_ajax_action() {
13463 $this->_logger->entrance();
13464
13465 $this->check_ajax_referer( 'activate_license' );
13466
13467 $license_key = trim( fs_request_get_raw( 'license_key' ) );
13468
13469 if ( empty( $license_key ) ) {
13470 $license_id = trim( fs_request_get_raw( 'license_id' ) );
13471
13472 if ( FS_Plugin_License::is_valid_id( $license_id ) ) {
13473 $license = $this->_get_license_by_id( $license_id, false );
13474
13475 if ( is_object( $license ) ) {
13476 $license_key = $license->secret_key;
13477 }
13478 }
13479 }
13480
13481 if ( empty( $license_key ) ) {
13482 exit;
13483 }
13484
13485 $sites = fs_is_network_admin() ?
13486 fs_request_get( 'sites', array(), 'post' ) :
13487 array();
13488
13489 $result = $this->activate_license(
13490 $license_key,
13491 $sites,
13492 fs_request_get_bool( 'is_marketing_allowed', null ),
13493 fs_request_get( 'blog_id', null ),
13494 fs_request_get( 'module_id', null, 'post' ),
13495 fs_request_get( 'user_id', null ),
13496 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
13497 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
13498 fs_request_get( 'license_owner_id', null )
13499 );
13500
13501 if (
13502 $result['success'] &&
13503 $this->is_bundle_license_auto_activation_enabled()
13504 ) {
13505 $license = new FS_Plugin_License();
13506 $license->secret_key = $license_key;
13507
13508 $this->maybe_activate_bundle_license( $license, $sites );
13509 }
13510
13511 echo json_encode( $result );
13512
13513 exit;
13514 }
13515
13516 /**
13517 * User change WP AJAX handler.
13518 *
13519 * @author Leo Fajardo (@leorw)
13520 * @since 2.3.2
13521 */
13522 function _user_change_ajax_action() {
13523 $this->_logger->entrance();
13524
13525 $this->check_ajax_referer( 'change_user' );
13526
13527 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13528 $new_user_id = fs_request_get( 'user_id' );
13529
13530 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13531 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13532 }
13533
13534 $params = array();
13535
13536 if ( ! empty( $new_email_address ) ) {
13537 $params['user_email'] = $new_email_address;
13538 } else {
13539 $params['user_id'] = $new_user_id;
13540 }
13541
13542 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13543 $install_ids = array();
13544
13545 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13546 $install_ids[ $slug ] = $install_info['install']->id;
13547 }
13548
13549 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13550
13551 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13552
13553 if ( FS_Api::is_api_error( $install ) ) {
13554 $error = '';
13555
13556 if ( is_object( $install ) ) {
13557 switch ( $install->error->code ) {
13558 case 'user_exist':
13559 $error = (
13560 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13561 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
13562 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>' ) .
13563 sprintf(
13564 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13565 $this->get_account_url( 'change_owner', array(
13566 'state' => 'init',
13567 'candidate_email' => $new_email_address
13568 ) ),
13569 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
13570 )
13571 );
13572 break;
13573 }
13574 }
13575
13576 if ( empty( $error ) ) {
13577 $error = FS_Api::is_api_error_object( $install ) ?
13578 $install->error->message :
13579 var_export( $install->error, true );
13580 }
13581
13582 self::shoot_ajax_failure( $error );
13583 } else {
13584 if (
13585 // If successful ownership change.
13586 $this->get_user()->id != $install->user_id ||
13587 ! empty( $new_email_address )
13588 ) {
13589 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
13590 }
13591 }
13592
13593 self::shoot_ajax_success();
13594 }
13595
13596 /**
13597 * @author Leo Fajardo (@leorw)
13598 * @since 2.3.2.14
13599 */
13600 function starting_migration() {
13601 if ( ! empty( $this->_storage->license_migration ) ) {
13602 // Do not overwrite the data if already set.
13603 return;
13604 }
13605
13606 $this->_storage->license_migration = array(
13607 'is_migrating' => true,
13608 'start_timestamp' => time()
13609 );
13610 }
13611
13612 /**
13613 * @author Leo Fajardo (@leorw)
13614 * @since 2.3.2.14
13615 */
13616 function is_migration() {
13617 if ( $this->is_addon() ) {
13618 return $this->get_parent_instance()->is_migration();
13619 }
13620
13621 if ( empty( $this->_storage->license_migration ) ) {
13622 return false;
13623 }
13624
13625 if ( ! $this->_storage->license_migration['is_migrating'] ) {
13626 return false;
13627 }
13628
13629 return (
13630 // Return `true` if the migration is within 5 minutes from the starting time.
13631 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
13632 );
13633 }
13634
13635 /**
13636 *
13637 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
13638 *
13639 * @author Vova Feldman (@svovaf)
13640 * @since 2.3.0
13641 *
13642 * @param string $license_key
13643 * @param null|bool $is_marketing_allowed
13644 * @param null|number $plugin_id
13645 * @param array $sites
13646 * @param int $blog_id
13647 * @param null|number $license_owner_id
13648 *
13649 * @return array {
13650 * @var bool $success
13651 * @var string $error
13652 * @var string $next_page
13653 * }
13654 *
13655 * @uses Freemius::activate_license()
13656 */
13657 function activate_migrated_license(
13658 $license_key,
13659 $is_marketing_allowed = null,
13660 $plugin_id = null,
13661 $sites = array(),
13662 $blog_id = null,
13663 $license_owner_id = null
13664 ) {
13665 $this->_logger->entrance();
13666
13667 $result = $this->activate_license(
13668 $license_key,
13669 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
13670 $this->get_sites_for_network_level_optin() :
13671 $sites,
13672 $is_marketing_allowed,
13673 $blog_id,
13674 $plugin_id,
13675 null,
13676 null,
13677 null,
13678 $license_owner_id
13679 );
13680
13681 // No need to show the sticky after license activation notice after migrating a license.
13682 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13683
13684 return $result;
13685 }
13686
13687 /**
13688 * @author Leo Fajardo (@leorw)
13689 * @since 2.3.1
13690 *
13691 * @return string
13692 */
13693 function get_pricing_js_path() {
13694 if ( ! isset( $this->_pricing_js_path ) ) {
13695 $default_path = WP_FS__DIR_JS . '/pricing/freemius-pricing.js';
13696
13697 $pricing_js_path = $this->apply_filters( 'freemius_pricing_js_path', $default_path );
13698
13699 // Backward compatibility for people who placed the freemius-pricing inside `includes` directory. Let it take more preference than the default path.
13700 if ( empty( $pricing_js_path ) ) {
13701 global $fs_active_plugins;
13702
13703 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
13704 if ( $data->plugin_path == $this->get_plugin_basename() ) {
13705 $plugin_or_theme_root_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
13706
13707 $pricing_js_path = $plugin_or_theme_root_dir
13708 . '/'
13709 // The basename will be `plugins`, `themes`, or the basename of a custom plugins or themes directory.
13710 . str_replace( '../' . basename( $plugin_or_theme_root_dir ) . '/', '', $sdk_path )
13711 . '/includes/freemius-pricing/freemius-pricing.js';
13712
13713 break;
13714 }
13715 }
13716 }
13717
13718 // If it is still empty, load the default pricing JS.
13719 if ( ! file_exists( $pricing_js_path ) ) {
13720 $pricing_js_path = $default_path;
13721 }
13722
13723 $this->_pricing_js_path = $pricing_js_path;
13724 }
13725
13726 return $this->_pricing_js_path;
13727 }
13728
13729 /**
13730 * @author Leo Fajardo (@leorw)
13731 * @since 2.3.1
13732 *
13733 * @deprecated Since v2.9.0 we have removed the iFrame based pricing. This will always return `false`.
13734 *
13735 * @return bool
13736 */
13737 function should_use_external_pricing() {
13738 return false;
13739 }
13740
13741 /**
13742 * The implementation of this method was previously in `_activate_license_ajax_action()`.
13743 *
13744 * @author Vova Feldman (@svovaf)
13745 * @since 2.2.4
13746 * @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).
13747 * @param string $license_key
13748 * @param array $sites
13749 * @param null|bool $is_marketing_allowed
13750 * @param null|int $blog_id
13751 * @param null|number $plugin_id
13752 * @param null|number $user_id
13753 * @param bool|null $is_extensions_tracking_allowed
13754 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint.
13755 * @param null|number $license_owner_id
13756 *
13757 *
13758 * @return array {
13759 * @var bool $success
13760 * @var string $error
13761 * @var string $next_page
13762 * }
13763 */
13764 private function activate_license(
13765 $license_key,
13766 $sites = array(),
13767 $is_marketing_allowed = null,
13768 $blog_id = null,
13769 $plugin_id = null,
13770 $user_id = null,
13771 $is_extensions_tracking_allowed = null,
13772 $is_diagnostic_tracking_allowed = null,
13773 $license_owner_id = null
13774 ) {
13775 $this->_logger->entrance();
13776
13777 $license_key = trim( $license_key );
13778
13779 $is_network_activation_or_migration = (
13780 fs_is_network_admin() ||
13781 ( ! empty( $sites ) && $this->is_migration() )
13782 );
13783
13784 if ( ! $is_network_activation_or_migration ) {
13785 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
13786 $sites = array();
13787 }
13788
13789 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
13790 $this :
13791 $this->get_addon_instance( $plugin_id );
13792
13793 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
13794 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
13795 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
13796 ) );
13797
13798 $error = false;
13799 $next_page = false;
13800
13801 $has_valid_blog_id = is_numeric( $blog_id );
13802
13803 $user = null;
13804
13805 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
13806 /**
13807 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
13808 *
13809 * @author Vova Feldman (@svovaf)
13810 */
13811 $user = $fs->get_parent_instance()->get_current_or_network_user();
13812 } else if ( $fs->is_registered() ) {
13813 $user = $fs->get_current_or_network_user();
13814 }
13815
13816 if ( $has_valid_blog_id ) {
13817 /**
13818 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
13819 *
13820 * @author Leo Fajardo (@leorw)
13821 */
13822 $fs->switch_to_blog( $blog_id );
13823 }
13824
13825 if ( is_object( $user ) ) {
13826 $result = true;
13827
13828 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
13829 // If no specific blog ID was provided, activate the license for all sites in the network.
13830 $blog_2_install_map = array();
13831 $site_ids = array();
13832
13833 foreach ( $sites as $site ) {
13834 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
13835 continue;
13836 }
13837
13838 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
13839
13840 if ( is_object( $install ) ) {
13841 $blog_2_install_map[ $site['blog_id'] ] = $install;
13842 } else {
13843 $site_ids[] = $site['blog_id'];
13844 }
13845 }
13846
13847 if ( ! empty( $blog_2_install_map ) ) {
13848 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
13849 }
13850
13851 if ( true === $result && ! empty( $site_ids ) ) {
13852 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
13853 }
13854 } else {
13855 if ( $fs->is_registered() ) {
13856 $params = array(
13857 'license_key' => $fs->apply_filters( 'license_key', $license_key )
13858 );
13859
13860 $install_ids = array();
13861
13862 $change_owner = FS_User::is_valid_id( $user_id );
13863
13864 if ( $change_owner ) {
13865 $params['user_id'] = $user_id;
13866
13867 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
13868
13869 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13870 $install_ids[ $slug ] = $install_info['install']->id;
13871 }
13872
13873 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13874 }
13875
13876 $api = $fs->get_api_site_scope();
13877
13878 $result = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
13879
13880 if ( ! FS_Api::is_api_error( $result ) ) {
13881 $install = $result;
13882
13883 $fs->reconnect_locally( $has_valid_blog_id );
13884
13885 if (
13886 $change_owner &&
13887 // If successful ownership change.
13888 $fs->get_user()->id != $install->user_id
13889 ) {
13890 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
13891 }
13892 }
13893 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
13894 $result = $fs->activate_license_on_site( $user, $license_key );
13895 }
13896 }
13897
13898 $is_connected = null;
13899
13900 if ( true !== $result && ! FS_Api::is_api_result_entity( $result ) ) {
13901 if ( FS_Api::is_blocked( $result ) ) {
13902 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
13903
13904 $is_connected = false;
13905 }
13906
13907 $error = FS_Api::is_api_error_object( $result ) ?
13908 $result->error->message :
13909 var_export( $result, true );
13910 } else {
13911 $is_connected = true;
13912
13913 $fs->network_upgrade_mode_completed();
13914
13915 $fs->_user = $user;
13916
13917 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
13918 $fs->_site = $fs->get_network_install();
13919 }
13920
13921 $fs->_sync_license( true, $has_valid_blog_id );
13922
13923 $this->maybe_sync_install_user();
13924
13925 $next_page = $fs->is_addon() ?
13926 $fs->get_parent_instance()->get_account_url() :
13927 $fs->get_after_activation_url( 'after_connect_url' );
13928 }
13929
13930 $fs->update_connectivity_info( $is_connected );
13931 } else {
13932 $next_page = $fs->opt_in(
13933 false,
13934 false,
13935 false,
13936 $license_key,
13937 false,
13938 false,
13939 false,
13940 $is_marketing_allowed,
13941 $sites,
13942 true,
13943 $license_owner_id
13944 );
13945
13946 if ( isset( $next_page->error ) ) {
13947 $error = $next_page->error;
13948 } else {
13949 if ( $is_network_activation_or_migration ) {
13950 /**
13951 * Get the list of sites that were just opted-in (and license activated).
13952 * This is an optimization for the next part below saving some DB queries.
13953 */
13954 $connected_sites = array();
13955 foreach ( $sites as $site ) {
13956 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
13957 $connected_sites[ $site['blog_id'] ] = true;
13958 }
13959 }
13960
13961 $all_sites = self::get_sites();
13962 $pending_blog_ids = array();
13963
13964 /**
13965 * 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.
13966 *
13967 * @author Vova Feldman (@svovaf)
13968 */
13969 foreach ( $all_sites as $site ) {
13970 $blog_id = self::get_site_blog_id( $site );
13971
13972 if ( isset( $connected_sites[ $blog_id ] ) ) {
13973 // Site was just connected.
13974 continue;
13975 }
13976
13977 if ( $fs->is_installed_on_site( $blog_id ) ) {
13978 // Site was already connected before.
13979 continue;
13980 }
13981
13982 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
13983 // Site's connection was delegated.
13984 continue;
13985 }
13986
13987 if ( $fs->is_anonymous_site( $blog_id ) ) {
13988 // Site connection was already skipped.
13989 continue;
13990 }
13991
13992 $pending_blog_ids[] = $blog_id;
13993 }
13994
13995 if ( ! empty( $pending_blog_ids ) ) {
13996 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
13997 $fs->skip_connection( $pending_blog_ids );
13998 } else {
13999 $fs->delegate_connection( $pending_blog_ids );
14000 }
14001 }
14002 }
14003 }
14004 }
14005
14006 if ( false === $error && true === $fs->_storage->require_license_activation ) {
14007 $fs->_storage->require_license_activation = false;
14008 }
14009
14010 $result = array(
14011 'success' => ( false === $error )
14012 );
14013
14014 if ( false !== $error ) {
14015 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
14016 } else {
14017 if ( $fs->is_addon() || $fs->has_addons() ) {
14018 /**
14019 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
14020 * an updated valid user licenses collection will be fetched from the server which is used to also
14021 * update the account add-ons (add-ons the user has licenses for).
14022 *
14023 * @author Leo Fajardo (@leorw)
14024 * @since 2.2.4
14025 */
14026 $fs->purge_valid_user_licenses_cache();
14027 }
14028
14029 $result['next_page'] = $next_page;
14030 }
14031
14032 return $result;
14033 }
14034
14035 /**
14036 * @author Leo Fajardo (@leorw)
14037 * @since 2.3.2
14038 *
14039 * @return array {
14040 * @key string Product slug.
14041 * @value array {
14042 * @property FS_Site $site
14043 * @property FS_Plugin_License $license
14044 * }
14045 * }
14046 */
14047 private function get_parent_and_addons_installs_info() {
14048 $fs = $this->is_addon() ?
14049 $this->get_parent_instance() :
14050 $this;
14051
14052 $installed_addons_ids = array();
14053
14054 $installed_addons_instances = $fs->get_installed_addons();
14055 foreach ( $installed_addons_instances as $instance ) {
14056 $installed_addons_ids[] = $instance->get_id();
14057 }
14058
14059 $addons_ids = array_unique( array_merge(
14060 $installed_addons_ids,
14061 $fs->get_updated_account_addons()
14062 ) );
14063
14064 // Add parent product info.
14065 $installs_info_by_slug_map = array(
14066 $fs->get_slug() => array(
14067 'install' => $fs->get_site(),
14068 'license' => $fs->_get_license()
14069 )
14070 );
14071
14072 foreach ( $addons_ids as $addon_id ) {
14073 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
14074
14075 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
14076
14077 if ( ! isset( $addon_info['is_connected'] ) || ! $addon_info['is_connected'] ) {
14078 // Add-on is not associated with an install entity.
14079 continue;
14080 }
14081
14082 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
14083 'install' => $addon_info['site'],
14084 'license' => isset( $addon_info['license'] ) ?
14085 $addon_info['license'] :
14086 null
14087 );
14088 }
14089
14090 return $installs_info_by_slug_map;
14091 }
14092
14093 /**
14094 * @author Leo Fajardo (@leorw)
14095 * @since 1.2.3.1
14096 */
14097 function _network_activate_ajax_action() {
14098 $this->_logger->entrance();
14099
14100 $this->check_ajax_referer( 'network_activate' );
14101
14102 $plugin_id = fs_request_get( 'module_id', '', 'post' );
14103 $fs = ( $plugin_id == $this->_module_id ) ?
14104 $this :
14105 $this->get_addon_instance( $plugin_id );
14106
14107 $error = false;
14108
14109 $sites = fs_request_get( 'sites', array(), 'post' );
14110 if ( is_array( $sites ) && ! empty( $sites ) ) {
14111 $sites_by_action = array(
14112 'allow' => array(),
14113 'delegate' => array(),
14114 'skip' => array()
14115 );
14116
14117 foreach ( $sites as $site ) {
14118 $sites_by_action[ $site['action'] ][] = $site;
14119 }
14120
14121 $total_sites = count( $sites );
14122 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
14123
14124 $next_page = '';
14125
14126 $has_any_install = fs_request_get_bool( 'has_any_install' );
14127
14128 if ( $total_sites === $total_sites_to_delegate &&
14129 ! $this->is_network_upgrade_mode() &&
14130 ! $has_any_install
14131 ) {
14132 $this->delegate_connection();
14133 } else {
14134 if ( ! empty( $sites_by_action['delegate'] ) ) {
14135 $this->delegate_connection( self::get_sites_blog_ids( $sites_by_action['delegate'] ) );
14136 }
14137
14138 if ( ! empty( $sites_by_action['skip'] ) ) {
14139 $this->skip_connection( self::get_sites_blog_ids( $sites_by_action['skip'] ) );
14140 }
14141
14142 if ( empty( $sites_by_action['allow'] ) ) {
14143 if ( $has_any_install ) {
14144 $first_install = $fs->find_first_install();
14145
14146 if ( ! is_null( $first_install ) ) {
14147 $fs->_site = $first_install['install'];
14148 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
14149
14150 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
14151 $fs->_storage->network_user_id = $fs->_user->id;
14152 }
14153 }
14154 } else {
14155 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
14156 $next_page = $fs->opt_in(
14157 false,
14158 false,
14159 false,
14160 false,
14161 false,
14162 false,
14163 false,
14164 fs_request_get_bool( 'is_marketing_allowed', null ),
14165 $sites_by_action['allow']
14166 );
14167 } else {
14168 $next_page = $fs->install_with_user(
14169 $this->get_network_user(),
14170 false,
14171 false,
14172 false,
14173 true,
14174 $sites_by_action['allow']
14175 );
14176 }
14177
14178 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
14179 $error = $next_page->error;
14180 }
14181 }
14182 }
14183
14184 if ( empty( $next_page ) ) {
14185 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
14186 }
14187 } else {
14188 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
14189 }
14190
14191 $result = array(
14192 'success' => ( false === $error )
14193 );
14194
14195 if ( false !== $error ) {
14196 $result['error'] = $error;
14197 } else {
14198 $result['next_page'] = $next_page;
14199 }
14200
14201 echo json_encode( $result );
14202
14203 exit;
14204 }
14205
14206 /**
14207 * Billing update AJAX callback.
14208 *
14209 * @author Vova Feldman (@svovaf)
14210 * @since 1.2.1.5
14211 */
14212 function _update_billing_ajax_action() {
14213 $this->_logger->entrance();
14214
14215 $this->check_ajax_referer( 'update_billing' );
14216
14217 if ( ! $this->is_user_admin() ) {
14218 // Only for admins.
14219 self::shoot_ajax_failure();
14220 }
14221
14222 $billing = fs_request_get( 'billing' );
14223
14224 $api = $this->get_api_user_scope();
14225 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
14226 'plugin_id' => $this->get_parent_id(),
14227 ) ) );
14228
14229 if ( ! $this->is_api_result_entity( $result ) ) {
14230 self::shoot_ajax_failure();
14231 }
14232
14233 // Purge cached billing.
14234 $this->get_api_user_scope()->purge_cache( 'billing.json' );
14235
14236 self::shoot_ajax_success();
14237 }
14238
14239 /**
14240 * Trial start for anonymous users (AJAX callback).
14241 *
14242 * @author Vova Feldman (@svovaf)
14243 * @since 1.2.1.5
14244 */
14245 function _start_trial_ajax_action() {
14246 $this->_logger->entrance();
14247
14248 $this->check_ajax_referer( 'start_trial' );
14249
14250 if ( ! $this->is_user_admin() ) {
14251 // Only for admins.
14252 self::shoot_ajax_failure();
14253 }
14254
14255 $trial_data = fs_request_get( 'trial' );
14256
14257 $next_page = $this->opt_in(
14258 false,
14259 false,
14260 false,
14261 false,
14262 false,
14263 $trial_data['plan_id']
14264 );
14265
14266 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14267 self::shoot_ajax_failure(
14268 isset( $next_page->error ) ?
14269 $next_page->error->message :
14270 var_export( $next_page, true )
14271 );
14272 }
14273
14274 $this->shoot_ajax_success( array(
14275 'next_page' => $next_page,
14276 ) );
14277 }
14278
14279 /**
14280 * @author Leo Fajardo (@leorw)
14281 * @since 1.2.0
14282 */
14283 function _resend_license_key_ajax_action() {
14284 $this->_logger->entrance();
14285
14286 $this->check_ajax_referer( 'resend_license_key' );
14287
14288 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14289
14290 if ( empty( $email_address ) ) {
14291 exit;
14292 }
14293
14294 $error = false;
14295
14296 $api = $this->get_api_plugin_scope();
14297 $result = $api->call( '/licenses/resend.json', 'post',
14298 array(
14299 'email' => $email_address,
14300 'url' => home_url(),
14301 )
14302 );
14303
14304 if ( is_object( $result ) && isset( $result->error ) ) {
14305 $error = $result->error;
14306
14307 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14308 $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' );
14309 } else if ( 'no_license' === $error->code ) {
14310 $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' );
14311 } else {
14312 $error = $error->message;
14313 }
14314 }
14315
14316 $licenses = array(
14317 'success' => ( false === $error )
14318 );
14319
14320 if ( false !== $error ) {
14321 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14322 }
14323
14324 echo json_encode( $licenses );
14325
14326 exit;
14327 }
14328
14329 /**
14330 * @author Vova Feldman (@svovaf)
14331 * @since 1.2.1.8
14332 *
14333 * @var string
14334 */
14335 private static $_pagenow;
14336
14337 /**
14338 * Get current page or the referer if executing a WP AJAX request.
14339 *
14340 * @author Vova Feldman (@svovaf)
14341 * @since 1.2.1.8
14342 *
14343 * @return string
14344 */
14345 static function get_current_page() {
14346 if ( ! isset( self::$_pagenow ) ) {
14347 global $pagenow;
14348 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14349 /**
14350 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14351 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14352 *
14353 * @author Leo Fajardo (@leorw)
14354 * @since 2.2.3
14355 */
14356 if ( is_network_admin() ) {
14357 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14358 } else if ( is_user_admin() ) {
14359 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14360 } else {
14361 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14362 }
14363
14364 $pagenow = $self_matches[1];
14365 $pagenow = trim( $pagenow, '/' );
14366 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14367 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14368 $pagenow = 'index.php';
14369 } else {
14370 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14371 $pagenow = strtolower( $self_matches[1] );
14372 if ( '.php' !== substr($pagenow, -4, 4) )
14373 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14374 }
14375 }
14376
14377 self::$_pagenow = $pagenow;
14378
14379 if ( self::is_ajax() &&
14380 'admin-ajax.php' === $pagenow
14381 ) {
14382 $referer = fs_get_raw_referer();
14383
14384 if ( is_string( $referer ) ) {
14385 $parts = explode( '?', $referer );
14386
14387 self::$_pagenow = basename( $parts[0] );
14388 }
14389 }
14390 }
14391
14392 return self::$_pagenow;
14393 }
14394
14395 /**
14396 * Helper method to check if user in the plugins page.
14397 *
14398 * @author Vova Feldman (@svovaf)
14399 * @since 1.2.1.5
14400 *
14401 * @return bool
14402 */
14403 static function is_plugins_page() {
14404 return ( 'plugins.php' === self::get_current_page() );
14405 }
14406
14407 /**
14408 * @author Leo Fajardo (@leorw)
14409 * @since 2.2.3
14410 *
14411 * @return bool
14412 */
14413 static function is_plugin_install_page() {
14414 return ( 'plugin-install.php' === self::get_current_page() );
14415 }
14416
14417 /**
14418 * @author Leo Fajardo (@leorw)
14419 * @since 2.0.2
14420 *
14421 * @return bool
14422 */
14423 static function is_updates_page() {
14424 return ( 'update-core.php' === self::get_current_page() );
14425 }
14426
14427 /**
14428 * Helper method to check if user in the themes page.
14429 *
14430 * @author Vova Feldman (@svovaf)
14431 * @since 1.2.2.6
14432 *
14433 * @return bool
14434 */
14435 static function is_themes_page() {
14436 return ( 'themes.php' === self::get_current_page() );
14437 }
14438
14439 #----------------------------------------------------------------------------------
14440 #region Affiliation
14441 #----------------------------------------------------------------------------------
14442
14443 /**
14444 * @author Leo Fajardo (@leorw)
14445 * @since 1.2.3
14446 *
14447 * @return bool
14448 */
14449 function has_affiliate_program() {
14450 if ( ! is_object( $this->_plugin ) ) {
14451 return false;
14452 }
14453
14454 return $this->_plugin->has_affiliate_program();
14455 }
14456
14457 /**
14458 * Get Plugin ID under which we will track affiliate application.
14459 *
14460 * This could either be the Bundle ID or the main plugin ID.
14461 *
14462 * @return number Bundle ID if developer has provided one, else the main plugin ID.
14463 */
14464 private function get_plugin_id_for_affiliate_terms() {
14465 return $this->has_bundle_context() ?
14466 $this->get_bundle_id() :
14467 $this->_plugin->id;
14468 }
14469
14470 /**
14471 * @author Leo Fajardo (@leorw)
14472 * @since 1.2.4
14473 */
14474 private function fetch_affiliate_terms() {
14475 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14476 /**
14477 * In case we have a bundle set in SDK configuration, we would like to use that for affiliates, not the main plugin.
14478 */
14479 $plugins_api = $this->has_bundle_context() ?
14480 $this->get_api_bundle_scope() :
14481 $this->get_api_plugin_scope();
14482
14483 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14484
14485 /**
14486 * 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.
14487 */
14488 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14489 return;
14490 }
14491
14492 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14493 }
14494 }
14495
14496 /**
14497 * @author Leo Fajardo (@leorw)
14498 * @since 1.2.4
14499 */
14500 private function fetch_affiliate_and_custom_terms() {
14501 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14502 $application_data = $this->_storage->affiliate_application_data;
14503 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14504
14505 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14506
14507 $users_api = $this->get_api_user_scope();
14508 $result = $users_api->get( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14509 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14510 if ( ! empty( $result->affiliates ) ) {
14511 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14512
14513 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14514 $application_data['status'] = $affiliate->status;
14515 $this->_storage->affiliate_application_data = $application_data;
14516 }
14517
14518 if ( $affiliate->is_using_custom_terms ) {
14519 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14520 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14521 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14522 }
14523 }
14524
14525 $this->affiliate = $affiliate;
14526 }
14527 }
14528 }
14529 }
14530
14531 /**
14532 * @author Leo Fajardo (@leorw)
14533 * @since 1.2.3
14534 */
14535 private function fetch_affiliate_and_terms() {
14536 $this->_logger->entrance();
14537
14538 $this->fetch_affiliate_terms();
14539 $this->fetch_affiliate_and_custom_terms();
14540 }
14541
14542 /**
14543 * @author Leo Fajardo (@leorw)
14544 * @since 1.2.3
14545 *
14546 * @return FS_Affiliate
14547 */
14548 function get_affiliate() {
14549 return $this->affiliate;
14550 }
14551
14552
14553 /**
14554 * @author Leo Fajardo (@leorw)
14555 * @since 1.2.3
14556 *
14557 * @return FS_AffiliateTerms
14558 */
14559 function get_affiliate_terms() {
14560 return is_object( $this->custom_affiliate_terms ) ?
14561 $this->custom_affiliate_terms :
14562 $this->plugin_affiliate_terms;
14563 }
14564
14565 /**
14566 * @author Leo Fajardo (@leorw)
14567 * @since 1.2.3
14568 */
14569 function _submit_affiliate_application() {
14570 $this->_logger->entrance();
14571
14572 $this->check_ajax_referer( 'submit_affiliate_application' );
14573
14574 if ( ! $this->is_user_admin() ) {
14575 // Only for admins.
14576 self::shoot_ajax_failure();
14577 }
14578
14579 $affiliate = fs_request_get( 'affiliate' );
14580
14581 if ( empty( $affiliate['promotion_methods'] ) ) {
14582 unset( $affiliate['promotion_methods'] );
14583 }
14584
14585 if ( ! empty( $affiliate['additional_domains'] ) ) {
14586 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
14587 }
14588
14589 if ( ! $this->is_registered() ) {
14590 $email_address = isset( $affiliate['email'] ) ? $affiliate['email'] : '';
14591
14592 if ( ! is_email( $email_address ) ) {
14593 self::shoot_ajax_failure('Invalid email address.');
14594 }
14595
14596 // Opt in but don't track usage.
14597 $next_page = $this->opt_in(
14598 $email_address,
14599 false,
14600 false,
14601 false,
14602 false,
14603 false,
14604 true
14605 );
14606
14607 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14608 self::shoot_ajax_failure(
14609 isset( $next_page->error ) ?
14610 $next_page->error->message :
14611 var_export( $next_page, true )
14612 );
14613 } else if ( $this->is_pending_activation() ) {
14614 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' ) );
14615 }
14616 }
14617
14618 $this->fetch_affiliate_terms();
14619
14620 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14621
14622 $api = $this->get_api_user_scope();
14623 $result = $api->call(
14624 ( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
14625 'post',
14626 $affiliate
14627 );
14628
14629 if ( $this->is_api_error( $result ) ) {
14630 self::shoot_ajax_failure(
14631 isset( $result->error ) ?
14632 $result->error->message :
14633 var_export( $result, true )
14634 );
14635 } else {
14636 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
14637 $this->_admin_notices->remove_sticky( 'affiliate_program' );
14638 }
14639
14640 $affiliate_application_data = array(
14641 'status' => 'pending',
14642 'stats_description' => $affiliate['stats_description'],
14643 'promotion_method_description' => $affiliate['promotion_method_description'],
14644 );
14645
14646 if ( ! empty( $affiliate['promotion_methods'] ) ) {
14647 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
14648 }
14649
14650 if ( ! empty( $affiliate['domain'] ) ) {
14651 $affiliate_application_data['domain'] = $affiliate['domain'];
14652 }
14653
14654 if ( ! empty( $affiliate['additional_domains'] ) ) {
14655 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
14656 }
14657
14658 $this->_storage->affiliate_application_data = $affiliate_application_data;
14659 }
14660
14661 // Purge cached affiliate.
14662 $api->purge_cache( 'affiliate.json' );
14663
14664 self::shoot_ajax_success( $result );
14665 }
14666
14667 /**
14668 * @author Leo Fajardo (@leorw)
14669 * @since 1.2.3
14670 *
14671 * @return array|null
14672 */
14673 function get_affiliate_application_data() {
14674 if ( empty( $this->_storage->affiliate_application_data ) ) {
14675 return null;
14676 }
14677
14678 return $this->_storage->affiliate_application_data;
14679 }
14680
14681 #endregion Affiliation ------------------------------------------------------------
14682
14683 #----------------------------------------------------------------------------------
14684 #region URL Generators
14685 #----------------------------------------------------------------------------------
14686
14687 /**
14688 * Alias to pricing_url().
14689 *
14690 * @author Vova Feldman (@svovaf)
14691 * @since 1.0.2
14692 *
14693 * @uses pricing_url()
14694 *
14695 * @param string $period Billing cycle
14696 * @param bool $is_trial
14697 *
14698 * @return string
14699 */
14700 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14701 return $this->pricing_url( $period, $is_trial );
14702 }
14703
14704 /**
14705 * @author Vova Feldman (@svovaf)
14706 * @since 1.0.9
14707 *
14708 * @uses get_upgrade_url()
14709 *
14710 * @return string
14711 */
14712 function get_trial_url() {
14713 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
14714 }
14715
14716 /**
14717 * @author Leo Fajardo (@leorw)
14718 * @since 2.1.4
14719 *
14720 * @param string $new_version
14721 *
14722 * @return string
14723 */
14724 function version_upgrade_checkout_link( $new_version ) {
14725 if ( ! is_object( $this->_license ) ) {
14726 $url = $this->pricing_url();
14727
14728 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
14729 } else {
14730 $subscription = $this->_get_subscription( $this->_license->id );
14731
14732 $url = $this->checkout_url(
14733 is_object( $subscription ) ?
14734 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
14735 WP_FS__PERIOD_LIFETIME,
14736 false,
14737 array( 'licenses' => $this->_license->quota )
14738 );
14739
14740 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
14741 }
14742
14743 return sprintf(
14744 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
14745 sprintf(
14746 '<a href="%s">%s</a>',
14747 $this->apply_filters( 'update_notice_checkout_url', $url ),
14748 $purchase_license_text
14749 ),
14750 $new_version
14751 );
14752 }
14753
14754 /**
14755 * Plugin's pricing URL.
14756 *
14757 * @author Vova Feldman (@svovaf)
14758 * @since 1.0.4
14759 *
14760 * @param string $billing_cycle Billing cycle
14761 *
14762 * @param bool $is_trial
14763 *
14764 * @return string
14765 */
14766 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14767 $this->_logger->entrance();
14768
14769 $params = array(
14770 'billing_cycle' => $billing_cycle
14771 );
14772
14773 if ( $is_trial ) {
14774 $params['trial'] = 'true';
14775 }
14776
14777 $url = $this->is_addon() ?
14778 $this->_parent->addon_url( $this->_slug ) :
14779 $this->_get_admin_page_url( 'pricing', $params );
14780
14781 return $this->get_pricing_url_with_filter( $url );
14782 }
14783
14784 /**
14785 * Retrieves the filtered pricing URL.
14786 *
14787 * @author Leo Fajardo (@leorw)
14788 * @since 2.7.4
14789 *
14790 * @param string $url
14791 *
14792 * @return string
14793 */
14794 private function get_pricing_url_with_filter( $url ) {
14795 return $this->apply_filters( 'pricing_url', $url );
14796 }
14797
14798 /**
14799 * Checkout page URL.
14800 *
14801 * @author Vova Feldman (@svovaf)
14802 * @since 1.0.6
14803 *
14804 * @param string $billing_cycle Billing cycle
14805 * @param bool $is_trial
14806 * @param array $extra (optional) Extra parameters, override other query params.
14807 * @param bool|null $network
14808 *
14809 * @return string
14810 */
14811 function checkout_url(
14812 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14813 $is_trial = false,
14814 $extra = array(),
14815 $network = null
14816 ) {
14817 $this->_logger->entrance();
14818
14819 $params = array(
14820 'checkout' => 'true',
14821 'billing_cycle' => $billing_cycle,
14822 );
14823
14824 if ( $is_trial ) {
14825 $params['trial'] = 'true';
14826 }
14827
14828 /**
14829 * Params in extra override other params.
14830 */
14831 $params = array_merge( $params, $extra );
14832
14833 return $this->apply_filters( 'checkout_url', $this->_get_admin_page_url( 'pricing', $params, $network ) );
14834 }
14835
14836 /**
14837 * Add-on checkout URL.
14838 *
14839 * @author Vova Feldman (@svovaf)
14840 * @since 1.1.7
14841 *
14842 * @param number $addon_id
14843 * @param number $pricing_id
14844 * @param string $billing_cycle
14845 * @param bool $is_trial
14846 * @param bool|null $network
14847 *
14848 * @return string
14849 */
14850 function addon_checkout_url(
14851 $addon_id,
14852 $pricing_id,
14853 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14854 $is_trial = false,
14855 $network = null
14856 ) {
14857 return $this->checkout_url( $billing_cycle, $is_trial, array(
14858 'plugin_id' => $addon_id,
14859 'pricing_id' => $pricing_id,
14860 ), $network );
14861 }
14862
14863 #endregion
14864
14865 #endregion ------------------------------------------------------------------
14866
14867 /**
14868 * Check if plugin has any add-ons.
14869 *
14870 * @author Vova Feldman (@svovaf)
14871 * @since 1.0.5
14872 *
14873 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
14874 *
14875 * @return bool
14876 */
14877 function has_addons() {
14878 $this->_logger->entrance();
14879
14880 return $this->_has_addons;
14881 }
14882
14883 /**
14884 * Check if plugin can work in anonymous mode.
14885 *
14886 * @author Vova Feldman (@svovaf)
14887 * @since 1.0.9
14888 *
14889 * @return bool
14890 *
14891 * @deprecated Please use is_enable_anonymous() instead.
14892 */
14893 function enable_anonymous() {
14894 return $this->_enable_anonymous;
14895 }
14896
14897 /**
14898 * Check if plugin can work in anonymous mode.
14899 *
14900 * @author Vova Feldman (@svovaf)
14901 * @since 1.1.9
14902 *
14903 * @return bool
14904 */
14905 function is_enable_anonymous() {
14906 return $this->_enable_anonymous;
14907 }
14908
14909 /**
14910 * Check if plugin is premium only (no free plans).
14911 *
14912 * @author Vova Feldman (@svovaf)
14913 * @since 1.1.9
14914 *
14915 * @return bool
14916 */
14917 function is_only_premium() {
14918 return $this->_is_premium_only;
14919 }
14920
14921 /**
14922 * Checks if the plugin's type is "plugin". The other type is "theme".
14923 *
14924 * @author Leo Fajardo (@leorw)
14925 * @since 1.2.2
14926 *
14927 * @return bool
14928 */
14929 function is_plugin() {
14930 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
14931 }
14932
14933 /**
14934 * @author Leo Fajardo (@leorw)
14935 * @since 1.2.2
14936 *
14937 * @return string
14938 */
14939 function get_module_type() {
14940 if ( ! isset( $this->_module_type ) ) {
14941 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
14942 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
14943 }
14944
14945 return $this->_module_type;
14946 }
14947
14948 /**
14949 * @author Leo Fajardo (@leorw)
14950 * @since 1.2.2
14951 *
14952 * @return string
14953 */
14954 function get_plugin_main_file_path() {
14955 return $this->_plugin_main_file_path;
14956 }
14957
14958 /**
14959 * Check if module has a premium code version.
14960 *
14961 * Serviceware module might be freemium without any
14962 * premium code version, where the paid features
14963 * are all part of the service.
14964 *
14965 * @author Vova Feldman (@svovaf)
14966 * @since 1.2.1.6
14967 *
14968 * @return bool
14969 */
14970 function has_premium_version() {
14971 return $this->_has_premium_version;
14972 }
14973
14974 /**
14975 * Check if feature supported with current site's plan.
14976 *
14977 * @author Vova Feldman (@svovaf)
14978 * @since 1.0.1
14979 *
14980 * @todo IMPLEMENT
14981 *
14982 * @param number $feature_id
14983 *
14984 * @throws Exception
14985 */
14986 function is_feature_supported( $feature_id ) {
14987 throw new Exception( 'not implemented' );
14988 }
14989
14990 /**
14991 * @author Vova Feldman (@svovaf)
14992 * @since 1.0.1
14993 *
14994 * @return bool Is running in SSL/HTTPS
14995 */
14996 function is_ssl() {
14997 return WP_FS__IS_HTTPS;
14998 }
14999
15000 /**
15001 * @author Vova Feldman (@svovaf)
15002 * @since 1.0.9
15003 *
15004 * @return bool Is running in AJAX call.
15005 *
15006 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
15007 */
15008 static function is_ajax() {
15009 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
15010 }
15011
15012 /**
15013 * Check if it's an AJAX call targeted for the current module.
15014 *
15015 * @author Vova Feldman (@svovaf)
15016 * @since 1.2.0
15017 *
15018 * @param array|string $actions Collection of AJAX actions.
15019 *
15020 * @return bool
15021 */
15022 function is_ajax_action( $actions ) {
15023 // Verify it's an ajax call.
15024 if ( ! self::is_ajax() ) {
15025 return false;
15026 }
15027
15028 // Verify the call is relevant for the plugin.
15029 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
15030 return false;
15031 }
15032
15033 // Verify it's one of the specified actions.
15034 if ( is_string( $actions ) ) {
15035 $actions = explode( ',', $actions );
15036 }
15037
15038 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15039 $ajax_action = fs_request_get( 'action' );
15040
15041 foreach ( $actions as $action ) {
15042 if ( $ajax_action === $this->get_action_tag( $action ) ) {
15043 return true;
15044 }
15045 }
15046 }
15047
15048 return false;
15049 }
15050
15051 /**
15052 * Check if it's an AJAX call targeted for current request.
15053 *
15054 * @author Vova Feldman (@svovaf)
15055 * @since 1.2.0
15056 *
15057 * @param array|string $actions Collection of AJAX actions.
15058 * @param number|null $module_id
15059 *
15060 * @return bool
15061 */
15062 static function is_ajax_action_static( $actions, $module_id = null ) {
15063 // Verify it's an ajax call.
15064 if ( ! self::is_ajax() ) {
15065 return false;
15066 }
15067
15068
15069 if ( ! empty( $module_id ) ) {
15070 // Verify the call is relevant for the plugin.
15071 if ( $module_id != fs_request_get( 'module_id' ) ) {
15072 return false;
15073 }
15074 }
15075
15076 // Verify it's one of the specified actions.
15077 if ( is_string( $actions ) ) {
15078 $actions = explode( ',', $actions );
15079 }
15080
15081 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15082 $ajax_action = fs_request_get( 'action' );
15083
15084 foreach ( $actions as $action ) {
15085 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
15086 return true;
15087 }
15088 }
15089 }
15090
15091 return false;
15092 }
15093
15094 /**
15095 * @author Vova Feldman (@svovaf)
15096 * @since 1.1.7
15097 *
15098 * @return bool
15099 */
15100 static function is_cron() {
15101 return ( defined( 'DOING_CRON' ) && DOING_CRON );
15102 }
15103
15104 /**
15105 * @author Leo Fajardo (@leorw)
15106 * @since 2.5.0
15107 *
15108 * @return bool
15109 */
15110 static function is_admin_post() {
15111 return ( 'admin-post.php' === self::get_current_page() );
15112 }
15113
15114 /**
15115 * Check if a real user is visiting the admin dashboard.
15116 *
15117 * @author Vova Feldman (@svovaf)
15118 * @since 1.1.7
15119 *
15120 * @return bool
15121 */
15122 function is_user_in_admin() {
15123 return (
15124 is_admin() &&
15125 ! self::is_ajax() &&
15126 ! self::is_cron() &&
15127 ! self::is_admin_post()
15128 );
15129 }
15130
15131 /**
15132 * Check if a real user is in the customizer view.
15133 *
15134 * @author Vova Feldman (@svovaf)
15135 * @since 1.2.2.7
15136 *
15137 * @return bool
15138 */
15139 static function is_customizer() {
15140 return is_customize_preview();
15141 }
15142
15143 /**
15144 * Check if running in HTTPS and if site's plan matching the specified plan.
15145 *
15146 * @param string $plan
15147 * @param bool $exact
15148 *
15149 * @return bool
15150 */
15151 function is_ssl_and_plan( $plan, $exact = false ) {
15152 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
15153 }
15154
15155 /**
15156 * Construct plugin's settings page URL.
15157 *
15158 * @author Vova Feldman (@svovaf)
15159 * @since 1.0.4
15160 *
15161 * @param string $page
15162 * @param array $params
15163 * @param bool|null $network
15164 *
15165 * @return string
15166 */
15167 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
15168 if ( is_null( $network ) ) {
15169 $network = (
15170 $this->_is_network_active &&
15171 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
15172 );
15173 }
15174
15175 if ( 0 < count( $params ) ) {
15176 foreach ( $params as $k => $v ) {
15177 $params[ $k ] = urlencode( $v );
15178 }
15179 }
15180
15181 $page_param = $this->_menu->get_slug( $page );
15182
15183 if ( empty( $page ) &&
15184 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
15185 $this->show_opt_in_on_themes_page()
15186 ) {
15187 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
15188
15189 return add_query_arg(
15190 $params,
15191 $this->admin_url( 'themes.php', 'admin', $network )
15192 );
15193 }
15194
15195 if ( ! $this->has_settings_menu() ) {
15196 if ( ! empty( $page ) ) {
15197 // Module doesn't have a setting page, but since the request is for
15198 // a specific Freemius page, use the admin.php path.
15199 return add_query_arg( array_merge( $params, array(
15200 'page' => $page_param,
15201 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15202 } else {
15203 if ( $this->is_activation_mode() ) {
15204 /**
15205 * @author Vova Feldman
15206 * @since 1.2.1.6
15207 *
15208 * If plugin doesn't have a settings page, create one for the opt-in screen.
15209 */
15210 return add_query_arg( array_merge( $params, array(
15211 'page' => $this->_slug,
15212 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15213 } else {
15214 // Plugin without a settings page.
15215 return add_query_arg(
15216 $params,
15217 $this->admin_url( 'plugins.php', 'admin', $network )
15218 );
15219 }
15220 }
15221 }
15222
15223 // Module has a submenu settings page.
15224 if ( ! $this->_menu->is_top_level() ) {
15225 $parent_slug = $this->_menu->get_parent_slug();
15226 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
15227 $parent_slug :
15228 'admin.php';
15229
15230 return add_query_arg( array_merge( $params, array(
15231 'page' => $page_param,
15232 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
15233 }
15234
15235 // Module has a top level CPT settings page.
15236 if ( $this->_menu->is_cpt() ) {
15237 if ( empty( $page ) && $this->is_activation_mode() ) {
15238 return add_query_arg( array_merge( $params, array(
15239 'page' => $page_param
15240 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15241 } else {
15242 if ( ! empty( $page ) ) {
15243 $params['page'] = $page_param;
15244 }
15245
15246 return add_query_arg(
15247 $params,
15248 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
15249 );
15250 }
15251 }
15252
15253 // Module has a custom top level settings page.
15254 return add_query_arg( array_merge( $params, array(
15255 'page' => $page_param,
15256 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15257 }
15258
15259 #--------------------------------------------------------------------------------
15260 #region Multisite
15261 #--------------------------------------------------------------------------------
15262
15263 /**
15264 * @author Leo Fajardo (@leorw)
15265 * @since 2.0.0
15266 *
15267 * @return bool
15268 */
15269 function is_network_active() {
15270 return $this->_is_network_active;
15271 }
15272
15273 /**
15274 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
15275 *
15276 * @author Leo Fajardo (@leorw)
15277 * @since 2.0.0
15278 *
15279 * @param bool|int[] $all_or_blog_ids
15280 */
15281 private function delegate_connection( $all_or_blog_ids = true ) {
15282 $this->_logger->entrance();
15283
15284 $this->_admin_notices->remove_sticky( 'connect_account' );
15285
15286 if ( true === $all_or_blog_ids ) {
15287 // All sites delegation.
15288 $this->_storage->store( 'is_delegated_connection', true, true );
15289 } else {
15290 // Specified sites delegation.
15291 foreach ( $all_or_blog_ids as $blog_id ) {
15292 $this->delegate_site_connection( $blog_id );
15293 }
15294 }
15295
15296 $this->network_upgrade_mode_completed();
15297 }
15298
15299 /**
15300 * Delegate specific network site conncetion to the site admin.
15301 *
15302 * @author Vova Feldman (@svovaf)
15303 * @since 2.0.0
15304 *
15305 * @param int $blog_id
15306 */
15307 private function delegate_site_connection( $blog_id ) {
15308 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
15309 }
15310
15311 /**
15312 * Check if super-admin delegated the connection of ALL sites to the site admins.
15313 *
15314 * @author Vova Feldman (@svovaf)
15315 * @since 2.0.0
15316 *
15317 * @return bool
15318 */
15319 function is_network_delegated_connection() {
15320 if ( ! $this->_is_network_active ) {
15321 return false;
15322 }
15323
15324 return $this->_storage->get( 'is_delegated_connection', false, true );
15325 }
15326
15327 /**
15328 * @author Leo Fajardo (@leorw)
15329 * @since 2.0.0
15330 *
15331 * @param int $blog_id
15332 *
15333 * @return bool
15334 */
15335 function is_site_delegated_connection( $blog_id = 0 ) {
15336 if ( ! $this->_is_network_active ) {
15337 return false;
15338 }
15339
15340 if ( 0 == $blog_id ) {
15341 $blog_id = get_current_blog_id();
15342 }
15343
15344 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15345 }
15346
15347 /**
15348 * Check if delegated the connection. When running within the network admin,
15349 * and haven't specified the blog ID, checks if network level delegated. If running
15350 * within a site admin or specified a blog ID, check if delegated the connection for
15351 * the current context site.
15352 *
15353 * If executed outside the the admin, check if delegated the connection
15354 * for the current context site OR the whole network.
15355 *
15356 * @author Vova Feldman (@svovaf)
15357 * @since 2.0.0
15358 *
15359 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15360 *
15361 * @return bool
15362 */
15363 function is_delegated_connection( $blog_id = 0 ) {
15364 if ( ! $this->_is_network_active ) {
15365 return false;
15366 }
15367
15368 if ( fs_is_network_admin() && 0 == $blog_id ) {
15369 return $this->is_network_delegated_connection();
15370 }
15371
15372 return (
15373 $this->is_network_delegated_connection() ||
15374 $this->is_site_delegated_connection( $blog_id )
15375 );
15376 }
15377
15378 /**
15379 * Check if the current module is active for the site.
15380 *
15381 * @author Vova Feldman (@svovaf)
15382 * @since 2.0.0
15383 *
15384 * @param int $blog_id
15385 *
15386 * @return bool
15387 */
15388 function is_active_for_site( $blog_id ) {
15389 if ( ! is_multisite() ) {
15390 // Not a multisite and this code is executed, means that the plugin is active.
15391 return true;
15392 }
15393
15394 if ( $this->is_theme() ) {
15395 // All themes are site level activated.
15396 return true;
15397 }
15398
15399 if ( $this->_is_network_active ) {
15400 // Plugin was network activated so it's active.
15401 return true;
15402 }
15403
15404 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15405 }
15406
15407 /**
15408 * @todo Implement pagination when accessing the subsites collection.
15409 *
15410 * @author Leo Fajardo (@leorw)
15411 * @since 2.0.0
15412 *
15413 * @param int $limit Default to 1,000
15414 * @param int $offset Default to 0
15415 *
15416 * @return array Active & public sites collection.
15417 */
15418 static function get_sites( $limit = 1000, $offset = 0 ) {
15419 if ( ! is_multisite() ) {
15420 return array();
15421 }
15422
15423 /**
15424 * For consistency with get_blog_list() which only return active public sites.
15425 *
15426 * @author Vova Feldman (@svovaf)
15427 */
15428 $args = array(
15429 /**
15430 * Commented out in order to handle the migration of site options whether the site is public or not.
15431 *
15432 * @author Leo Fajardo (@leorw)
15433 * @since 2.2.1
15434 */
15435 // 'public' => 1,
15436 'archived' => 0,
15437 'mature' => 0,
15438 'spam' => 0,
15439 'deleted' => 0,
15440 'number' => $limit,
15441 'offset' => $offset,
15442 );
15443
15444 return get_sites( $args );
15445 }
15446
15447 /**
15448 * Checks if a given blog is active.
15449 *
15450 * @author Vova Feldman (@svovaf)
15451 * @since 2.0.0
15452 *
15453 * @param $blog_id
15454 *
15455 * @return bool
15456 */
15457 private static function is_site_active( $blog_id ) {
15458 global $wpdb;
15459
15460 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15461
15462 if ( ! is_object( $blog_info ) ) {
15463 return false;
15464 }
15465
15466 return (
15467 true == $blog_info->public &&
15468 false == $blog_info->archived &&
15469 false == $blog_info->mature &&
15470 false == $blog_info->spam &&
15471 false == $blog_info->deleted
15472 );
15473 }
15474
15475 /**
15476 * Get a mapping between the site addresses to their blog IDs.
15477 *
15478 * @author Vova Feldman (@svovaf)
15479 * @since 2.0.0
15480 *
15481 * @return array {
15482 * @key string Site address without protocol with a trailing slash.
15483 * @value int Site's blog ID.
15484 * }
15485 */
15486 private function get_address_to_blog_map() {
15487 $sites = self::get_sites();
15488
15489 // Map site addresses to their blog IDs.
15490 $address_to_blog_map = array();
15491 foreach ( $sites as $site ) {
15492 $blog_id = self::get_site_blog_id( $site );
15493 $address = self::get_unfiltered_site_url( $blog_id, true, true );
15494 $address_to_blog_map[ $address ] = $blog_id;
15495 }
15496
15497 return $address_to_blog_map;
15498 }
15499
15500 /**
15501 * Get a mapping between the site addresses to their blog IDs.
15502 *
15503 * @author Vova Feldman (@svovaf)
15504 * @since 2.0.0
15505 *
15506 * @return array {
15507 * @key int Site's blog ID.
15508 * @value FS_Site Associated install.
15509 * }
15510 */
15511 function get_blog_install_map() {
15512 $sites = self::get_sites();
15513
15514 // Map site blog ID to its install.
15515 $install_map = array();
15516
15517 foreach ( $sites as $site ) {
15518 $blog_id = self::get_site_blog_id( $site );
15519 $install = $this->get_install_by_blog_id( $blog_id );
15520
15521 if ( is_object( $install ) ) {
15522 $install_map[ $blog_id ] = $install;
15523 }
15524 }
15525
15526 return $install_map;
15527 }
15528
15529 /**
15530 * @author Vova Feldman (@svovaf)
15531 * @since 2.5.1
15532 *
15533 * @param bool|null $is_delegated When `true`, returns only connection delegated blog IDs. When `false`, only non-delegated blog IDs.
15534 *
15535 * @return int[]
15536 */
15537 private function get_blog_ids( $is_delegated = null ) {
15538 $blog_ids = array();
15539
15540 $sites = self::get_sites();
15541 foreach ( $sites as $site ) {
15542 $blog_id = self::get_site_blog_id( $site );
15543
15544 if (
15545 is_null( $is_delegated ) ||
15546 $is_delegated === $this->is_site_delegated_connection( $blog_id )
15547 ) {
15548 $blog_ids[] = $blog_id;
15549 }
15550 }
15551
15552 return $blog_ids;
15553 }
15554
15555 /**
15556 * @author Vova Feldman (@svovaf)
15557 * @since 2.5.1
15558 *
15559 * @return int[]
15560 */
15561 private function get_non_delegated_blog_ids() {
15562 return $this->get_blog_ids( false );
15563 }
15564
15565 /**
15566 * Gets a map of module IDs that the given user has opted-in to.
15567 *
15568 * @author Leo Fajardo (@leorw)
15569 * @since 2.1.0
15570 *
15571 * @param number $fs_user_id
15572 *
15573 * @return array {
15574 * @key number $plugin_id
15575 * @value bool Always true.
15576 * }
15577 */
15578 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15579 self::$_static_logger->entrance();
15580
15581 if ( ! is_multisite() ) {
15582 $installs = array_merge(
15583 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15584 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15585 );
15586 } else {
15587 $sites = self::get_sites();
15588
15589 $installs = array();
15590 foreach ( $sites as $site ) {
15591 $blog_id = self::get_site_blog_id( $site );
15592
15593 $installs = array_merge(
15594 $installs,
15595 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15596 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15597 );
15598 }
15599 }
15600
15601 $module_ids_map = array();
15602 foreach ( $installs as $install ) {
15603 if ( is_object( $install ) &&
15604 FS_Site::is_valid_id( $install->id ) &&
15605 FS_User::is_valid_id( $install->user_id ) &&
15606 ( $install->user_id == $fs_user_id )
15607 ) {
15608 $module_ids_map[ $install->plugin_id ] = true;
15609 }
15610 }
15611
15612 return $module_ids_map;
15613 }
15614
15615 /**
15616 * @author Leo Fajardo (@leorw)
15617 *
15618 * @return null|array {
15619 * 'install' => FS_Site Module's install,
15620 * 'blog_id' => string The associated blog ID.
15621 * }
15622 */
15623 function find_first_install() {
15624 $sites = self::get_sites();
15625
15626 foreach ( $sites as $site ) {
15627 $blog_id = self::get_site_blog_id( $site );
15628 $install = $this->get_install_by_blog_id( $blog_id );
15629
15630 if ( is_object( $install ) ) {
15631 return array(
15632 'install' => $install,
15633 'blog_id' => $blog_id
15634 );
15635 }
15636 }
15637
15638 return null;
15639 }
15640
15641 /**
15642 * Switches the Freemius site level context to a specified blog.
15643 *
15644 * @author Vova Feldman (@svovaf)
15645 * @since 2.0.0
15646 *
15647 * @param int $blog_id
15648 * @param FS_Site $install
15649 * @param bool $flush
15650 *
15651 * @return bool Since 2.3.1 returns if a switch was made.
15652 */
15653 function switch_to_blog( $blog_id, $install = null, $flush = false ) {
15654 if ( ! is_numeric( $blog_id ) ) {
15655 return false;
15656 }
15657
15658 if ( ! $flush && $blog_id == $this->_context_is_network_or_blog_id ) {
15659 return false;
15660 }
15661
15662 switch_to_blog( $blog_id );
15663 $this->_context_is_network_or_blog_id = $blog_id;
15664
15665 self::$_accounts->set_site_blog_context( $blog_id );
15666 $this->_storage->set_site_blog_context( $blog_id );
15667 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
15668
15669 $this->_site = is_object( $install ) ?
15670 $install :
15671 $this->get_install_by_blog_id( $blog_id );
15672
15673 $this->_user = false;
15674 $this->_licenses = false;
15675 $this->_license = null;
15676 $this->is_whitelabeled = null;
15677
15678 if ( is_object( $this->_site ) ) {
15679 // Try to fetch user from install.
15680 $this->_user = self::_get_user_by_id( $this->_site->user_id );
15681
15682 if ( ! is_object( $this->_user ) &&
15683 FS_User::is_valid_id( $this->_storage->prev_user_id )
15684 ) {
15685 // Try to fetch previously saved user.
15686 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
15687
15688 if ( ! is_object( $this->_user ) ) {
15689 // Fallback to network's user.
15690 $this->_user = $this->get_network_user();
15691 }
15692 }
15693
15694 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
15695
15696 if ( ! empty( $all_plugin_licenses ) ) {
15697 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
15698 $this->_license = null;
15699 } else {
15700 $license_found = false;
15701 foreach ( $all_plugin_licenses as $license ) {
15702 if ( $license->id == $this->_site->license_id ) {
15703 // License found.
15704 $this->_license = $license;
15705 $license_found = true;
15706 break;
15707 }
15708 }
15709
15710 if ( $license_found ) {
15711 $this->link_license_2_user( $this->_license->id, $this->_user->id );
15712 }
15713 }
15714
15715 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15716 }
15717 }
15718
15719 unset( $this->_site_api );
15720 unset( $this->_user_api );
15721
15722 return true;
15723 }
15724
15725 /**
15726 * Restore the blog context to the blog that originally loaded the module.
15727 *
15728 * @author Vova Feldman (@svovaf)
15729 * @since 2.0.0
15730 */
15731 function restore_current_blog() {
15732 $this->switch_to_blog( $this->_blog_id );
15733 }
15734
15735 /**
15736 * @author Vova Feldman (@svovaf)
15737 * @since 2.0.0
15738 *
15739 * @param array|WP_Site $site
15740 *
15741 * @return int
15742 */
15743 static function get_site_blog_id( &$site ) {
15744 return ( $site instanceof WP_Site ) ?
15745 $site->blog_id :
15746 ( is_object( $site ) && isset( $site->userblog_id ) ?
15747 $site->userblog_id :
15748 $site['blog_id'] );
15749 }
15750
15751 /**
15752 * @author Vova Feldman (@svovaf)
15753 * @since 2.5.1
15754 *
15755 * @param WP_Site[]|array[] $sites
15756 *
15757 * @return int[]
15758 */
15759 static function get_sites_blog_ids( $sites ) {
15760 $blog_ids = array();
15761 foreach ( $sites as $site ) {
15762 $blog_ids[] = self::get_site_blog_id( $site );
15763 }
15764
15765 return $blog_ids;
15766 }
15767
15768 /**
15769 * @author Leo Fajardo (@leorw)
15770 * @since 2.0.0
15771 *
15772 * @param array|WP_Site|null $site
15773 * @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.
15774 *
15775 * @return array
15776 */
15777 function get_site_info( $site = null, $load_registration = false ) {
15778 $this->_logger->entrance();
15779
15780 $switched = false;
15781
15782 $registration_date = null;
15783
15784 if ( is_null( $site ) ) {
15785 $url = self::get_unfiltered_site_url();
15786 $name = get_bloginfo( 'name' );
15787 $blog_id = null;
15788 } else {
15789 $blog_id = self::get_site_blog_id( $site );
15790
15791 if ( get_current_blog_id() != $blog_id ) {
15792 switch_to_blog( $blog_id );
15793 $switched = true;
15794 }
15795
15796 if ( $site instanceof WP_Site ) {
15797 $url = $site->siteurl;
15798 $name = $site->blogname;
15799 $registration_date = $site->registered;
15800 } else {
15801 $url = self::get_unfiltered_site_url( $blog_id );
15802 $name = get_bloginfo( 'name' );
15803 }
15804 }
15805
15806 if ( empty( $registration_date ) && $load_registration ) {
15807 $blog_details = get_blog_details( $blog_id, false );
15808
15809 if ( is_object( $blog_details ) && isset( $blog_details->registered ) ) {
15810 $registration_date = $blog_details->registered;
15811 }
15812 }
15813
15814 $info = array(
15815 'uid' => $this->get_anonymous_id( $blog_id ),
15816 'url' => $url,
15817 );
15818
15819 // Add these diagnostic information only if user allowed to track.
15820 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
15821 $info = array_merge( $info, array(
15822 'title' => $name,
15823 'language' => self::get_sanitized_language(),
15824 ) );
15825 }
15826
15827 if ( is_numeric( $blog_id ) ) {
15828 $info['blog_id'] = $blog_id;
15829 }
15830
15831 if ( ! empty( $registration_date ) ) {
15832 $info[ 'registration_date' ] = $registration_date;
15833 }
15834
15835 if ( $switched ) {
15836 restore_current_blog();
15837 }
15838
15839 return $info;
15840 }
15841
15842 /**
15843 * Load the module's install based on the blog ID.
15844 *
15845 * @author Vova Feldman (@svovaf)
15846 * @since 2.0.0
15847 *
15848 * @param int|null $blog_id
15849 *
15850 * @return FS_Site
15851 */
15852 function get_install_by_blog_id( $blog_id = null ) {
15853 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15854 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15855
15856 if ( is_object( $install ) &&
15857 is_numeric( $install->id ) &&
15858 is_numeric( $install->user_id ) &&
15859 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15860 ) {
15861 // Load site.
15862 $install = clone $install;
15863 }
15864
15865 return $install;
15866 }
15867
15868 /**
15869 * Check if module is installed on a specified site.
15870 *
15871 * @author Vova Feldman (@svovaf)
15872 * @since 2.0.0
15873 *
15874 * @param int|null $blog_id
15875 *
15876 * @return bool
15877 */
15878 function is_installed_on_site( $blog_id = null ) {
15879 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15880 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15881
15882 return (
15883 is_object( $install ) &&
15884 is_numeric( $install->id ) &&
15885 is_numeric( $install->user_id ) &&
15886 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15887 );
15888 }
15889
15890 /**
15891 * Check if super-admin connected at least one site via the network opt-in.
15892 *
15893 * @author Vova Feldman (@svovaf)
15894 * @since 2.0.0
15895 *
15896 * @return bool
15897 */
15898 function is_network_registered() {
15899 if ( ! $this->_is_network_active ) {
15900 return false;
15901 }
15902
15903 return FS_User::is_valid_id( $this->_storage->network_user_id );
15904 }
15905
15906 /**
15907 * Returns the main user associated with the network.
15908 *
15909 * @author Vova Feldman (@svovaf)
15910 * @since 2.0.0
15911 *
15912 * @return FS_User
15913 */
15914 function get_network_user() {
15915 if ( ! $this->_is_network_active ) {
15916 return null;
15917 }
15918
15919 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
15920 self::_get_user_by_id( $this->_storage->network_user_id ) :
15921 null;
15922 }
15923
15924 /**
15925 * Returns the current context user or the network's main user.
15926 *
15927 * @author Vova Feldman (@svovaf)
15928 * @since 2.0.0
15929 *
15930 * @return FS_User
15931 */
15932 function get_current_or_network_user() {
15933 return ( $this->_user instanceof FS_User ) ?
15934 $this->_user :
15935 $this->get_network_user();
15936 }
15937
15938 /**
15939 * Returns the main install associated with the network.
15940 *
15941 * @author Vova Feldman (@svovaf)
15942 * @since 2.0.0
15943 *
15944 * @return FS_Site
15945 */
15946 function get_network_install() {
15947 if ( ! $this->_is_network_active ) {
15948 return null;
15949 }
15950
15951 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15952 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
15953 null;
15954 }
15955
15956 /**
15957 * Returns the blog ID that is associated with the main install.
15958 *
15959 * @author Leo Fajardo (@leorw)
15960 * @since 2.0.0
15961 *
15962 * @return int|null
15963 */
15964 function get_network_install_blog_id() {
15965 if ( ! $this->_is_network_active ) {
15966 return null;
15967 }
15968
15969 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15970 $this->_storage->network_install_blog_id :
15971 null;
15972 }
15973
15974 /**
15975 * Returns the current context install or the network's main install.
15976 *
15977 * @author Vova Feldman (@svovaf)
15978 * @since 2.0.0
15979 *
15980 * @return FS_Site
15981 */
15982 function get_current_or_network_install() {
15983 return ( $this->_site instanceof FS_Site ) ?
15984 $this->_site :
15985 $this->get_network_install();
15986 }
15987
15988 /**
15989 * Check if executing a site level action from the network level admin.
15990 *
15991 * @author Vova Feldman (@svovaf)
15992 * @since 2.0.0
15993 *
15994 * @return false|int If yes, return the requested blog ID.
15995 */
15996 private function is_network_level_site_specific_action() {
15997 if ( ! $this->_is_network_active ) {
15998 return false;
15999 }
16000
16001 if ( ! fs_is_network_admin() ) {
16002 return false;
16003 }
16004
16005 $blog_id = fs_request_get( 'blog_id', '' );
16006
16007 return is_numeric( $blog_id ) ? $blog_id : false;
16008 }
16009
16010 /**
16011 * Check if executing an action from the network level admin.
16012 *
16013 * @author Vova Feldman (@svovaf)
16014 * @since 2.0.0
16015 *
16016 * @return bool
16017 */
16018 private function is_network_level_action() {
16019 return ( $this->_is_network_active && fs_is_network_admin() );
16020 }
16021
16022 /**
16023 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
16024 * 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.
16025 *
16026 * @author Vova Feldman (@svovaf)
16027 * @since 2.0.0
16028 *
16029 * @param int $context_blog_id
16030 */
16031 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
16032 $this->_logger->entrance();
16033
16034 if ( $this->_is_network_active ) {
16035 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
16036 $installs_map = $this->get_blog_install_map();
16037
16038 foreach ( $installs_map as $blog_id => $install ) {
16039 /**
16040 * @var FS_Site $install
16041 */
16042 if ( $context_blog_id == $blog_id ) {
16043 continue;
16044 }
16045
16046 if ( $install->user_id != $this->_storage->network_user_id ) {
16047 continue;
16048 }
16049
16050 // Switch reference to a blog that is opted-in and belong to the same super-admin.
16051 $this->_storage->network_install_blog_id = $blog_id;
16052 break;
16053 }
16054 }
16055 }
16056
16057 if ( ! $this->is_registered() ) {
16058 return;
16059 }
16060
16061 if ( $this->is_sync_cron_scheduled() &&
16062 $context_blog_id == $this->get_sync_cron_blog_id()
16063 ) {
16064 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
16065 }
16066
16067 if ( $this->is_install_sync_scheduled() &&
16068 $context_blog_id == $this->get_install_sync_cron_blog_id()
16069 ) {
16070 $this->maybe_schedule_install_sync_cron( $context_blog_id );
16071 }
16072 }
16073
16074 /**
16075 * Executed after site deactivation, archive, or flag as spam.
16076 *
16077 * @author Vova Feldman (@svovaf)
16078 * @since 2.0.0
16079 *
16080 * @param int $context_blog_id
16081 */
16082 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
16083 $this->_logger->entrance();
16084
16085 $install = $this->get_install_by_blog_id( $context_blog_id );
16086
16087 if ( ! is_object( $install ) ) {
16088 // Site not connected.
16089 return;
16090 }
16091
16092 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16093
16094 if ( ! $this->is_registered() ) {
16095 return;
16096 }
16097
16098 $current_blog_id = get_current_blog_id();
16099
16100 $this->switch_to_blog( $context_blog_id );
16101
16102 // Send deactivation event.
16103 $this->sync_install( array(
16104 'is_active' => false,
16105 ) );
16106
16107 $this->switch_to_blog( $current_blog_id );
16108 }
16109
16110 /**
16111 * Executed after site deletion.
16112 *
16113 * @author Vova Feldman (@svovaf)
16114 * @since 2.0.0
16115 *
16116 * @param int $context_blog_id
16117 * @param bool $drop True if site's database tables should be dropped. Default is false.
16118 */
16119 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
16120 $this->_logger->entrance();
16121
16122 $install = $this->get_install_by_blog_id( $context_blog_id );
16123
16124 if ( ! is_object( $install ) ) {
16125 // Site not connected.
16126 return;
16127 }
16128
16129 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16130
16131 if ( ! $this->is_registered() ) {
16132 return;
16133 }
16134
16135 $current_blog_id = get_current_blog_id();
16136
16137 $this->switch_to_blog( $context_blog_id );
16138
16139 if ( $drop ) {
16140 // Delete install if dropping site DB.
16141 $this->delete_account_event();
16142 } else {
16143 // Send deactivation event.
16144 $this->sync_install( array(
16145 'is_active' => false,
16146 ) );
16147 }
16148
16149 $this->switch_to_blog( $current_blog_id );
16150 }
16151
16152 /**
16153 * Executed after site deletion, called from wp_delete_site
16154 *
16155 * @author Dario Curvino (@dudo)
16156 * @since 2.5.0
16157 *
16158 * @param WP_Site $old_site
16159 */
16160 public function _after_wpsite_deleted_callback( WP_Site $old_site ) {
16161 $this->_logger->entrance();
16162
16163 $this->_after_site_deleted_callback( $old_site->blog_id, true );
16164 }
16165
16166 /**
16167 * Executed after site re-activation.
16168 *
16169 * @author Vova Feldman (@svovaf)
16170 * @since 2.0.0
16171 *
16172 * @param int $context_blog_id
16173 */
16174 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
16175 $this->_logger->entrance();
16176
16177 $install = $this->get_install_by_blog_id( $context_blog_id );
16178
16179 if ( ! is_object( $install ) ) {
16180 // Site not connected.
16181 return;
16182 }
16183
16184 if ( ! self::is_site_active( $context_blog_id ) ) {
16185 // Site not yet active (can be in spam mode, archived, deleted...).
16186 return;
16187 }
16188
16189 $current_blog_id = get_current_blog_id();
16190
16191 $this->switch_to_blog( $context_blog_id );
16192
16193 // Send re-activation event.
16194 $this->sync_install( array(
16195 'is_active' => true,
16196 ) );
16197
16198 $this->switch_to_blog( $current_blog_id );
16199 }
16200
16201 #endregion Multisite
16202
16203 /**
16204 * @author Leo Fajardo (@leorw)
16205 *
16206 * @param string $path
16207 * @param string $scheme
16208 * @param bool $network
16209 *
16210 * @return string
16211 */
16212 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
16213 return ( $this->_is_network_active && $network ) ?
16214 network_admin_url( $path, $scheme ) :
16215 admin_url( $path, $scheme );
16216 }
16217
16218 /**
16219 * Check if currently in a specified admin page.
16220 *
16221 * @author Vova Feldman (@svovaf)
16222 * @since 1.2.2.7
16223 *
16224 * @param string $page
16225 *
16226 * @return bool
16227 */
16228 function is_admin_page( $page ) {
16229 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
16230 }
16231
16232 /**
16233 * Check if currently in the product's main admin page.
16234 *
16235 * @author Vova Feldman (@svovaf)
16236 * @since 2.3.1
16237 *
16238 * @return bool
16239 */
16240 function is_main_admin_page() {
16241 return $this->is_admin_page( '' );
16242 }
16243
16244 /**
16245 * Get module's main admin setting page URL.
16246 *
16247 * @author Vova Feldman (@svovaf)
16248 * @since 1.2.2.7
16249 *
16250 * @return string
16251 */
16252 function main_menu_url() {
16253 return $this->_menu->main_menu_url();
16254 }
16255
16256 /**
16257 * Check if currently on the theme's setting page or
16258 * on any of the Freemius added pages (via tabs).
16259 *
16260 * @author Vova Feldman (@svovaf)
16261 * @since 1.2.2.7
16262 *
16263 * @return bool
16264 *
16265 * @deprecated Please use is_product_settings_page() instead;
16266 */
16267 function is_theme_settings_page() {
16268 return $this->is_product_settings_page();
16269 }
16270
16271 /**
16272 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
16273 *
16274 * @author Vova Feldman (@svovaf)
16275 * @since 1.2.2.7
16276 *
16277 * @return bool
16278 */
16279 function is_product_settings_page() {
16280 $page = fs_request_get( 'page', '', 'get' );
16281 $menu_slug = $this->_menu->get_slug();
16282
16283 if ( $page === $menu_slug ) {
16284 return true;
16285 }
16286
16287 return fs_starts_with(
16288 // e.g., {$menu_slug}-account, {$menu_slug}-affiliation, etc.
16289 $page,
16290 ( $menu_slug . '-' )
16291 );
16292 }
16293
16294 /**
16295 * Plugin's account page + sync license URL.
16296 *
16297 * @author Vova Feldman (@svovaf)
16298 * @since 1.1.9.1
16299 *
16300 * @param bool|number $plugin_id
16301 * @param bool $add_action_nonce
16302 * @param array $params
16303 *
16304 * @return string
16305 */
16306 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
16307 if ( is_numeric( $plugin_id ) ) {
16308 $params['plugin_id'] = $plugin_id;
16309 }
16310
16311 return $this->get_account_url(
16312 $this->get_unique_affix() . '_sync_license',
16313 $params,
16314 $add_action_nonce
16315 );
16316 }
16317
16318 /**
16319 * Plugin's account URL.
16320 *
16321 * @author Vova Feldman (@svovaf)
16322 * @since 1.0.4
16323 *
16324 * @param bool|string $action
16325 * @param array $params
16326 *
16327 * @param bool $add_action_nonce
16328 *
16329 * @return string
16330 */
16331 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
16332 if ( is_string( $action ) ) {
16333 $params['fs_action'] = $action;
16334 }
16335
16336 self::require_pluggable_essentials();
16337
16338 return ( $add_action_nonce && is_string( $action ) ) ?
16339 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
16340 $this->_get_admin_page_url( 'account', $params );
16341 }
16342
16343 /**
16344 * @author Vova Feldman (@svovaf)
16345 * @since 1.2.0
16346 *
16347 * @param string $tab
16348 * @param bool $action
16349 * @param array $params
16350 * @param bool $add_action_nonce
16351 *
16352 * @return string
16353 *
16354 * @uses get_account_url()
16355 */
16356 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
16357 $params['tab'] = $tab;
16358
16359 return $this->get_account_url( $action, $params, $add_action_nonce );
16360 }
16361
16362 /**
16363 * Plugin's account URL.
16364 *
16365 * @author Vova Feldman (@svovaf)
16366 * @since 1.0.4
16367 *
16368 * @param bool|string $topic
16369 * @param bool|string $message
16370 * @param bool|string $summary Since 2.5.1.
16371 *
16372 * @return string
16373 */
16374 function contact_url( $topic = false, $message = false, $summary = false ) {
16375 $params = array();
16376 if ( is_string( $topic ) ) {
16377 $params['topic'] = $topic;
16378 }
16379 if ( is_string( $message ) ) {
16380 $params['message'] = $message;
16381 }
16382
16383 if ( is_string( $summary ) ) {
16384 $params['summary'] = $summary;
16385 }
16386
16387 if ( $this->is_addon() ) {
16388 $params['addon_id'] = $this->get_id();
16389
16390 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
16391 } else {
16392 return $this->_get_admin_page_url( 'contact', $params );
16393 }
16394 }
16395
16396 /**
16397 * Add-on direct info URL.
16398 *
16399 * @author Vova Feldman (@svovaf)
16400 * @since 1.1.0
16401 *
16402 * @param string $slug
16403 *
16404 * @return string
16405 */
16406 function addon_url( $slug ) {
16407 return $this->_get_admin_page_url( 'addons', array(
16408 'slug' => $slug
16409 ) );
16410 }
16411
16412 /**
16413 * Add-ons URL.
16414 *
16415 * @author Vova Feldman (@svovaf)
16416 * @since 2.4.5
16417 *
16418 * @return string
16419 */
16420 function get_addons_url() {
16421 return $this->_get_admin_page_url( 'addons' );
16422 }
16423
16424 /* Logger
16425 ------------------------------------------------------------------------------------------------------------------*/
16426 /**
16427 * @param string $id
16428 * @param bool $prefix_slug
16429 *
16430 * @return FS_Logger
16431 */
16432 function get_logger( $id = '', $prefix_slug = true ) {
16433 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16434 }
16435
16436 /**
16437 * Note: This method is used externally so don't delete it.
16438 *
16439 * @param $id
16440 * @param bool $load_options
16441 * @param bool $prefix_slug
16442 *
16443 * @return FS_Option_Manager
16444 */
16445 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16446 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16447 }
16448
16449 /* Security
16450 ------------------------------------------------------------------------------------------------------------------*/
16451 private static function _encrypt( $str ) {
16452 if ( is_null( $str ) ) {
16453 return null;
16454 }
16455
16456 /**
16457 * The encrypt/decrypt functions are used to protect
16458 * the user from messing up with some of the sensitive
16459 * data stored for the module as a JSON in the database.
16460 *
16461 * I used the same suggested hack by the theme review team.
16462 * For more details, look at the function `Base64UrlDecode()`
16463 * in `./sdk/FreemiusBase.php`.
16464 *
16465 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16466 *
16467 * @author Vova Feldman (@svovaf)
16468 * @since 1.2.2
16469 */
16470 $fn = 'base64' . '_encode';
16471
16472 return $fn( $str );
16473 }
16474
16475 static function _decrypt( $str ) {
16476 if ( is_null( $str ) ) {
16477 return null;
16478 }
16479
16480 /**
16481 * The encrypt/decrypt functions are used to protect
16482 * the user from messing up with some of the sensitive
16483 * data stored for the module as a JSON in the database.
16484 *
16485 * I used the same suggested hack by the theme review team.
16486 * For more details, look at the function `Base64UrlDecode()`
16487 * in `./sdk/FreemiusBase.php`.
16488 *
16489 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16490 *
16491 * @author Vova Feldman (@svovaf)
16492 * @since 1.2.2
16493 */
16494 $fn = 'base64' . '_decode';
16495
16496 return $fn( $str );
16497 }
16498
16499 /**
16500 * @author Vova Feldman (@svovaf)
16501 * @since 1.0.5
16502 *
16503 * @param FS_Entity $entity
16504 *
16505 * @return FS_Entity Return an encrypted clone entity.
16506 */
16507 private static function _encrypt_entity( FS_Entity $entity ) {
16508 $clone = clone $entity;
16509 $props = get_object_vars( $entity );
16510
16511 foreach ( $props as $key => $val ) {
16512 $clone->{$key} = self::_encrypt( $val );
16513 }
16514
16515 return $clone;
16516 }
16517
16518 /**
16519 * @author Vova Feldman (@svovaf)
16520 * @since 1.0.5
16521 *
16522 * @param FS_Entity $entity
16523 *
16524 * @return FS_Entity Return an decrypted clone entity.
16525 */
16526 private static function decrypt_entity( FS_Entity $entity ) {
16527 $clone = clone $entity;
16528 $props = get_object_vars( $entity );
16529
16530 foreach ( $props as $key => $val ) {
16531 $clone->{$key} = self::_decrypt( $val );
16532 }
16533
16534 return $clone;
16535 }
16536
16537 /**
16538 * @author Vova Feldman (@svovaf)
16539 * @since 1.0.7
16540 *
16541 * @param string $email
16542 *
16543 * @return FS_User|false
16544 */
16545 public static function _get_user_by_email( $email ) {
16546 self::$_static_logger->entrance();
16547
16548 $email = trim( strtolower( $email ) );
16549
16550 $users = self::get_all_users();
16551
16552 if ( is_array( $users ) ) {
16553 foreach ( $users as $user ) {
16554 if ( $email === trim( strtolower( $user->email ) ) ) {
16555 return $user;
16556 }
16557 }
16558 }
16559
16560 return false;
16561 }
16562
16563 #----------------------------------------------------------------------------------
16564 #region Account (Loading, Updates & Activation)
16565 #----------------------------------------------------------------------------------
16566
16567 /***
16568 * Load account information (user + site).
16569 *
16570 * @author Vova Feldman (@svovaf)
16571 * @since 1.0.1
16572 */
16573 private function _load_account() {
16574 $this->_logger->entrance();
16575
16576 $this->do_action( 'before_account_load' );
16577
16578 $users = self::get_all_users();
16579 $plans = self::get_all_plans( $this->_module_type );
16580
16581 if ( $this->_logger->is_on() && is_admin() ) {
16582 $this->_logger->log( 'users = ' . var_export( $users, true ) );
16583 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
16584 }
16585
16586 $site = fs_is_network_admin() ?
16587 $this->get_network_install() :
16588 $this->get_install_by_blog_id();
16589
16590 if ( fs_is_network_admin() &&
16591 $this->is_network_active() &&
16592 ! is_object( $site ) &&
16593 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
16594 ) {
16595 $first_install = $this->find_first_install();
16596
16597 if ( is_null( $first_install ) ) {
16598 unset( $this->_storage->network_install_blog_id );
16599 } else {
16600 $site = $first_install['install'];
16601 $this->_storage->network_install_blog_id = $first_install['blog_id'];
16602 }
16603 }
16604
16605 if ( is_object( $site ) &&
16606 is_numeric( $site->id ) &&
16607 is_numeric( $site->user_id ) &&
16608 FS_Plugin_Plan::is_valid_id( $site->plan_id )
16609 ) {
16610 // Load site.
16611 $this->_site = $site;
16612 }
16613
16614 $user = null;
16615 if ( fs_is_network_admin() && $this->_is_network_active ) {
16616 $user = $this->get_network_user();
16617 }
16618
16619 if ( is_object( $user ) ) {
16620 $this->_user = clone $user;
16621 } else if ( $this->_site ) {
16622 $user = self::_get_user_by_id( $this->_site->user_id );
16623
16624 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
16625 /**
16626 * Try to load the previous owner. This recovery is used for the following use-case:
16627 * 1. Opt-in
16628 * 2. Cloning site1 to site2
16629 * 3. Ownership switch in site1 (same applies for site2)
16630 * 4. Install data sync on site2
16631 * 5. Now site2's install is associated with the new owner which does not exists locally.
16632 */
16633 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
16634 }
16635
16636 if ( ! is_object( $user ) ) {
16637 /**
16638 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
16639 */
16640 if (
16641 ! isset( $this->_storage->user_recovery_from_install_last_attempt_timestamp ) ||
16642 time() > ( $this->_storage->user_recovery_from_install_last_attempt_timestamp + FS_Clone_Manager::CLONE_RESOLUTION_MAX_EXECUTION_TIME )
16643 ) {
16644 $user = $this->sync_user_by_current_install();
16645 } else {
16646 return;
16647 }
16648
16649 if ( is_object( $user ) ) {
16650 $this->_storage->user_was_recovered_from_install = true;
16651 } else {
16652 $this->_storage->user_recovery_from_install_attempts = isset( $this->_storage->user_recovery_from_install_attempts ) ?
16653 ( $this->_storage->user_recovery_from_install_attempts + 1 ) :
16654 1;
16655
16656 if ( $this->_storage->user_recovery_from_install_attempts >= 3 ) {
16657 $this->delete_current_install( false );
16658 } else {
16659 $this->_storage->user_recovery_from_install_last_attempt_timestamp = time();
16660
16661 return;
16662 }
16663 }
16664 }
16665
16666 $this->_user = ( $user instanceof FS_User ) ?
16667 clone $user :
16668 null;
16669 }
16670
16671 if ( is_object( $this->_user ) ) {
16672 // Load licenses.
16673 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16674 }
16675
16676 if ( is_object( $this->_site ) ) {
16677 // Load plans.
16678 $this->_plans = isset( $plans[ $this->_slug ] ) ?
16679 $plans[ $this->_slug ] :
16680 array();
16681
16682 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
16683 $this->_sync_plans();
16684 } else {
16685 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16686 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
16687 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
16688 } else {
16689 unset( $this->_plans[ $i ] );
16690 }
16691 }
16692 }
16693
16694 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
16695
16696 if ( $this->_site->version != $this->get_plugin_version() ) {
16697 // If stored install version is different than current installed plugin version,
16698 // then update plugin version event.
16699 $this->update_plugin_version_event();
16700 }
16701 }
16702
16703 if ( true === $this->_storage->require_license_activation &&
16704 ! fs_request_get_bool( 'require_license', true )
16705 ) {
16706 $this->_storage->require_license_activation = false;
16707 }
16708
16709 if ( $this->is_theme() ) {
16710 $this->_register_account_hooks();
16711 }
16712
16713 if ( $this->is_user_in_admin() && $this->is_clone() ) {
16714 if ( empty( FS_Clone_Manager::instance()->get_clone_identification_timestamp() ) ) {
16715 FS_Clone_Manager::instance()->store_clone_identification_timestamp();
16716 }
16717
16718 FS_Clone_Manager::instance()->maybe_update_clone_resolution_support_flag( $this->_storage->sdk_last_version );
16719 $this->send_pending_clone_update_once();
16720 }
16721 }
16722
16723 /**
16724 * Special user recovery mechanism.
16725 *
16726 * @author Vova Feldman (@svovaf)
16727 * @since 2.0.0
16728 *
16729 * @param number|null $site_user_id
16730 *
16731 * @return \FS_User|mixed
16732 */
16733 private function sync_user_by_current_install( $site_user_id = null ) {
16734 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
16735 $site_user_id :
16736 $this->_site->user_id;
16737
16738 $api = $this->get_api_site_scope();
16739
16740 $uid = $this->get_anonymous_id();
16741 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
16742
16743 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
16744
16745 if ( $this->is_api_result_entity( $result ) ) {
16746 $user = new FS_User( $result );
16747 $this->_user = $user;
16748 $this->_store_user();
16749
16750 return $user;
16751 }
16752
16753 $error_code = FS_Api::get_error_code( $result );
16754
16755 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
16756 /**
16757 * Those API errors will continue coming and are not recoverable with the
16758 * current site's data. Therefore, extend the API call's cached result to 7 days.
16759 */
16760 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
16761 }
16762
16763 return $result;
16764 }
16765
16766 /**
16767 * @author Vova Feldman (@svovaf)
16768 * @since 1.0.1
16769 *
16770 * @param FS_User $user
16771 * @param FS_Site $site
16772 * @param bool|array $plans
16773 */
16774 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
16775 $site->user_id = $user->id;
16776
16777 $this->_site = $site;
16778 $this->_user = $user;
16779 if ( false !== $plans ) {
16780 $this->_plans = $plans;
16781 }
16782
16783 $this->send_install_update();
16784
16785 $this->_store_account();
16786
16787 }
16788
16789 /**
16790 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
16791 * Each version is trimmed after the 16th char.
16792 *
16793 * @author Vova Feldman (@svovaf)
16794 * @since 2.2.1
16795 *
16796 * @return array
16797 */
16798 private function get_versions() {
16799 $versions = array();
16800 $versions['sdk_version'] = $this->version;
16801
16802 // Collect these diagnostic information only if it's allowed.
16803 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
16804 $versions['platform_version'] = get_bloginfo( 'version' );
16805 $versions['programming_language_version'] = phpversion();
16806 }
16807
16808 foreach ( $versions as $k => $version ) {
16809 $versions[ $k ] = self::get_api_sanitized_property( $k, $version );
16810 }
16811
16812 return $versions;
16813 }
16814
16815 /**
16816 * Get sanitized site language.
16817 *
16818 * @param string $language
16819 * @param int $max_len
16820 *
16821 * @since 2.5.1
16822 * @author Vova Feldman (@svovaf)
16823 *
16824 * @return string
16825 */
16826 private static function get_sanitized_language( $language = '', $max_len = self::LANGUAGE_MAX_CHARS ) {
16827 if ( empty( $language ) ) {
16828 $language = get_bloginfo( 'language' );
16829 }
16830
16831 return substr( $language, 0, $max_len );
16832 }
16833
16834 /**
16835 * Get core version stripped from pre-release and build.
16836 *
16837 * @since 2.5.1
16838 * @author Vova Feldman (@svovaf)
16839 *
16840 * @param string $version
16841 * @param int $parts
16842 * @param int $max_len
16843 * @param bool $include_pre_release
16844 *
16845 * @return string
16846 */
16847 private static function get_core_version(
16848 $version,
16849 $parts = 3,
16850 $max_len = self::VERSION_MAX_CHARS,
16851 $include_pre_release = false
16852 ) {
16853 if ( empty( $version ) ) {
16854 // Version is empty.
16855 return '';
16856 }
16857
16858 if ( is_numeric( $version ) ) {
16859 $is_float_version = is_float( $version );
16860
16861 $version = (string) $version;
16862
16863 /**
16864 * 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.
16865 */
16866 if ( $is_float_version && false === strpos( $version, '.' ) ) {
16867 $version .= '.0';
16868 }
16869 }
16870
16871 if ( ! is_string( $version ) ) {
16872 return '';
16873 }
16874
16875 if ( $parts < 1 ) {
16876 return '';
16877 }
16878
16879 $pre_release_regex = $include_pre_release ?
16880 '(\-(alpha|beta|RC)([0-9]+)?)?' :
16881 '';
16882
16883 if ( 0 === preg_match( '/^([0-9]+(\.[0-9]+){0,' . ( $parts - 1 ) . '}' . $pre_release_regex . ')/i', $version, $matches ) ) {
16884 // Version is not starting with a digit.
16885 return '';
16886 }
16887
16888 return substr( $matches[1], 0, $max_len );
16889 }
16890
16891 /**
16892 * @param string $prop
16893 * @param mixed $val
16894 *
16895 * @return mixed
16896 *@author Vova Feldman (@svovaf)
16897 *
16898 * @since 2.5.1
16899 */
16900 private static function get_api_sanitized_property( $prop, $val ) {
16901 if ( ! is_string( $val ) || empty( $val ) ) {
16902 return $val;
16903 }
16904
16905 switch ( $prop ) {
16906 case 'programming_language_version':
16907 // Get core PHP version, which can have up to 3 parts (ignore pre-releases).
16908 return self::get_core_version( $val );
16909 case 'platform_version':
16910 // Get the exact WordPress version, which can have up to 3 parts (including pre-releases).
16911 return self::get_core_version( $val, 3, self::VERSION_MAX_CHARS, true );
16912 case 'sdk_version':
16913 // Get the exact SDK version, which can have up to 4 parts.
16914 return self::get_core_version( $val, 4 );
16915 case 'version':
16916 // Get the entire version but just limited in length.
16917 return substr( $val, 0, self::VERSION_MAX_CHARS );
16918 case 'language':
16919 return self::get_sanitized_language( $val );
16920 default:
16921 return $val;
16922 }
16923 }
16924
16925 /**
16926 * @author Leo Fajardo (@leorw)
16927 * @since 2.3.0
16928 *
16929 * @return bool
16930 */
16931 function has_beta_update() {
16932 return (
16933 ! empty( $this->_storage->beta_data ) &&
16934 ( true === $this->_storage->beta_data['is_beta'] ) &&
16935 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
16936 );
16937 }
16938
16939 /**
16940 * @author Leo Fajardo (@leorw)
16941 * @since 2.3.0
16942 *
16943 * @return bool
16944 */
16945 function is_beta() {
16946 return (
16947 ! empty( $this->_storage->beta_data ) &&
16948 ( true === $this->_storage->beta_data['is_beta'] ) &&
16949 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
16950 );
16951 }
16952
16953 /**
16954 * @author Vova Feldman (@svovaf)
16955 * @since 1.1.7.4
16956 *
16957 * @param array $override_with
16958 * @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.
16959 * @param bool $skip_user_info
16960 *
16961 * @return array
16962 */
16963 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null, $skip_user_info = false ) {
16964 $this->_logger->entrance();
16965
16966 $activation_action = $this->get_unique_affix() . '_activate_new';
16967 $return_url = $this->is_anonymous() ?
16968 // If skipped already, then return to the account page.
16969 $this->get_account_url( $activation_action, array(), false ) :
16970 // Return to the module's main page.
16971 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
16972
16973 $versions = $this->get_versions();
16974
16975 $params = array_merge( $versions, array(
16976 'plugin_slug' => $this->_slug,
16977 'plugin_id' => $this->get_id(),
16978 'plugin_public_key' => $this->get_public_key(),
16979 'plugin_version' => $this->get_plugin_version(),
16980 'return_url' => fs_nonce_url( $return_url, $activation_action ),
16981 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
16982 'account',
16983 array( 'fs_action' => 'sync_user' )
16984 ), 'sync_user' ),
16985 'is_premium' => $this->is_premium(),
16986 'is_active' => true,
16987 'is_uninstalled' => false,
16988 'is_localhost' => WP_FS__IS_LOCALHOST,
16989 ) );
16990
16991 if (
16992 ! $skip_user_info &&
16993 (
16994 empty( $override_with['user_firstname'] ) ||
16995 empty( $override_with['user_lastname'] ) ||
16996 empty( $override_with['user_email'] )
16997 )
16998 ) {
16999 $current_user = self::_get_current_wp_user();
17000
17001 $params['user_firstname'] = $current_user->user_firstname;
17002 $params['user_lastname'] = $current_user->user_lastname;
17003 $params['user_email'] = $current_user->user_email;
17004 }
17005
17006 if ( $this->is_addon() ) {
17007 $parent_fs = $this->get_parent_instance();
17008
17009 $params['parent_plugin_slug'] = $parent_fs->_slug;
17010 $params['parent_plugin_id'] = $parent_fs->get_id();
17011 }
17012
17013 if ( true === $network_level_or_blog_id ) {
17014 if ( ! isset( $override_with['sites'] ) ) {
17015 $params['sites'] = $this->get_sites_for_network_level_optin();
17016 }
17017 } else {
17018 $site = is_numeric( $network_level_or_blog_id ) ?
17019 array( 'blog_id' => $network_level_or_blog_id ) :
17020 null;
17021
17022 $site = $this->get_site_info( $site );
17023
17024 $diagnostic_info = array();
17025 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17026 $diagnostic_info = array(
17027 'site_name' => $site['title'],
17028 'language' => self::get_sanitized_language( $site['language'] ),
17029 );
17030 }
17031
17032 $params = array_merge( $params, $diagnostic_info, array(
17033 'site_uid' => $site['uid'],
17034 'site_url' => $site['url'],
17035 ) );
17036 }
17037
17038 if ( $this->is_pending_activation() &&
17039 ! empty( $this->_storage->pending_license_key )
17040 ) {
17041 $params['license_key'] = $this->_storage->pending_license_key;
17042 }
17043
17044 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
17045 // Even though rand() is known for its security issues,
17046 // the timestamp adds another layer of protection.
17047 // It would be very hard for an attacker to get the secret key form here.
17048 // Plus, this should never run in production since the secret should never
17049 // be included in the production version.
17050 $params['ts'] = WP_FS__SCRIPT_START_TIME;
17051 $params['salt'] = md5( uniqid( rand() ) );
17052 $params['secure'] = md5(
17053 $params['ts'] .
17054 $params['salt'] .
17055 $this->get_secret_key()
17056 );
17057 }
17058
17059 if ( is_multisite() && function_exists( 'get_network' ) ) {
17060 $params['network_uid'] = $this->get_anonymous_network_id();
17061 }
17062
17063 return array_merge( $params, $override_with );
17064 }
17065
17066 /**
17067 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
17068 * 2. If there was an API error, return the API result.
17069 *
17070 * @author Vova Feldman (@svovaf)
17071 * @since 1.1.7.4
17072 *
17073 * @param string|bool $email
17074 * @param string|bool $first
17075 * @param string|bool $last
17076 * @param string|bool $license_key
17077 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
17078 * In this case, the user and site info will be sent to the server but no
17079 * data will be saved to the WP installation's database.
17080 * @param number|bool $trial_plan_id
17081 * @param bool $is_disconnected Whether to opt in without tracking.
17082 * @param null|bool $is_marketing_allowed
17083 * @param array $sites If network-level opt-in, an array of containing details of sites.
17084 * @param bool $redirect
17085 * @param null|number $license_owner_id
17086 *
17087 * @return string|object
17088 * @use WP_Error
17089 */
17090 function opt_in(
17091 $email = false,
17092 $first = false,
17093 $last = false,
17094 $license_key = false,
17095 $is_uninstall = false,
17096 $trial_plan_id = false,
17097 $is_disconnected = false,
17098 $is_marketing_allowed = null,
17099 $sites = array(),
17100 $redirect = true,
17101 $license_owner_id = null
17102 ) {
17103 $this->_logger->entrance();
17104
17105 /**
17106 * @since 1.2.1 If activating with license key, ignore the context-user
17107 * since the user will be automatically loaded from the license.
17108 */
17109 if ( empty( $license_key ) ) {
17110 // Clean up pending license if opt-ing in again.
17111 $this->_storage->remove( 'pending_license_key' );
17112
17113 if ( ! $is_uninstall ) {
17114 if ( false === $email ) {
17115 $current_user = self::_get_current_wp_user();
17116 $email = $current_user->user_email;
17117 }
17118
17119 $fs_user = Freemius::_get_user_by_email( $email );
17120 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
17121 return $this->install_with_user(
17122 $fs_user,
17123 false,
17124 $trial_plan_id,
17125 $redirect,
17126 true,
17127 $sites
17128 );
17129 }
17130 }
17131 }
17132
17133 $skip_user_info = ( ! empty( $license_key ) && FS_User::is_valid_id( $license_owner_id ) );
17134
17135 $user_info = array();
17136
17137 if ( ! $skip_user_info ) {
17138 if ( ! empty( $email ) ) {
17139 $user_info['user_email'] = $email;
17140 }
17141
17142 if ( ! empty( $first ) ) {
17143 $user_info['user_firstname'] = $first;
17144 }
17145
17146 if ( ! empty( $last ) ) {
17147 $user_info['user_lastname'] = $last;
17148 }
17149 }
17150
17151 if ( ! empty( $sites ) ) {
17152 $is_network = true;
17153
17154 $user_info['sites'] = $sites;
17155 } else {
17156 $is_network = false;
17157 }
17158
17159 $params = $this->get_opt_in_params( $user_info, $is_network, $skip_user_info );
17160
17161 $filtered_license_key = false;
17162 if ( is_string( $license_key ) ) {
17163 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
17164 $params['license_key'] = $filtered_license_key;
17165 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17166 $params['trial_plan_id'] = $trial_plan_id;
17167 }
17168
17169 if ( $is_uninstall ) {
17170 $params['uninstall_params'] = array(
17171 'reason_id' => $this->_storage->uninstall_reason->id,
17172 'reason_info' => $this->_storage->uninstall_reason->info
17173 );
17174 }
17175
17176 if ( isset( $params['license_key'] ) ) {
17177 $fs_user = Freemius::_get_user_by_email( $email );
17178
17179 if ( is_object( $fs_user ) ) {
17180 /**
17181 * If opting in with a context license and the context WP Admin user already opted in
17182 * before from the current site, add the user context security params to avoid the
17183 * unnecessary email activation when the context license is owned by the same context user.
17184 *
17185 * @author Leo Fajardo (@leorw)
17186 * @since 1.2.3
17187 */
17188 $params = array_merge( $params, FS_Security::instance()->get_context_params(
17189 $fs_user,
17190 false,
17191 'install_with_existing_user'
17192 ) );
17193 }
17194 }
17195
17196 if ( is_bool( $is_marketing_allowed ) ) {
17197 $params['is_marketing_allowed'] = $is_marketing_allowed;
17198 }
17199
17200 $params['is_disconnected'] = $is_disconnected;
17201 $params['format'] = 'json';
17202 $params['is_extensions_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed();
17203 $params['is_diagnostic_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed();
17204
17205 $request = array(
17206 'method' => 'POST',
17207 'body' => $params,
17208 'timeout' => 60,
17209 );
17210
17211 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
17212 $response = self::safe_remote_post( $url, $request );
17213
17214 if ( is_wp_error( $response ) ) {
17215 /**
17216 * @var WP_Error $response
17217 */
17218 $result = new stdClass();
17219
17220 $error_code = $response->get_error_code();
17221 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
17222
17223 $result->error = (object) array(
17224 'type' => $error_type,
17225 'message' => $response->get_error_message(),
17226 'code' => $error_code,
17227 'http' => 402
17228 );
17229
17230 $this->maybe_modify_api_curl_error_message( $result );
17231
17232 if ( FS_Api::is_blocked( $result ) ) {
17233 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
17234 }
17235
17236 $is_connected = null;
17237
17238 if ( empty( $license_key ) && $this->is_enable_anonymous() ) {
17239 $this->skip_connection( fs_is_network_admin() );
17240
17241 $is_connected = ( ! FS_Api::is_blocked( $result ) );
17242 }
17243
17244 $this->update_connectivity_info( $is_connected );
17245
17246 return $result;
17247 }
17248
17249 $this->update_connectivity_info( true );
17250
17251 // Module is being uninstalled, don't handle the returned data.
17252 if ( $is_uninstall ) {
17253 return true;
17254 }
17255
17256 /**
17257 * 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.
17258 *
17259 * @author Vova Feldman (@svovaf)
17260 * @since 1.2.3
17261 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
17262 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
17263 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
17264 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
17265 */
17266 $decoded = is_string( $response['body'] ) ?
17267 json_decode( $response['body'] ) :
17268 null;
17269
17270 if ( empty( $decoded ) ) {
17271 return false;
17272 }
17273
17274 if ( ! $this->is_api_result_object( $decoded ) ) {
17275 if ( ! empty( $params['license_key'] ) ) {
17276 // Pass the fully entered license key to the failure handler.
17277 $params['license_key'] = $license_key;
17278 }
17279
17280 return $is_uninstall ?
17281 $decoded :
17282 $this->apply_filters( 'after_install_failure', $decoded, $params );
17283 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
17284 if ( $is_network ) {
17285 $site_ids = array();
17286 foreach ( $sites as $site ) {
17287 $site_ids[] = $site['blog_id'];
17288 }
17289
17290 /**
17291 * Store the sites so that they can be installed once the user has clicked on the activation link
17292 * in the email.
17293 *
17294 * @author Leo Fajardo (@leorw)
17295 */
17296 $this->_storage->pending_sites_info = array(
17297 'blog_ids' => $site_ids,
17298 'license_key' => $license_key,
17299 'trial_plan_id' => $trial_plan_id
17300 );
17301 }
17302
17303 // Pending activation, add message.
17304 return $this->set_pending_confirmation(
17305 ( isset( $decoded->email ) ?
17306 $decoded->email :
17307 true ),
17308 false,
17309 $filtered_license_key,
17310 ! empty( $params['trial_plan_id'] ),
17311 isset( $decoded->is_suspicious_email ) && $decoded->is_suspicious_email
17312 );
17313 } else if ( isset( $decoded->install_secret_key ) ) {
17314 return $this->install_with_new_user(
17315 $decoded->user_id,
17316 $decoded->user_public_key,
17317 $decoded->user_secret_key,
17318 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17319 $decoded->is_marketing_allowed :
17320 null ),
17321 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17322 $decoded->is_extensions_tracking_allowed :
17323 null ),
17324 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17325 $decoded->is_diagnostic_tracking_allowed :
17326 null ),
17327 $decoded->install_id,
17328 $decoded->install_public_key,
17329 $decoded->install_secret_key,
17330 false
17331 );
17332 } else if ( is_array( $decoded->installs ) ) {
17333 return $this->install_many_with_new_user(
17334 $decoded->user_id,
17335 $decoded->user_public_key,
17336 $decoded->user_secret_key,
17337 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17338 $decoded->is_marketing_allowed :
17339 null ),
17340 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17341 $decoded->is_extensions_tracking_allowed :
17342 null ),
17343 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17344 $decoded->is_diagnostic_tracking_allowed :
17345 null ),
17346 $decoded->installs,
17347 false
17348 );
17349 }
17350
17351 return $decoded;
17352 }
17353
17354 /**
17355 * Set user and site identities.
17356 *
17357 * @author Vova Feldman (@svovaf)
17358 * @since 1.0.9
17359 *
17360 * @param FS_User $user
17361 * @param FS_Site $site
17362 * @param bool $redirect
17363 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
17364 * redirect (or return a URL) to the account page with a special parameter to
17365 * trigger the auto installation processes.
17366 *
17367 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17368 */
17369 function setup_account(
17370 FS_User $user,
17371 FS_Site $site,
17372 $redirect = true,
17373 $auto_install = false
17374 ) {
17375 return $this->setup_network_account(
17376 $user,
17377 array( $site ),
17378 $redirect,
17379 $auto_install,
17380 false
17381 );
17382 }
17383
17384 /**
17385 * Set user and site identities.
17386 *
17387 * @author Vova Feldman (@svovaf)
17388 * @since 2.0.0
17389 *
17390 * @param FS_User $user
17391 * @param FS_Site[] $installs
17392 * @param bool $redirect
17393 * @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.
17394 * @param bool $is_network_level_opt_in
17395 *
17396 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17397 */
17398 function setup_network_account(
17399 FS_User $user,
17400 array $installs,
17401 $redirect = true,
17402 $auto_install = false,
17403 $is_network_level_opt_in = true
17404 ) {
17405 $first_install = $installs[0];
17406
17407 $this->_user = $user;
17408 $this->_site = $first_install;
17409
17410 $this->_sync_plans();
17411
17412 if ( $this->_storage->handle_gdpr_admin_notice &&
17413 $this->should_handle_gdpr_admin_notice() &&
17414 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
17415 ) {
17416 /**
17417 * Clear user lock after an opt-in.
17418 */
17419 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
17420 FS_User_Lock::instance()->unlock();
17421 }
17422
17423 if ( 1 < count( $installs ) ) {
17424 // Only network level opt-in can have more than one install.
17425 $is_network_level_opt_in = true;
17426 }
17427
17428 $this->update_connectivity_info( true );
17429
17430 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
17431 // If Freemius was OFF before, turn it on.
17432 $this->turn_on();
17433
17434 $this->handle_account_connection(
17435 $installs,
17436 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
17437 );
17438
17439 if ( is_numeric( $first_install->license_id ) ) {
17440 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
17441 }
17442
17443 $this->_admin_notices->remove_sticky( 'connect_account' );
17444
17445 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
17446 $this->clear_pending_activation_mode();
17447
17448 if ( ! $this->is_paying_or_trial() ) {
17449 $this->_admin_notices->add_sticky(
17450 sprintf( $this->get_text_inline( '%s opt-in was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
17451 'activation_complete'
17452 );
17453 }
17454 }
17455
17456 if ( $this->is_paying_or_trial() ) {
17457 if ( ! $this->is_premium() ||
17458 ! $this->has_premium_version() ||
17459 ! $this->has_settings_menu()
17460 ) {
17461 if ( $this->is_paying() ) {
17462 $this->add_complete_upgrade_instructions_notice(
17463 sprintf(
17464 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
17465 $this->get_plan_title()
17466 ),
17467 'plan_upgraded'
17468 );
17469 } else {
17470 $trial_plan = $this->get_trial_plan();
17471
17472 $this->add_complete_upgrade_instructions_notice(
17473 sprintf(
17474 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
17475 '<i>' . $this->get_plugin_name() . '</i>'
17476 ),
17477 'trial_started',
17478 $trial_plan->title
17479 );
17480 }
17481 }
17482
17483 $this->_admin_notices->remove_sticky( array(
17484 'trial_promotion',
17485 ) );
17486 }
17487
17488 $plugin_id = fs_request_get( 'plugin_id', false );
17489
17490 // Store activation time ONLY for plugins & themes (not add-ons).
17491 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
17492 if ( empty( $this->_storage->activation_timestamp ) ) {
17493 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
17494 }
17495 }
17496
17497 $next_page = '';
17498
17499 $extra = array();
17500 if ( $auto_install ) {
17501 $extra['auto_install'] = 'true';
17502 }
17503
17504 if ( is_numeric( $plugin_id ) ) {
17505 /**
17506 * @author Leo Fajardo (@leorw)
17507 * @since 1.2.1.6
17508 *
17509 * Also sync the license after an anonymous user subscribes.
17510 */
17511 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
17512 // Add-on was installed - sync license right after install.
17513 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
17514 }
17515 } else {
17516 /**
17517 * @author Vova Feldman (@svovaf)
17518 * @since 1.1.9 If site installed with a valid license, sync license.
17519 */
17520 if ( $this->is_paying() ) {
17521 $this->_sync_plugin_license(
17522 true,
17523 // Installs data is already synced in the beginning of this method directly or via _set_account().
17524 false
17525 );
17526 }
17527
17528 // Reload the page with the keys.
17529 $next_page = $this->is_anonymous() ?
17530 // If user previously skipped, redirect to account page.
17531 $this->get_account_url( false, $extra ) :
17532 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
17533 }
17534
17535 if ( ! empty( $next_page ) && $redirect ) {
17536 fs_redirect( $next_page );
17537 }
17538
17539 return $next_page;
17540 }
17541
17542 /**
17543 * Install plugin with new user information after approval.
17544 *
17545 * @author Vova Feldman (@svovaf)
17546 * @since 1.0.7
17547 */
17548 function _install_with_new_user() {
17549 $this->_logger->entrance();
17550
17551 if ( $this->is_registered() ) {
17552 return;
17553 }
17554
17555 $has_pending_activation_confirmation_param = fs_request_has( 'pending_activation' );
17556
17557 $this->update_license_required_permissions_if_anonymous();
17558
17559 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17560 // @todo This logic should be improved because it's executed on every load of a theme.
17561 $this->is_theme()
17562 ) {
17563 // check_admin_referer( $this->_slug . '_activate_new' );
17564
17565 if ( fs_request_has( 'user_secret_key' ) ) {
17566 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17567 $pending_sites_info = $this->_storage->pending_sites_info;
17568
17569 $this->install_many_pending_with_user(
17570 fs_request_get( 'user_id' ),
17571 fs_request_get_raw( 'user_public_key' ),
17572 fs_request_get_raw( 'user_secret_key' ),
17573 fs_request_get_bool( 'is_marketing_allowed', null ),
17574 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17575 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17576 $pending_sites_info['blog_ids'],
17577 $pending_sites_info['license_key'],
17578 $pending_sites_info['trial_plan_id']
17579 );
17580 } else {
17581 $this->install_with_new_user(
17582 fs_request_get( 'user_id' ),
17583 fs_request_get_raw( 'user_public_key' ),
17584 fs_request_get_raw( 'user_secret_key' ),
17585 fs_request_get_bool( 'is_marketing_allowed', null ),
17586 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17587 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17588 fs_request_get( 'install_id' ),
17589 fs_request_get_raw( 'install_public_key' ),
17590 fs_request_get_raw( 'install_secret_key' ),
17591 true,
17592 fs_request_get_bool( 'auto_install' )
17593 );
17594 }
17595 } else if ( $has_pending_activation_confirmation_param ) {
17596 $this->set_pending_confirmation(
17597 fs_request_get( 'user_email' ),
17598 true,
17599 false,
17600 false,
17601 fs_request_get_bool( 'is_suspicious_email' ),
17602 fs_request_get_bool( 'has_upgrade_context' ),
17603 fs_request_get( 'support_email_address' )
17604 );
17605 }
17606 }
17607 }
17608
17609 /**
17610 * @author Vova Feldman (@svovaf)
17611 * @since 2.0.0
17612 *
17613 * @param number $id
17614 * @param string $public_key
17615 * @param string $secret_key
17616 *
17617 * @return \FS_User
17618 */
17619 private function setup_user( $id, $public_key, $secret_key ) {
17620 $user = self::_get_user_by_id( $id );
17621
17622 if ( is_object( $user ) ) {
17623 $this->_user = $user;
17624 } else {
17625 $user = new FS_User();
17626 $user->id = $id;
17627 $user->public_key = $public_key;
17628 $user->secret_key = $secret_key;
17629
17630 $this->_user = $user;
17631 $user_result = $this->get_api_user_scope()->get();
17632 $user = new FS_User( $user_result );
17633
17634 $this->_user = $user;
17635 $this->_store_user();
17636 }
17637
17638 return $user;
17639 }
17640
17641 /**
17642 * Install plugin with new user.
17643 *
17644 * @author Vova Feldman (@svovaf)
17645 * @since 1.1.7.4
17646 *
17647 * @param number $user_id
17648 * @param string $user_public_key
17649 * @param string $user_secret_key
17650 * @param bool|null $is_marketing_allowed
17651 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17652 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17653 * @param number $install_id
17654 * @param string $install_public_key
17655 * @param string $install_secret_key
17656 * @param bool $redirect
17657 * @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.
17658 *
17659 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17660 */
17661 private function install_with_new_user(
17662 $user_id,
17663 $user_public_key,
17664 $user_secret_key,
17665 $is_marketing_allowed,
17666 $is_extensions_tracking_allowed,
17667 $is_diagnostic_tracking_allowed,
17668 $install_id,
17669 $install_public_key,
17670 $install_secret_key,
17671 $redirect = true,
17672 $auto_install = false
17673 ) {
17674 /**
17675 * This method is also executed after opting in with a license key since the
17676 * license can be potentially associated with a different owner.
17677 *
17678 * @since 2.0.0
17679 */
17680 $user = self::_get_user_by_id( $user_id );
17681
17682 if ( ! is_object( $user ) ) {
17683 $user = new FS_User();
17684 $user->id = $user_id;
17685 $user->public_key = $user_public_key;
17686 $user->secret_key = $user_secret_key;
17687
17688 $this->_user = $user;
17689 $user_result = $this->get_api_user_scope()->get();
17690 $user = new FS_User( $user_result );
17691 }
17692
17693 $this->_user = $user;
17694
17695 $site = new FS_Site();
17696 $site->id = $install_id;
17697 $site->public_key = $install_public_key;
17698 $site->secret_key = $install_secret_key;
17699
17700 $this->_site = $site;
17701 $site_result = $this->get_api_site_scope( true )->get();
17702 $site = new FS_Site( $site_result );
17703 $this->_site = $site;
17704
17705 if ( ! is_null( $is_marketing_allowed ) ) {
17706 $this->disable_opt_in_notice_and_lock_user();
17707 }
17708
17709 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17710 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17711 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17712 ) );
17713
17714 return $this->setup_account(
17715 $this->_user,
17716 $this->_site,
17717 $redirect,
17718 $auto_install
17719 );
17720 }
17721
17722 /**
17723 * Install plugin with user.
17724 *
17725 * @author Leo Fajardo (@leorw)
17726 * @since 2.0.0
17727 *
17728 * @param number $user_id
17729 * @param string $user_public_key
17730 * @param string $user_secret_key
17731 * @param bool|null $is_marketing_allowed
17732 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17733 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17734 * @param array $site_ids
17735 * @param bool $license_key
17736 * @param bool $trial_plan_id
17737 * @param bool $redirect
17738 *
17739 * @return void
17740 */
17741 private function install_many_pending_with_user(
17742 $user_id,
17743 $user_public_key,
17744 $user_secret_key,
17745 $is_marketing_allowed,
17746 $is_extensions_tracking_allowed,
17747 $is_diagnostic_tracking_allowed,
17748 $site_ids,
17749 $license_key = false,
17750 $trial_plan_id = false,
17751 $redirect = true
17752 ) {
17753 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17754
17755 if ( ! is_null( $is_marketing_allowed ) ) {
17756 $this->disable_opt_in_notice_and_lock_user();
17757 }
17758
17759 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17760 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17761 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17762 ) );
17763
17764 $sites = array();
17765 foreach ( $site_ids as $site_id ) {
17766 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
17767 }
17768
17769 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17770 }
17771
17772 /**
17773 * Multi-site install with a new user.
17774 *
17775 * @author Vova Feldman (@svovaf)
17776 * @since 2.0.0
17777 *
17778 * @param number $user_id
17779 * @param string $user_public_key
17780 * @param string $user_secret_key
17781 * @param bool|null $is_marketing_allowed
17782 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17783 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17784 * @param object[] $installs
17785 * @param bool $redirect
17786 * @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.
17787 *
17788 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17789 */
17790 private function install_many_with_new_user(
17791 $user_id,
17792 $user_public_key,
17793 $user_secret_key,
17794 $is_marketing_allowed,
17795 $is_extensions_tracking_allowed,
17796 $is_diagnostic_tracking_allowed,
17797 array $installs,
17798 $redirect = true,
17799 $auto_install = false
17800 ) {
17801 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17802
17803 if ( ! is_null( $is_marketing_allowed ) ) {
17804 $this->disable_opt_in_notice_and_lock_user();
17805 }
17806
17807 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17808 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17809 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17810 ) );
17811
17812 $install_ids = array();
17813
17814 foreach ( $installs as $install ) {
17815 $install_ids[] = $install->id;
17816 }
17817
17818 $items_per_request = 25;
17819 $left = count( $install_ids );
17820 $offset = 0;
17821
17822 $installs = array();
17823 while ( $left > 0 ) {
17824 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, $items_per_request ) ) );
17825
17826 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
17827 // @todo Handle API error.
17828 }
17829
17830 $installs = array_merge( $installs, $result->installs );
17831
17832 $left -= $items_per_request;
17833 $offset += $items_per_request;
17834 }
17835
17836 foreach ( $installs as &$install ) {
17837 $install = new FS_Site( $install );
17838 }
17839
17840 return $this->setup_network_account(
17841 $this->_user,
17842 $installs,
17843 $redirect,
17844 $auto_install
17845 );
17846 }
17847
17848 /**
17849 * @author Vova Feldman (@svovaf)
17850 * @since 1.1.7.4
17851 *
17852 * @param string|bool $email
17853 * @param bool $redirect
17854 * @param string|bool $license_key Since 1.2.1.5
17855 * @param bool $is_pending_trial Since 1.2.1.5
17856 * @param bool $is_suspicious_email Since 2.5.0
17857 * @param bool $has_upgrade_context Since 2.5.3
17858 * @param bool|string $support_email_address Since 2.5.3
17859 *
17860 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
17861 */
17862 private function set_pending_confirmation(
17863 $email = false,
17864 $redirect = true,
17865 $license_key = false,
17866 $is_pending_trial = false,
17867 $is_suspicious_email = false,
17868 $has_upgrade_context = false,
17869 $support_email_address = false
17870 ) {
17871 $is_network_admin = fs_is_network_admin();
17872
17873 if ( $this->_ignore_pending_mode && ! $has_upgrade_context ) {
17874 /**
17875 * If explicitly asked to ignore pending mode, set to anonymous mode
17876 * 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).
17877 *
17878 * @author Vova Feldman
17879 * @since 1.2.1.6
17880 */
17881 $this->skip_connection( $is_network_admin );
17882 } else {
17883 // Install must be activated via email since
17884 // user with the same email already exist.
17885 $this->_storage->is_pending_activation = true;
17886 $this->_add_pending_activation_notice(
17887 $email,
17888 $is_pending_trial,
17889 $is_suspicious_email,
17890 $has_upgrade_context,
17891 $support_email_address
17892 );
17893 }
17894
17895 if ( ! empty( $license_key ) ) {
17896 $this->_storage->pending_license_key = $license_key;
17897 }
17898
17899 // Remove the opt-in sticky notice.
17900 $this->_admin_notices->remove_sticky( array(
17901 'connect_account',
17902 'trial_promotion',
17903 ) );
17904
17905 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
17906
17907 if ( $redirect ) {
17908 // Reload the page with a pending activation message.
17909 fs_redirect( $next_page );
17910 }
17911
17912 return $next_page;
17913 }
17914
17915 /**
17916 * Install plugin with current logged WP user info.
17917 *
17918 * @author Vova Feldman (@svovaf)
17919 * @since 1.0.7
17920 */
17921 function _install_with_current_user() {
17922 $this->_logger->entrance();
17923
17924 if ( $this->is_registered() ) {
17925 return;
17926 }
17927
17928 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
17929 check_admin_referer( $this->get_unique_affix() . '_activate_existing' );
17930
17931 /**
17932 * @author Vova Feldman (@svovaf)
17933 * @since 1.1.9 Add license key if given.
17934 */
17935 $license_key = fs_request_get_raw( 'license_secret_key' );
17936
17937 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17938 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17939 FS_Permission_Manager::PERMISSION_EXTENSIONS => fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17940 ) );
17941
17942 $this->install_with_current_user( $license_key );
17943 }
17944 }
17945
17946
17947 /**
17948 * @author Vova Feldman (@svovaf)
17949 * @since 1.1.7.4
17950 *
17951 * @param string|bool $license_key
17952 * @param number|bool $trial_plan_id
17953 * @param array $sites Since 2.0.0
17954 * @param bool $redirect
17955 *
17956 * @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.
17957 */
17958 function install_with_current_user(
17959 $license_key = false,
17960 $trial_plan_id = false,
17961 $sites = array(),
17962 $redirect = true
17963 ) {
17964 // Get current logged WP user.
17965 $current_user = self::_get_current_wp_user();
17966
17967 // Find the relevant FS user by the email.
17968 $user = self::_get_user_by_email( $current_user->user_email );
17969
17970 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17971 }
17972
17973 /**
17974 * @author Vova Feldman (@svovaf)
17975 * @since 2.0.0
17976 *
17977 * @param \FS_User $user
17978 * @param string|bool $license_key
17979 * @param number|bool $trial_plan_id
17980 * @param bool $redirect
17981 * @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.
17982 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
17983 *
17984 * @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.
17985 */
17986 function install_with_user(
17987 FS_User $user,
17988 $license_key = false,
17989 $trial_plan_id = false,
17990 $redirect = true,
17991 $setup_account = true,
17992 $sites = array()
17993 ) {
17994 // We have to set the user before getting user scope API handler.
17995 $this->_user = $user;
17996
17997 // Install the plugin.
17998 $result = $this->create_installs_with_user(
17999 $user,
18000 $license_key,
18001 $trial_plan_id,
18002 $sites,
18003 $redirect
18004 );
18005
18006 if ( ! $this->is_api_result_entity( $result ) &&
18007 ! $this->is_api_result_object( $result, 'installs' )
18008 ) {
18009 // @todo Handler potential API error of the $result
18010 }
18011
18012 if ( empty( $sites ) ) {
18013 $site = new FS_Site( $result );
18014 $this->_site = $site;
18015
18016 if ( ! $setup_account ) {
18017 $this->_store_site();
18018
18019 $this->sync_plan_if_not_exist( $site->plan_id );
18020
18021 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
18022 $this->sync_license_if_not_exist( $site->license_id, $license_key );
18023 }
18024
18025 $this->_admin_notices->remove_sticky( 'connect_account', false );
18026
18027 return $site;
18028 }
18029
18030 return $this->setup_account( $this->_user, $this->_site, $redirect );
18031 } else {
18032 $installs = array();
18033 foreach ( $result->installs as $install ) {
18034 $installs[] = new FS_Site( $install );
18035 }
18036
18037 return $this->setup_network_account(
18038 $user,
18039 $installs,
18040 $redirect
18041 );
18042 }
18043 }
18044
18045 /**
18046 * Initiate an API request to create a collection of installs.
18047 *
18048 * @author Vova Feldman (@svovaf)
18049 * @since 2.0.0
18050 *
18051 * @param \FS_User $user
18052 * @param bool $license_key
18053 * @param bool $trial_plan_id
18054 * @param array $sites
18055 * @param bool $redirect
18056 * @param bool $silent
18057 *
18058 * @return object|mixed
18059 */
18060 private function create_installs_with_user(
18061 FS_User $user,
18062 $license_key = false,
18063 $trial_plan_id = false,
18064 $sites = array(),
18065 $redirect = false,
18066 $silent = false
18067 ) {
18068 $extra_install_params = array(
18069 'uid' => $this->get_anonymous_id(),
18070 'is_disconnected' => false,
18071 );
18072
18073 if ( ! empty( $license_key ) ) {
18074 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
18075
18076 if ( $silent ) {
18077 $extra_install_params['ignore_license_owner'] = true;
18078 }
18079 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
18080 $extra_install_params['trial_plan_id'] = $trial_plan_id;
18081 }
18082
18083 if ( ! empty( $sites ) ) {
18084 $extra_install_params['sites'] = $sites;
18085 }
18086
18087 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
18088
18089 // Install the plugin.
18090 $result = $this->get_api_user_scope_by_user( $user )->call(
18091 "/plugins/{$this->get_id()}/installs.json",
18092 'post',
18093 $args
18094 );
18095
18096 if ( ! $this->is_api_result_entity( $result ) &&
18097 ! $this->is_api_result_object( $result, 'installs' )
18098 ) {
18099 if ( ! empty( $args['license_key'] ) ) {
18100 // Pass the fully entered license key to the failure handler.
18101 $args['license_key'] = $license_key;
18102 }
18103
18104 $result = $this->apply_filters( 'after_install_failure', $result, $args );
18105
18106 if ( ! $silent ) {
18107 $this->_admin_notices->add(
18108 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18109 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
18110 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18111 'error'
18112 );
18113 }
18114
18115 if ( $redirect ) {
18116 /**
18117 * We set the user before getting the user scope API handler, so the user became temporarily
18118 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
18119 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
18120 * activation page based on the return value of `is_registered()`. In addition, in case the
18121 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
18122 * misleading plugin activation errors will be shown on the `Plugins` page.
18123 *
18124 * @author Leo Fajardo (@leorw)
18125 */
18126 $user = $this->_user;
18127
18128 $this->_user = null;
18129
18130 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
18131
18132 /**
18133 * Restore the user after the redirect, this is relevant when there are cases where the redirect will choose not to do anything.
18134 */
18135 $this->_user = $user;
18136 }
18137 }
18138
18139 return $result;
18140 }
18141
18142 /**
18143 * Tries to activate add-on account based on parent plugin info.
18144 *
18145 * @author Vova Feldman (@svovaf)
18146 * @since 1.0.6
18147 *
18148 * @param Freemius $parent_fs
18149 * @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.
18150 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
18151 */
18152 private function _activate_addon_account(
18153 Freemius $parent_fs,
18154 $network_level_or_blog_id = null,
18155 $bundle_license = null
18156 ) {
18157 if ( $this->is_registered() ) {
18158 // Already activated.
18159 return;
18160 }
18161
18162 $permission_ids = FS_Permission_Manager::get_all_permission_ids();
18163 $permissions = array();
18164 foreach ( $permission_ids as $permission_id ) {
18165 $permissions[ $permission_id ] = FS_Permission_Manager::instance( $parent_fs )->is_permission( $permission_id, true );
18166 }
18167
18168 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( $permissions );
18169
18170 /**
18171 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
18172 * already returns the data for the current blog.
18173 *
18174 * @author Leo Fajardo (@leorw)
18175 * @since 2.3.0
18176 */
18177 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
18178 array() :
18179 array( 'uid' => $this->get_anonymous_id() );
18180
18181 $params = $this->get_install_data_for_api(
18182 $uid_param_to_override,
18183 false,
18184 false,
18185 /**
18186 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
18187 * already includes the data for it.
18188 *
18189 * @author Leo Fajardo (@leorw)
18190 * @since 2.3.0
18191 */
18192 ( true !== $network_level_or_blog_id )
18193 );
18194
18195 if ( true === $network_level_or_blog_id ) {
18196 $params['sites'] = $this->get_sites_for_network_level_optin();
18197
18198 if ( empty( $params['sites'] ) ) {
18199 return;
18200 }
18201 }
18202
18203 if ( is_object( $bundle_license ) ) {
18204 $params['license_key'] = $bundle_license->secret_key;
18205 }
18206
18207 // Activate add-on with parent plugin credentials.
18208 $result = $parent_fs->get_api_site_scope()->call(
18209 "/addons/{$this->_plugin->id}/installs.json",
18210 'post',
18211 $params
18212 );
18213
18214 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18215 if ( is_object( $bundle_license ) ) {
18216 /**
18217 * 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.
18218 *
18219 * @author Leo Fajardo (@leorw)
18220 * @since 2.4.0
18221 */
18222 } else {
18223 $error_message = FS_Api::is_api_error_object( $result ) ?
18224 $result->error->message :
18225 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
18226
18227 $this->_admin_notices->add(
18228 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18229 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
18230 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18231 'error'
18232 );
18233 }
18234
18235 return;
18236 }
18237
18238 $addon_installs = $result->installs;
18239 foreach ( $addon_installs as $key => $addon_install ) {
18240 $addon_installs[ $key ] = new FS_Site( $addon_install );
18241 }
18242
18243 $first_install = $addon_installs[0];
18244
18245 // Get user information based on parent's plugin.
18246 $user = $parent_fs->get_user();
18247
18248 // First of all, set site and user info - otherwise we won't
18249 // be able to invoke API calls.
18250 $this->_site = $first_install;
18251 $this->_user = $user;
18252
18253 // Sync add-on plans.
18254 $this->_sync_plans();
18255
18256 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
18257
18258 // Get site's current plan.
18259 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
18260
18261 // Sync licenses.
18262 $this->_sync_licenses();
18263
18264 if ( ! fs_is_network_admin() ) {
18265 // Try to activate premium license.
18266 $this->_activate_license( true, $bundle_license );
18267
18268 if ( is_object( $bundle_license ) ) {
18269 $this->maybe_activate_bundle_license( $bundle_license );
18270 }
18271 } else {
18272 if ( is_object( $bundle_license ) ) {
18273 $premium_license = $bundle_license;
18274 } else {
18275 $license_id = fs_request_get( 'license_id' );
18276
18277 if ( is_object( $this->_site ) &&
18278 FS_Plugin_License::is_valid_id( $license_id ) &&
18279 $license_id == $this->_site->license_id
18280 ) {
18281 // License is already activated.
18282 return;
18283 }
18284
18285 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
18286 $this->_get_license_by_id( $license_id ) :
18287 $this->_get_available_premium_license();
18288 }
18289
18290 if ( is_object( $premium_license ) ) {
18291 $this->maybe_network_activate_addon_license( $premium_license );
18292 }
18293 }
18294 }
18295
18296 /**
18297 * @author Leo Fajardo (@leorw)
18298 * @since 2.3.0
18299 *
18300 * @param FS_Site[] $installs
18301 * @param bool $is_site_level
18302 */
18303 private function handle_account_connection( $installs, $is_site_level ) {
18304 $first_install = $installs[0];
18305
18306 if ( $is_site_level ) {
18307 $this->_set_account( $this->_user, $first_install );
18308
18309 $this->do_action( 'after_account_connection', $this->_user, $first_install );
18310 } else {
18311 $this->_store_user();
18312
18313 // Map site addresses to their blog IDs.
18314 $address_to_blog_map = $this->get_address_to_blog_map();
18315
18316 $first_blog_id = null;
18317 $blog_2_install_map = array();
18318 foreach ( $installs as $install ) {
18319 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
18320 $blog_id = $address_to_blog_map[ $address ];
18321
18322 $this->_store_site( true, $blog_id, $install );
18323
18324 if ( is_null( $first_blog_id ) ) {
18325 $first_blog_id = $blog_id;
18326 }
18327
18328 $blog_2_install_map[ $blog_id ] = $install;
18329 }
18330
18331 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
18332 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
18333 ) {
18334 // Store network user.
18335 $this->_storage->network_user_id = $this->_user->id;
18336 }
18337
18338 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
18339 $this->_storage->network_install_blog_id = $first_blog_id;
18340 }
18341
18342 if ( count( $installs ) === count( $address_to_blog_map ) ) {
18343 // Super admin opted in for all sites in the network.
18344 $this->_storage->is_network_connected = true;
18345 }
18346
18347 $this->_store_licenses( false );
18348
18349 self::$_accounts->store();
18350
18351 // Don't sync the installs data on network upgrade
18352 if ( ! $this->network_upgrade_mode_completed() ) {
18353 $this->send_installs_update();
18354 }
18355
18356 $current_blog = get_current_blog_id();
18357
18358 foreach ( $blog_2_install_map as $blog_id => $install ) {
18359 $this->switch_to_blog( $blog_id );
18360
18361 $this->do_action( 'after_account_connection', $this->_user, $install );
18362 }
18363
18364 // Switch install context back to the first install.
18365 $this->switch_to_blog(
18366 $current_blog,
18367 $first_install,
18368 ( $this->_site->id != $first_install->id )
18369 );
18370
18371 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
18372 }
18373 }
18374
18375 /**
18376 * Tries to activate parent account based on add-on's info.
18377 *
18378 * @author Vova Feldman (@svovaf)
18379 * @since 1.2.2.7
18380 *
18381 * @param Freemius $parent_fs
18382 */
18383 private function activate_parent_account( Freemius $parent_fs ) {
18384 if ( ! $this->is_addon() ) {
18385 // This is not an add-on.
18386 return;
18387 }
18388
18389 if ( $parent_fs->is_registered() ) {
18390 // Already activated.
18391 return;
18392 }
18393
18394 // Activate parent with add-on's user credentials.
18395 $parent_install = $this->get_api_user_scope()->call(
18396 "/plugins/{$parent_fs->_plugin->id}/installs.json",
18397 'post',
18398 $parent_fs->get_install_data_for_api( array(
18399 'uid' => $parent_fs->get_anonymous_id(),
18400 ), false, false )
18401 );
18402
18403 if ( isset( $parent_install->error ) ) {
18404 $this->_admin_notices->add(
18405 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18406 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
18407 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18408 'error'
18409 );
18410
18411 return;
18412 }
18413
18414 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
18415
18416 if ( $parent_fs->is_pending_activation() ) {
18417 $parent_fs->clear_pending_activation_mode();
18418 }
18419
18420 // Get user information based on parent's plugin.
18421 $user = $this->get_user();
18422
18423 // First of all, set site info - otherwise we won't
18424 // be able to invoke API calls.
18425 $parent_fs->_site = new FS_Site( $parent_install );
18426 $parent_fs->_user = $user;
18427
18428 // Sync add-on plans.
18429 $parent_fs->_sync_plans();
18430
18431 $parent_fs->update_license_required_permissions_if_anonymous();
18432
18433 $parent_fs->_set_account( $user, $parent_fs->_site );
18434 }
18435
18436 #endregion
18437
18438 #----------------------------------------------------------------------------------
18439 #region Admin Menu Items
18440 #----------------------------------------------------------------------------------
18441
18442 private $_menu_items = array();
18443
18444 /**
18445 * @author Vova Feldman (@svovaf)
18446 * @since 1.2.1.8
18447 *
18448 * @return array
18449 */
18450 function get_menu_items() {
18451 return $this->_menu_items;
18452 }
18453
18454 /**
18455 * @author Vova Feldman (@svovaf)
18456 * @since 1.0.7
18457 *
18458 * @return string
18459 */
18460 function get_menu_slug() {
18461 return $this->_menu->get_slug();
18462 }
18463
18464 /**
18465 * @author Vova Feldman (@svovaf)
18466 * @since 1.0.9
18467 */
18468 function _prepare_admin_menu() {
18469 // if ( ! $this->is_on() ) {
18470 // return;
18471 // }
18472
18473 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
18474 return;
18475 }
18476
18477 /**
18478 * When running from a site admin with a network activated module and the connection
18479 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
18480 * site level settings.
18481 *
18482 * @author Vova Feldman (@svovaf)
18483 * @since 2.0.0
18484 */
18485 $should_hide_site_admin_settings = (
18486 $this->_is_network_active &&
18487 ! fs_is_network_admin() &&
18488 ! $this->is_delegated_connection() &&
18489 ! $this->is_anonymous() &&
18490 ! $this->is_registered()
18491 );
18492
18493 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
18494
18495 if ( ( false === $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
18496 $should_hide_site_admin_settings
18497 ) {
18498 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
18499 } else {
18500 $this->do_action( fs_is_network_admin() ?
18501 'before_network_admin_menu_init' :
18502 'before_admin_menu_init'
18503 );
18504
18505 $this->add_menu_action();
18506
18507 $this->add_network_menu_when_missing();
18508
18509 $this->add_submenu_items();
18510 }
18511 }
18512
18513 /**
18514 * Admin dashboard menu items modifications.
18515 *
18516 * NOTE: admin_menu action executed before admin_init.
18517 *
18518 * @author Vova Feldman (@svovaf)
18519 * @since 1.0.7
18520 *
18521 */
18522 private function add_menu_action() {
18523 if ( $this->is_activation_mode() ) {
18524 if ( $this->show_opt_in_on_setting_page() ) {
18525 $this->override_plugin_menu_with_activation();
18526 } else {
18527 /**
18528 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
18529 */
18530 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18531 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
18532 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
18533 fs_request_get_bool( 'pending_activation' )
18534 ) {
18535 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
18536 }
18537 }
18538 } else {
18539 if ( ! $this->is_registered() ) {
18540 // If not registered try to install user.
18541 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18542 $this->_install_with_new_user();
18543 }
18544 } else if (
18545 fs_request_is_action( 'sync_user' ) &&
18546 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
18547 ) {
18548 $this->_handle_account_user_sync();
18549 }
18550 }
18551 }
18552
18553 /**
18554 * @author Vova Feldman (@svovaf)
18555 * @since 1.0.1
18556 */
18557 function _redirect_on_clicked_menu_link() {
18558 $this->_logger->entrance();
18559
18560 $page = fs_request_get('page');
18561 $page = is_string($page) ? strtolower($page) : '';
18562
18563 $this->_logger->log( 'page = ' . $page );
18564
18565 foreach ( $this->_menu_items as $priority => $items ) {
18566 foreach ( $items as $item ) {
18567 if ( isset( $item['url'] ) ) {
18568 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
18569 $this->_logger->log( 'Redirecting to ' . $item['url'] );
18570
18571 fs_redirect( $item['url'] );
18572 }
18573 }
18574 }
18575 }
18576 }
18577
18578 /**
18579 * Remove plugin's all admin menu items & pages, and replace with activation page.
18580 *
18581 * @author Vova Feldman (@svovaf)
18582 * @since 1.0.1
18583 */
18584 private function override_plugin_menu_with_activation() {
18585 $this->_logger->entrance();
18586
18587 $hook = false;
18588
18589 if ( ! $this->has_settings_menu() ) {
18590 // Add the opt-in page without a menu item.
18591 $hook = FS_Admin_Menu_Manager::add_subpage(
18592 '',
18593 $this->get_plugin_name(),
18594 $this->get_plugin_name(),
18595 'manage_options',
18596 $this->_slug,
18597 array( &$this, '_connect_page_render' )
18598 );
18599 } else if ( $this->_menu->is_top_level() ) {
18600 if ( $this->_menu->is_override_exact() ) {
18601 // Make sure the current page is matching the activation page.
18602 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18603 return;
18604 }
18605 }
18606
18607 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18608
18609 if ( false === $hook ) {
18610 // Create new menu item just for the opt-in.
18611 $hook = FS_Admin_Menu_Manager::add_page(
18612 $this->get_plugin_name(),
18613 $this->get_plugin_name(),
18614 'manage_options',
18615 $this->_menu->get_slug(),
18616 array( &$this, '_connect_page_render' )
18617 );
18618 }
18619 } else {
18620 $menus = array( $this->_menu->get_parent_slug() );
18621
18622 if ( $this->_menu->is_override_exact() ) {
18623 // Make sure the current page is matching the activation page.
18624 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18625 return;
18626 }
18627 }
18628
18629 foreach ( $menus as $parent_slug ) {
18630 $hook = $this->_menu->override_submenu_action(
18631 $parent_slug,
18632 $this->_menu->get_raw_slug(),
18633 array( &$this, '_connect_page_render' )
18634 );
18635
18636 if ( false !== $hook ) {
18637 // Found plugin's submenu item.
18638 break;
18639 }
18640 }
18641 }
18642
18643 if ( $this->is_activation_page() ) {
18644 // Clean admin page from distracting content.
18645 self::_clean_admin_content_section();
18646 }
18647
18648 if ( false !== $hook ) {
18649 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18650 $this->_install_with_current_user();
18651 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18652 $this->_install_with_new_user();
18653 }
18654 }
18655 }
18656
18657 /**
18658 * If a plugin was network activated and connected but don't have a network
18659 * level settings, then add an artificial menu item for the Account and other
18660 * Freemius settings.
18661 *
18662 * @author Vova Feldman (@svovaf)
18663 * @since 2.0.0
18664 */
18665 private function add_network_menu_when_missing() {
18666 $this->_logger->entrance();
18667
18668 if ( ! $this->_is_network_active ) {
18669 // Plugin wasn't activated on the network level.
18670 return;
18671 }
18672
18673 if ( ! fs_is_network_admin() ) {
18674 // The context is not the network admin.
18675 return;
18676 }
18677
18678 if ( $this->_menu->has_network_menu() ) {
18679 // Plugin already has a network level menu.
18680 return;
18681 }
18682
18683 if ( $this->is_network_activation_mode() ) {
18684 /**
18685 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
18686 * screen is being shown.
18687 *
18688 * @author Leo Fajardo (@leorw)
18689 */
18690 return;
18691 }
18692
18693 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
18694 if ( $this->is_network_delegated_connection() ) {
18695 // Super-admin delegated the connection to the site admins.
18696 return;
18697 }
18698 }
18699
18700 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
18701
18702 if ( $this->_menu->has_menu() ||
18703 ! $this->is_addon() ||
18704 $this->is_activation_mode()
18705 ) {
18706 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
18707 $this->get_plugin_name(),
18708 $this->get_plugin_name(),
18709 'manage_options',
18710 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
18711 );
18712 }
18713 } else {
18714 $this->_menu->add_subpage_and_update(
18715 $this->_menu->get_parent_slug(),
18716 $this->get_plugin_name(),
18717 $this->get_plugin_name(),
18718 'manage_options',
18719 $this->_menu->get_slug()
18720 );
18721 }
18722 }
18723
18724 /**
18725 * @author Leo Fajardo (@leorw)
18726 * @since 1.2.1
18727 *
18728 * return string
18729 */
18730 function get_top_level_menu_capability() {
18731 global $menu;
18732
18733 $top_level_menu_slug = $this->get_top_level_menu_slug();
18734
18735 foreach ( $menu as $menu_info ) {
18736 /**
18737 * The second element in the menu info array is the capability/role that has access to the menu and the
18738 * third element is the menu slug.
18739 */
18740 if ( $menu_info[2] === $top_level_menu_slug ) {
18741 return $menu_info[1];
18742 }
18743 }
18744
18745 return 'read';
18746 }
18747
18748 /**
18749 * @author Vova Feldman (@svovaf)
18750 * @since 1.0.0
18751 *
18752 * @return string
18753 */
18754 private function get_top_level_menu_slug() {
18755 return ( $this->is_addon() ?
18756 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
18757 $this->_menu->get_top_level_menu_slug() );
18758 }
18759
18760 /**
18761 * @author Vova Feldman (@svovaf)
18762 * @since 1.2.2.7
18763 *
18764 * @return string
18765 */
18766 function get_pricing_cta_label() {
18767 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
18768
18769 if ( $this->is_in_trial_promotion() &&
18770 ! $this->is_paying_or_trial()
18771 ) {
18772 // If running a trial promotion, modify the pricing to load the trial.
18773 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
18774 } else if ( $this->is_paying() ) {
18775 $label = $this->get_text_inline( 'Pricing', 'pricing' );
18776 }
18777
18778 return $label;
18779 }
18780
18781 /**
18782 * @author Vova Feldman (@svovaf)
18783 * @since 1.2.2.7
18784 *
18785 * @return bool
18786 */
18787 function is_pricing_page_visible() {
18788 $visible = (
18789 // Has at least one paid plan.
18790 $this->has_paid_plan() &&
18791 // Didn't ask to hide the pricing page.
18792 $this->is_page_visible( 'pricing' ) &&
18793 // Don't have a valid active license or has more than one plan.
18794 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
18795 );
18796
18797 return $this->apply_filters( 'is_pricing_page_visible', $visible );
18798 }
18799
18800 /**
18801 * @author Leo Fajardo (@leorw)
18802 * @since 2.3.0
18803 *
18804 * @param bool $is_activation_mode
18805 *
18806 * @return bool
18807 */
18808 private function should_add_submenu_or_action_links( $is_activation_mode ) {
18809 if ( $this->is_addon() ) {
18810 // No submenu items or action links for add-ons.
18811 return false;
18812 }
18813
18814 if ( $this->show_opt_in_on_themes_page() ) {
18815 if ( ! fs_is_network_admin() ) {
18816 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
18817 return true;
18818 }
18819 } else if ( $is_activation_mode ) {
18820 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
18821 return false;
18822 }
18823
18824 if ( fs_is_network_admin() ) {
18825 /**
18826 * Add submenu items or action links to network level when plugin was network activated and the super
18827 * admin did NOT delegate the connection of all sites to site admins.
18828 */
18829 return (
18830 $this->_is_network_active &&
18831 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
18832 ! $this->is_network_delegated_connection() )
18833 );
18834 }
18835
18836 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
18837 }
18838
18839 /**
18840 * Add default Freemius menu items.
18841 *
18842 * @author Vova Feldman (@svovaf)
18843 * @since 1.0.0
18844 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
18845 */
18846 private function add_submenu_items() {
18847 $this->_logger->entrance();
18848
18849 $is_activation_mode = $this->is_activation_mode();
18850
18851 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
18852
18853 if ( $add_submenu_items ) {
18854 if ( $this->has_affiliate_program() ) {
18855 // Add affiliation page.
18856 $this->add_submenu_item(
18857 $this->get_text_inline( 'Affiliation', 'affiliation' ),
18858 array( &$this, '_affiliation_page_render' ),
18859 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
18860 'manage_options',
18861 'affiliation',
18862 'Freemius::_clean_admin_content_section',
18863 WP_FS__DEFAULT_PRIORITY,
18864 $this->is_submenu_item_visible( 'affiliation' )
18865 );
18866 }
18867 }
18868
18869 if ( $add_submenu_items ||
18870 ( $is_activation_mode &&
18871 $this->is_only_premium() &&
18872 $this->is_admin_page( 'account' ) &&
18873 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
18874 )
18875 ) {
18876 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
18877 $show_account = (
18878 $this->is_submenu_item_visible( 'account' ) &&
18879 /**
18880 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
18881 */
18882 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
18883 );
18884
18885 // Add user account page.
18886 $this->add_submenu_item(
18887 $this->get_text_inline( 'Account', 'account' ),
18888 array( &$this, '_account_page_render' ),
18889 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
18890 'manage_options',
18891 'account',
18892 array( &$this, '_account_page_load' ),
18893 WP_FS__DEFAULT_PRIORITY,
18894 ( $add_submenu_items && $show_account )
18895 );
18896 }
18897 }
18898
18899 if ( $add_submenu_items ) {
18900 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18901 // Add contact page.
18902 if ( $this->is_premium() ) {
18903 $this->add_submenu_item(
18904 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18905 array( &$this, '_contact_page_render' ),
18906 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
18907 'manage_options',
18908 'contact',
18909 'Freemius::_clean_admin_content_section',
18910 WP_FS__DEFAULT_PRIORITY,
18911 $this->is_submenu_item_visible( 'contact' )
18912 );
18913 } else {
18914 $this->add_submenu_link_item(
18915 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18916 FS_Contact_Form_Manager::instance()->get_standalone_link( $this ),
18917 'contact',
18918 'manage_options',
18919 WP_FS__DEFAULT_PRIORITY,
18920 $this->is_submenu_item_visible( 'contact' ),
18921 'fs_external_contact',
18922 true
18923 );
18924 }
18925 }
18926
18927 if ( $this->has_addons() ) {
18928 $this->add_submenu_item(
18929 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18930 array( &$this, '_addons_page_render' ),
18931 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18932 'manage_options',
18933 'addons',
18934 array( &$this, '_addons_page_load' ),
18935 WP_FS__LOWEST_PRIORITY - 1,
18936 $this->is_submenu_item_visible( 'addons' )
18937 );
18938 }
18939 }
18940
18941 if ( $add_submenu_items ||
18942 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
18943 ) {
18944 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18945 $show_pricing = (
18946 $this->is_submenu_item_visible( 'pricing' ) &&
18947 $this->is_pricing_page_visible()
18948 );
18949
18950 $pricing_cta_text = $this->get_pricing_cta_label();
18951 $pricing_class = 'upgrade-mode';
18952 if ( $show_pricing ) {
18953 if ( $this->is_in_trial_promotion() &&
18954 ! $this->is_paying_or_trial()
18955 ) {
18956 // If running a trial promotion, modify the pricing to load the trial.
18957 $pricing_class = 'trial-mode';
18958 } else if ( $this->is_paying() ) {
18959 $pricing_class = '';
18960 }
18961 }
18962
18963 $custom_pricing_url = $this->get_pricing_url_with_filter( null );
18964 $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' ) );
18965 $show_pricing_submenu_item = ( $add_submenu_items && $show_pricing );
18966
18967 // Add upgrade/pricing submenu item.
18968 if ( ! is_null( $custom_pricing_url ) ) {
18969 $this->add_submenu_link_item(
18970 $pricing_menu_title,
18971 $custom_pricing_url,
18972 'pricing',
18973 'manage_options',
18974 WP_FS__LOWEST_PRIORITY,
18975 $show_pricing_submenu_item,
18976 $pricing_class
18977 );
18978 } else {
18979 $this->add_submenu_item(
18980 $pricing_menu_title,
18981 array( &$this, '_pricing_page_render' ),
18982 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
18983 'manage_options',
18984 'pricing',
18985 'Freemius::_clean_admin_content_section',
18986 WP_FS__LOWEST_PRIORITY,
18987 $show_pricing_submenu_item,
18988 $pricing_class
18989 );
18990 }
18991 }
18992 }
18993
18994 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
18995 /**
18996 * Add the other menu items if there are any when not in activation mode or license activation is not
18997 * required (license activation is required for registered or anonymous users after activating the
18998 * premium version when the site is not in trial mode or there's no active valid license).
18999 *
19000 * @author Leo Fajardo (@leorw)
19001 * @since 2.2.1
19002 */
19003 if ( 0 < count( $this->_menu_items ) ) {
19004 if ( ! $this->_menu->is_top_level() ) {
19005 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
19006
19007 // Append submenu items right after the plugin's submenu item.
19008 $this->order_sub_submenu_items();
19009 } else {
19010 // Append submenu items.
19011 $this->embed_submenu_items();
19012 }
19013 }
19014 }
19015 }
19016
19017 /**
19018 * Moved the actual submenu item additions to a separated function,
19019 * in order to support sub-submenu items when the plugin's settings
19020 * only have a submenu and not top-level menu item.
19021 *
19022 * @author Vova Feldman (@svovaf)
19023 * @since 1.1.4
19024 */
19025 private function embed_submenu_items() {
19026 $item_classes = $this->_menu->is_top_level() ? 'fs-submenu-item' : 'fs-submenu-item fs-sub';
19027
19028 $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>';
19029
19030 $top_level_menu_capability = $this->get_top_level_menu_capability();
19031
19032 ksort( $this->_menu_items );
19033
19034 $is_first_submenu_item = true;
19035
19036 foreach ( $this->_menu_items as $priority => $items ) {
19037 foreach ( $items as $item ) {
19038 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
19039
19040 $menu_item = sprintf(
19041 $item_template,
19042 $this->get_unique_affix(),
19043 $item['menu_slug'],
19044 ! empty( $item['class'] ) ? $item['class'] : '',
19045 $item['menu_title'],
19046 esc_attr( isset( $item['url'] ) ? $item['url'] : '' ),
19047 esc_attr( isset( $item['new_tab'] ) ? 'true' : 'false' )
19048 );
19049
19050 $top_level_menu_slug = $this->get_top_level_menu_slug();
19051 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
19052
19053 if ( ! isset( $item['url'] ) ) {
19054 $hook = FS_Admin_Menu_Manager::add_subpage(
19055 $item['show_submenu'] ?
19056 $top_level_menu_slug :
19057 '',
19058 $item['page_title'],
19059 $menu_item,
19060 $capability,
19061 $menu_slug,
19062 $item['render_function']
19063 );
19064
19065 if ( false !== $item['before_render_function'] ) {
19066 add_action( "load-$hook", $item['before_render_function'] );
19067 }
19068 } else {
19069 FS_Admin_Menu_Manager::add_subpage(
19070 $item['show_submenu'] ?
19071 $top_level_menu_slug :
19072 '',
19073 $item['page_title'],
19074 $menu_item,
19075 $capability,
19076 $menu_slug,
19077 array( $this, '' )
19078 );
19079 }
19080
19081 if ( $item['show_submenu'] && $is_first_submenu_item ) {
19082 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
19083 /**
19084 * If the top-level menu has been dynamically created, remove the first submenu item that
19085 * WordPress automatically creates when there's no submenu item whose slug matches the
19086 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
19087 *
19088 * Awesome Plugin
19089 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
19090 *
19091 * @author Leo Fajardo (@leorw)
19092 */
19093 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
19094 }
19095
19096 $is_first_submenu_item = false;
19097 }
19098 }
19099 }
19100 }
19101
19102 /**
19103 * Re-order the submenu items so all Freemius added new submenu items
19104 * are added right after the plugin's settings submenu item.
19105 *
19106 * @author Vova Feldman (@svovaf)
19107 * @since 1.1.4
19108 */
19109 private function order_sub_submenu_items() {
19110 global $submenu;
19111
19112 $menu_slug = $this->_menu->get_top_level_menu_slug();
19113
19114 /**
19115 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
19116 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
19117 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
19118 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
19119 *
19120 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
19121 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
19122 * capability needed to access the parent menu as the capability for the submenus that we will add.
19123 */
19124 if ( empty( $submenu[ $menu_slug ] ) ) {
19125 return;
19126 }
19127
19128 $top_level_menu = &$submenu[ $menu_slug ];
19129
19130 $all_submenu_items_after = array();
19131
19132 $found_submenu_item = false;
19133
19134 foreach ( $top_level_menu as $submenu_id => $meta ) {
19135 if ( $found_submenu_item ) {
19136 // Remove all submenu items after the plugin's submenu item.
19137 $all_submenu_items_after[] = $meta;
19138 unset( $top_level_menu[ $submenu_id ] );
19139 }
19140
19141 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
19142 // Found the submenu item, put all below.
19143 $found_submenu_item = true;
19144 continue;
19145 }
19146 }
19147
19148 // Embed all plugin's new submenu items.
19149 $this->embed_submenu_items();
19150
19151 // Start with specially high number to make sure it's appended.
19152 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
19153 foreach ( $all_submenu_items_after as $meta ) {
19154 $top_level_menu[ $i ] = $meta;
19155 $i ++;
19156 }
19157
19158 // Sort submenu items.
19159 ksort( $top_level_menu );
19160 }
19161
19162 /**
19163 * Helper method to return the module's support forum URL.
19164 *
19165 * @author Vova Feldman (@svovaf)
19166 * @since 1.2.2.7
19167 *
19168 * @return string
19169 */
19170 function get_support_forum_url() {
19171 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
19172 }
19173
19174 /**
19175 * Displays the Support Forum link when enabled.
19176 *
19177 * Can be filtered like so:
19178 *
19179 * function _fs_show_support_menu( $is_visible, $menu_id ) {
19180 * if ( 'support' === $menu_id ) {
19181 * return _fs->is_registered();
19182 * }
19183 * return $is_visible;
19184 * }
19185 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
19186 *
19187 */
19188 function _add_default_submenu_items() {
19189 if ( ! $this->is_on() ) {
19190 return;
19191 }
19192
19193 if ( ! $this->is_activation_mode() &&
19194 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
19195 ( ! $this->_is_network_active && is_admin() ) )
19196 ) {
19197 $this->add_submenu_link_item(
19198 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
19199 $this->get_support_forum_url(),
19200 'wp-support-forum',
19201 null,
19202 50,
19203 $this->is_submenu_item_visible( 'support' ),
19204 '',
19205 true
19206 );
19207 }
19208 }
19209
19210 /**
19211 * @author Vova Feldman (@svovaf)
19212 * @since 1.0.1
19213 *
19214 * @param string $menu_title
19215 * @param callable $render_function
19216 * @param bool|string $page_title
19217 * @param string $capability
19218 * @param bool|string $menu_slug
19219 * @param bool|callable $before_render_function
19220 * @param int $priority
19221 * @param bool $show_submenu
19222 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
19223 */
19224 function add_submenu_item(
19225 $menu_title,
19226 $render_function,
19227 $page_title = false,
19228 $capability = 'manage_options',
19229 $menu_slug = false,
19230 $before_render_function = false,
19231 $priority = WP_FS__DEFAULT_PRIORITY,
19232 $show_submenu = true,
19233 $class = ''
19234 ) {
19235 $this->_logger->entrance( 'Title = ' . $menu_title );
19236
19237 if ( $this->is_addon() ) {
19238 $parent_fs = $this->get_parent_instance();
19239
19240 if ( is_object( $parent_fs ) ) {
19241 $parent_fs->add_submenu_item(
19242 $menu_title,
19243 $render_function,
19244 $page_title,
19245 $capability,
19246 $menu_slug,
19247 $before_render_function,
19248 $priority,
19249 $show_submenu,
19250 $class
19251 );
19252
19253 return;
19254 }
19255 }
19256
19257 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19258 $this->_menu_items[ $priority ] = array();
19259 }
19260
19261 $this->_menu_items[ $priority ][] = array(
19262 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
19263 'menu_title' => $menu_title,
19264 'capability' => $capability,
19265 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19266 'render_function' => $render_function,
19267 'before_render_function' => $before_render_function,
19268 'show_submenu' => $show_submenu,
19269 'class' => $class,
19270 );
19271 }
19272
19273 /**
19274 * @author Vova Feldman (@svovaf)
19275 * @since 1.0.1
19276 *
19277 * @param string $menu_title
19278 * @param string $url
19279 * @param bool $menu_slug
19280 * @param string $capability
19281 * @param int $priority
19282 * @param bool $show_submenu
19283 * @param string $class
19284 * @param bool $new_tab
19285 */
19286 function add_submenu_link_item(
19287 $menu_title,
19288 $url,
19289 $menu_slug = false,
19290 $capability = 'read',
19291 $priority = WP_FS__DEFAULT_PRIORITY,
19292 $show_submenu = true,
19293 $class = '',
19294 $new_tab = false
19295 ) {
19296 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
19297
19298 if ( $this->is_addon() ) {
19299 $parent_fs = $this->get_parent_instance();
19300
19301 if ( is_object( $parent_fs ) ) {
19302 $parent_fs->add_submenu_link_item(
19303 $menu_title,
19304 $url,
19305 $menu_slug,
19306 $capability,
19307 $priority,
19308 $show_submenu,
19309 $class,
19310 $new_tab
19311 );
19312
19313 return;
19314 }
19315 }
19316
19317 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19318 $this->_menu_items[ $priority ] = array();
19319 }
19320
19321 $this->_menu_items[ $priority ][] = array(
19322 'menu_title' => $menu_title,
19323 'capability' => $capability,
19324 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19325 'url' => $url,
19326 'page_title' => $menu_title,
19327 'render_function' => 'fs_dummy',
19328 'before_render_function' => '',
19329 'show_submenu' => $show_submenu,
19330 'class' => $class,
19331 'new_tab' => $new_tab,
19332 );
19333 }
19334
19335 #endregion ------------------------------------------------------------------
19336
19337 #--------------------------------------------------------------------------------
19338 #region Admin Notices
19339 #--------------------------------------------------------------------------------
19340
19341 /**
19342 * @author Vova Feldman (@svovaf)
19343 * @since 2.3.1
19344 *
19345 * @param string|string[] $ids
19346 * @param int|null $network_level_or_blog_id
19347 *
19348 * @uses FS_Admin_Notices::remove_sticky()
19349 */
19350 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
19351 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
19352 }
19353
19354 #endregion
19355
19356 #--------------------------------------------------------------------------------
19357 #region Actions / Hooks / Filters
19358 #--------------------------------------------------------------------------------
19359
19360 /**
19361 * @author Vova Feldman (@svovaf)
19362 * @since 1.1.7
19363 *
19364 * @param string $tag
19365 *
19366 * @return string
19367 */
19368 public function get_action_tag( $tag ) {
19369 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
19370 }
19371
19372 /**
19373 * @author Vova Feldman (@svovaf)
19374 * @since 1.2.1.6
19375 *
19376 * @param string $tag
19377 * @param string $slug
19378 * @param bool $is_plugin
19379 *
19380 * @return string
19381 */
19382 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
19383 $action = "fs_{$tag}";
19384
19385 if ( ! empty( $slug ) ) {
19386 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
19387 }
19388
19389 return $action;
19390 }
19391
19392 /**
19393 * Returns a string that can be used to generate a unique action name,
19394 * option name, HTML element ID, or HTML element class.
19395 *
19396 * @author Leo Fajardo (@leorw)
19397 * @since 1.2.2
19398 *
19399 * @return string
19400 */
19401 public function get_unique_affix() {
19402 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
19403 }
19404
19405 /**
19406 * Returns a string that can be used to generate a unique action name,
19407 * option name, HTML element ID, or HTML element class.
19408 *
19409 * @author Vova Feldman (@svovaf)
19410 * @since 1.2.2.5
19411 *
19412 * @param string $slug
19413 * @param bool $is_plugin
19414 *
19415 * @return string
19416 */
19417 static function get_module_unique_affix( $slug, $is_plugin = true ) {
19418 $affix = $slug;
19419
19420 if ( ! $is_plugin ) {
19421 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
19422 }
19423
19424 return $affix;
19425 }
19426
19427 /**
19428 * @author Vova Feldman (@svovaf)
19429 * @since 1.2.1
19430 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19431 * based on the slug for backward compatibility.
19432 *
19433 * @param string $tag
19434 *
19435 * @return string
19436 */
19437 function get_ajax_action( $tag ) {
19438 return self::get_ajax_action_static( $tag, $this->_module_id );
19439 }
19440
19441 /**
19442 * @author Vova Feldman (@svovaf)
19443 * @since 1.2.1.7
19444 *
19445 * @param string $tag
19446 *
19447 * @return string
19448 */
19449 function get_ajax_security( $tag ) {
19450 return wp_create_nonce( $this->get_ajax_action( $tag ) );
19451 }
19452
19453 /**
19454 * @author Vova Feldman (@svovaf)
19455 * @since 1.2.1.7
19456 *
19457 * @param string $tag
19458 */
19459 function check_ajax_referer( $tag ) {
19460 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
19461 }
19462
19463 /**
19464 * @author Vova Feldman (@svovaf)
19465 * @since 1.2.1.6
19466 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19467 * based on the slug for backward compatibility.
19468 *
19469 * @param string $tag
19470 * @param number|null $module_id
19471 *
19472 * @return string
19473 */
19474 static function get_ajax_action_static( $tag, $module_id = null ) {
19475 $action = "fs_{$tag}";
19476
19477 if ( ! empty( $module_id ) ) {
19478 $action .= "_{$module_id}";
19479 }
19480
19481 return $action;
19482 }
19483
19484 /**
19485 * Do action, specific for the current context plugin.
19486 *
19487 * @author Vova Feldman (@svovaf)
19488 * @since 1.0.1
19489 *
19490 * @param string $tag The name of the action to be executed.
19491 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
19492 * functions hooked to the action. Default empty.
19493 *
19494 * @uses do_action()
19495 */
19496 function do_action( $tag, $arg = '' ) {
19497 $args = func_get_args();
19498
19499 $this->_logger->entrance( $tag );
19500
19501 call_user_func_array( 'do_action', array_merge(
19502 array( $this->get_action_tag( $tag ) ),
19503 array_slice( $args, 1 ) )
19504 );
19505 }
19506
19507 /**
19508 * Add action, specific for the current context plugin.
19509 *
19510 * @author Vova Feldman (@svovaf)
19511 * @since 1.0.1
19512 *
19513 * @param string $tag
19514 * @param callable $function_to_add
19515 * @param int $priority
19516 * @param int $accepted_args
19517 *
19518 * @uses add_action()
19519 */
19520 function add_action(
19521 $tag,
19522 $function_to_add,
19523 $priority = WP_FS__DEFAULT_PRIORITY,
19524 $accepted_args = 1
19525 ) {
19526 $this->_logger->entrance( $tag );
19527
19528 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19529 }
19530
19531 /**
19532 * Add AJAX action, specific for the current context plugin.
19533 *
19534 * @author Vova Feldman (@svovaf)
19535 * @since 1.2.1
19536 *
19537 * @param string $tag
19538 * @param callable $function_to_add
19539 * @param int $priority
19540 *
19541 * @uses add_action()
19542 *
19543 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19544 */
19545 function add_ajax_action(
19546 $tag,
19547 $function_to_add,
19548 $priority = WP_FS__DEFAULT_PRIORITY
19549 ) {
19550 $this->_logger->entrance( $tag );
19551
19552 return self::add_ajax_action_static(
19553 $tag,
19554 $function_to_add,
19555 $priority,
19556 $this->_module_id
19557 );
19558 }
19559
19560 /**
19561 * Add AJAX action.
19562 *
19563 * @author Vova Feldman (@svovaf)
19564 * @since 1.2.1.6
19565 *
19566 * @param string $tag
19567 * @param callable $function_to_add
19568 * @param int $priority
19569 * @param number|null $module_id
19570 *
19571 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19572 * @uses add_action()
19573 *
19574 */
19575 static function add_ajax_action_static(
19576 $tag,
19577 $function_to_add,
19578 $priority = WP_FS__DEFAULT_PRIORITY,
19579 $module_id = null
19580 ) {
19581 self::$_static_logger->entrance( $tag );
19582
19583 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
19584 return false;
19585 }
19586
19587 add_action(
19588 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
19589 $function_to_add,
19590 $priority,
19591 0
19592 );
19593
19594 self::$_static_logger->info( "$tag AJAX callback action added." );
19595
19596 return true;
19597 }
19598
19599 /**
19600 * Send a JSON response back to an Ajax request.
19601 *
19602 * @author Vova Feldman (@svovaf)
19603 * @since 1.2.1.5
19604 *
19605 * @param mixed $response
19606 */
19607 static function shoot_ajax_response( $response ) {
19608 wp_send_json( $response );
19609 }
19610
19611 /**
19612 * Send a JSON response back to an Ajax request, indicating success.
19613 *
19614 * @author Vova Feldman (@svovaf)
19615 * @since 1.2.1.5
19616 *
19617 * @param mixed $data Data to encode as JSON, then print and exit.
19618 */
19619 static function shoot_ajax_success( $data = null ) {
19620 wp_send_json_success( $data );
19621 }
19622
19623 /**
19624 * Send a JSON response back to an Ajax request, indicating failure.
19625 *
19626 * @author Vova Feldman (@svovaf)
19627 * @since 1.2.1.5
19628 *
19629 * @param mixed $error Optional error message.
19630 */
19631 static function shoot_ajax_failure( $error = '' ) {
19632 $result = array( 'success' => false );
19633 if ( ! empty( $error ) ) {
19634 $result['error'] = $error;
19635 }
19636
19637 wp_send_json( $result );
19638 }
19639
19640 /**
19641 * Returns an AJAX URL with a special extra param to indicate whether the request was triggered from the network admin or blog admin.
19642 *
19643 * @author Vova Feldman (@svovaf)
19644 * @since 2.5.1
19645 *
19646 * @param string $wrap_with By default, returns the AJAX URL wrapped with single quotes.
19647 *
19648 * @return string
19649 */
19650 static function ajax_url( $wrap_with = "'") {
19651 if ( fs_is_network_admin() ) {
19652 $param_name = '_fs_network_admin';
19653 } else {
19654 $param_name = '_fs_blog_admin';
19655 }
19656
19657 $url = admin_url( 'admin-ajax.php', 'relative' );
19658 $url .= ( false === strpos( $url, '?' ) ) ? '?' : '&';
19659 $url .= "{$param_name}=true";
19660
19661 return "{$wrap_with}{$url}{$wrap_with}";
19662 }
19663
19664 /**
19665 * Apply filter, specific for the current context plugin.
19666 *
19667 * @author Vova Feldman (@svovaf)
19668 * @since 1.0.9
19669 *
19670 * @param string $tag The name of the filter hook.
19671 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
19672 *
19673 * @return mixed The filtered value after all hooked functions are applied to it.
19674 *
19675 * @uses apply_filters()
19676 */
19677 function apply_filters( $tag, $value ) {
19678 $args = func_get_args();
19679
19680 $this->_logger->entrance( $tag );
19681
19682 array_unshift( $args, $this->get_unique_affix() );
19683
19684 return call_user_func_array( 'fs_apply_filter', $args );
19685 }
19686
19687 /**
19688 * Add filter, specific for the current context plugin.
19689 *
19690 * @author Vova Feldman (@svovaf)
19691 * @since 1.0.9
19692 *
19693 * @param string $tag
19694 * @param callable $function_to_add
19695 * @param int $priority
19696 * @param int $accepted_args
19697 *
19698 * @uses add_filter()
19699 */
19700 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
19701 $this->_logger->entrance( $tag );
19702
19703 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19704 }
19705
19706 /**
19707 * Check if has filter.
19708 *
19709 * @author Vova Feldman (@svovaf)
19710 * @since 1.1.4
19711 *
19712 * @param string $tag
19713 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
19714 *
19715 * @return false|int
19716 *
19717 * @uses has_filter()
19718 */
19719 function has_filter( $tag, $function_to_check = false ) {
19720 $this->_logger->entrance( $tag );
19721
19722 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
19723 }
19724
19725 #endregion
19726
19727 /**
19728 * Override default i18n text phrases.
19729 *
19730 * @author Vova Feldman (@svovaf)
19731 * @since 1.1.6
19732 *
19733 * @param string[] string $key_value
19734 *
19735 * @uses fs_override_i18n()
19736 */
19737 function override_i18n( $key_value ) {
19738 fs_override_i18n( $key_value, $this->_slug );
19739 }
19740
19741 /* Account Page
19742 ------------------------------------------------------------------------------------------------------------------*/
19743 /**
19744 * Update site information.
19745 *
19746 * @author Vova Feldman (@svovaf)
19747 * @since 1.0.1
19748 *
19749 * @param bool $store Flush to Database if true.
19750 * @param null|int $network_level_or_blog_id Since 2.0.0
19751 * @param \FS_Site $site Since 2.0.0
19752 */
19753 private function _store_site( $store = true, $network_level_or_blog_id = null, $site = null, $is_backup = false ) {
19754 $this->_logger->entrance();
19755
19756 if ( is_null( $site ) ) {
19757 $site = $this->_site;
19758 }
19759
19760 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
19761 $this->_logger->error( "Empty install ID, can't store site." );
19762
19763 return;
19764 }
19765
19766 $site_clone = clone $site;
19767
19768 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id, $is_backup );
19769
19770 if (
19771 ! $is_backup &&
19772 is_object( $this->_user ) && $this->_user->id != $site->user_id
19773 ) {
19774 $this->sync_user_by_current_install( $site->user_id );
19775
19776 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
19777
19778 if ( empty( $prev_stored_user_id ) &&
19779 is_object($this->_user) && $this->_user->id != $site->user_id
19780 ) {
19781 /**
19782 * Store the current user ID as the previous user ID so that the previous user can be used
19783 * as the install's owner while the new owner's details are not yet available.
19784 *
19785 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
19786 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
19787 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
19788 * into `replica`.
19789 *
19790 * @author Leo Fajardo (@leorw)
19791 */
19792 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
19793 }
19794 }
19795
19796 $sites[ $this->_slug ] = $site_clone;
19797
19798 $this->set_account_option(
19799 ( $is_backup ? 'prev_' : '' ) . 'sites',
19800 $sites,
19801 $store,
19802 $network_level_or_blog_id
19803 );
19804 }
19805
19806 /**
19807 * 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).
19808 *
19809 * @author Leo Fajardo (@leorw)
19810 * @since 2.5.0
19811 */
19812 private function back_up_site() {
19813 $this->_logger->entrance();
19814
19815 $site_clone = clone $this->_site;
19816
19817 $this->_store_site( true, null, $site_clone, true );
19818 }
19819
19820 /**
19821 * Update plugin's plans information.
19822 *
19823 * @author Vova Feldman (@svovaf)
19824 * @since 1.0.2
19825 *
19826 * @param bool $store Flush to Database if true.
19827 */
19828 private function _store_plans( $store = true ) {
19829 $this->_logger->entrance();
19830
19831 $plans = self::get_all_plans( $this->_module_type );
19832
19833 // Copy plans.
19834 $encrypted_plans = array();
19835 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
19836 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
19837 }
19838
19839 $plans[ $this->_slug ] = $encrypted_plans;
19840
19841 $this->set_account_option( 'plans', $plans, $store );
19842 }
19843
19844 /**
19845 * Update user's plugin licenses.
19846 *
19847 * @author Vova Feldman (@svovaf)
19848 * @since 1.0.5
19849 *
19850 * @param bool $store
19851 * @param number|bool $module_id
19852 * @param FS_Plugin_License[] $licenses
19853 */
19854 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
19855 $this->_logger->entrance();
19856
19857 $all_licenses = self::get_all_licenses();
19858
19859 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
19860 $module_id = $this->_module_id;
19861
19862 $user_licenses = is_array( $this->_licenses ) ?
19863 $this->_licenses :
19864 array();
19865
19866 if ( empty( $user_licenses ) ) {
19867 // If the context user doesn't have any license, don't update the licenses collection.
19868 return;
19869 }
19870
19871 $new_user_licenses_map = array();
19872 foreach ( $user_licenses as $user_license ) {
19873 $new_user_licenses_map[ $user_license->id ] = $user_license;
19874 }
19875
19876 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
19877
19878 // Update user licenses.
19879 $licenses_to_update_count = count( $new_user_licenses_map );
19880 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
19881 if ( 0 === $licenses_to_update_count ) {
19882 break;
19883 }
19884
19885 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
19886 // Update license.
19887 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
19888 unset( $new_user_licenses_map[ $license->id ] );
19889
19890 $licenses_to_update_count --;
19891 }
19892 }
19893
19894 if ( ! empty( $new_user_licenses_map ) ) {
19895 // Add new licenses.
19896 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
19897 }
19898
19899 $licenses = $all_licenses[ $module_id ];
19900 }
19901
19902 if ( ! isset( $all_licenses[ $module_id ] ) ) {
19903 $all_licenses[ $module_id ] = array();
19904 }
19905
19906 $all_licenses[ $module_id ] = $licenses;
19907
19908 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
19909 }
19910
19911 /**
19912 * Update user information.
19913 *
19914 * @author Vova Feldman (@svovaf)
19915 * @since 1.0.1
19916 *
19917 * @param bool $store Flush to Database if true.
19918 */
19919 private function _store_user( $store = true ) {
19920 $this->_logger->entrance();
19921
19922 if ( empty( $this->_user->id ) ) {
19923 $this->_logger->error( "Empty user ID, can't store user." );
19924
19925 return;
19926 }
19927
19928 $users = self::get_all_users();
19929 $users[ $this->_user->id ] = $this->_user;
19930 self::$_accounts->set_option( 'users', $users, $store );
19931 }
19932
19933 /**
19934 * Update new updates information.
19935 *
19936 * @author Vova Feldman (@svovaf)
19937 * @since 1.0.4
19938 *
19939 * @param FS_Plugin_Tag|null $update
19940 * @param bool $store Flush to Database if true.
19941 * @param bool|number $plugin_id
19942 */
19943 private function _store_update( $update, $store = true, $plugin_id = false ) {
19944 $this->_logger->entrance();
19945
19946 if ( $update instanceof FS_Plugin_Tag ) {
19947 $update->updated = time();
19948 }
19949
19950 if ( ! is_numeric( $plugin_id ) ) {
19951 $plugin_id = $this->_plugin->id;
19952 }
19953
19954 $updates = self::get_all_updates();
19955 $updates[ $plugin_id ] = $update;
19956 self::$_accounts->set_option( 'updates', $updates, $store );
19957 }
19958
19959 /**
19960 * Update new updates information.
19961 *
19962 * @author Vova Feldman (@svovaf)
19963 * @since 1.0.6
19964 *
19965 * @param FS_Plugin[] $plugin_addons
19966 * @param bool $store Flush to Database if true.
19967 */
19968 private function _store_addons( $plugin_addons, $store = true ) {
19969 $this->_logger->entrance();
19970
19971 $addons = self::get_all_addons();
19972 $addons[ $this->_plugin->id ] = $plugin_addons;
19973 self::$_accounts->set_option( 'addons', $addons, $store );
19974 }
19975
19976 /**
19977 * Delete plugin's associated add-ons.
19978 *
19979 * @author Vova Feldman (@svovaf)
19980 * @since 1.0.8
19981 *
19982 * @param bool $store
19983 *
19984 * @return bool
19985 */
19986 private function _delete_account_addons( $store = true ) {
19987 $all_addons = self::get_all_account_addons();
19988
19989 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
19990 return false;
19991 }
19992
19993 unset( $all_addons[ $this->_plugin->id ] );
19994
19995 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
19996
19997 return true;
19998 }
19999
20000 /**
20001 * Update account add-ons list.
20002 *
20003 * @author Vova Feldman (@svovaf)
20004 * @since 1.0.6
20005 *
20006 * @param FS_Plugin[] $addons
20007 * @param bool $store Flush to Database if true.
20008 */
20009 private function _store_account_addons( $addons, $store = true ) {
20010 $this->_logger->entrance();
20011
20012 $all_addons = self::get_all_account_addons();
20013 $all_addons[ $this->_plugin->id ] = $addons;
20014 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20015 }
20016
20017 /**
20018 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
20019 * the valid user licenses will be fetched again and the account add-ons may be updated.
20020 *
20021 * @author Leo Fajardo (@leorw)
20022 * @since 2.2.4
20023 */
20024 private function purge_valid_user_licenses_cache() {
20025 if ( ! $this->is_registered() ) {
20026 return;
20027 }
20028
20029 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
20030 }
20031
20032 /**
20033 * @author Leo Fajardo (@leorw)
20034 * @since 2.3.0
20035 *
20036 * @param array $all_licenses
20037 * @param number|null $site_license_id
20038 * @param bool $include_parent_licenses
20039 *
20040 * @return array
20041 */
20042 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
20043 $foreign_licenses = array(
20044 'ids' => array(),
20045 'license_keys' => array()
20046 );
20047
20048 $parent_license_ids_map = array();
20049
20050 foreach ( $all_licenses as $license ) {
20051 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
20052 continue;
20053 }
20054
20055 $foreign_licenses['ids'][] = $license->id;
20056 $foreign_licenses['license_keys'][] = $license->secret_key;
20057
20058 if (
20059 $include_parent_licenses &&
20060 is_object( $this->_license ) &&
20061 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
20062 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
20063 ) {
20064 /**
20065 * Include the parent license's info only if it has not been included before since child licenses
20066 * can have the same parent license.
20067 */
20068 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
20069 $foreign_licenses['license_keys'][] = $license->secret_key;
20070
20071 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
20072 }
20073 }
20074
20075 if ( empty( $foreign_licenses['ids'] ) ) {
20076 $foreign_licenses = array();
20077 }
20078
20079 return $foreign_licenses;
20080 }
20081
20082 /**
20083 * @author Leo Fajardo (@leorw)
20084 * @since 2.3.0
20085 *
20086 * @return string
20087 */
20088 private function get_valid_user_licenses_endpoint() {
20089 $user_licenses_endpoint = '/licenses.json?type=active' .
20090 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
20091
20092 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
20093
20094 if ( ! empty ( $foreign_licenses ) ) {
20095 $foreign_licenses = array(
20096 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20097 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20098 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20099 );
20100
20101 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20102 }
20103
20104 return $user_licenses_endpoint;
20105 }
20106
20107 /**
20108 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
20109 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
20110 * method filters out non–add-on product IDs and stores the add-on IDs.
20111 *
20112 * @author Leo Fajardo (@leorw)
20113 * @since 2.2.4
20114 *
20115 * @return stdClass[] array
20116 */
20117 private function fetch_valid_user_licenses() {
20118 $this->_logger->entrance();
20119
20120 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
20121
20122 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
20123 ! is_array( $result->licenses )
20124 ) {
20125 return array();
20126 }
20127
20128 return $result->licenses;
20129 }
20130
20131 /**
20132 * @author Leo Fajardo (@leorw)
20133 * @since 2.2.4
20134 *
20135 * @return number[] Account add-on IDs.
20136 */
20137 function get_updated_account_addons() {
20138 $addons = $this->get_addons();
20139 if ( empty( $addons ) ) {
20140 return array();
20141 }
20142
20143 $account_addons = $this->get_account_addons();
20144 if ( ! is_array( $account_addons ) ) {
20145 $account_addons = array();
20146 }
20147
20148 $user_licenses = $this->is_registered() ?
20149 $this->fetch_valid_user_licenses() :
20150 array();
20151
20152 if ( empty( $user_licenses ) ) {
20153 return $account_addons;
20154 }
20155
20156 $addon_ids = array();
20157 foreach ( $addons as $addon ) {
20158 $addon_ids[] = $addon->id;
20159 }
20160
20161 $license_product_ids = array();
20162
20163 foreach ( $user_licenses as $license ) {
20164 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
20165 $license_product_ids = array_merge( $license_product_ids, $license->products );
20166 } else {
20167 $license_product_ids[] = $license->plugin_id;
20168 }
20169 }
20170
20171 // Filter out non–add-on IDs.
20172 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
20173 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
20174 $this->_store_account_addons( array_unique( $new_account_addons ) );
20175 }
20176
20177 return $new_account_addons;
20178 }
20179
20180 /**
20181 * Store account params in the Database.
20182 *
20183 * @author Vova Feldman (@svovaf)
20184 * @since 1.0.1
20185 *
20186 * @param null|int $blog_id Since 2.0.0
20187 */
20188 private function _store_account( $blog_id = null ) {
20189 $this->_logger->entrance();
20190
20191 $this->_store_site( false, $blog_id );
20192 $this->_store_user( false );
20193 $this->_store_plans( false );
20194 $this->_store_licenses( false );
20195
20196 self::$_accounts->store( $blog_id );
20197 }
20198
20199 /**
20200 * Sync user's information.
20201 *
20202 * @author Vova Feldman (@svovaf)
20203 * @since 1.0.3
20204 * @uses FS_Api
20205 */
20206 private function _handle_account_user_sync() {
20207 $this->_logger->entrance();
20208
20209 $api = $this->get_api_user_scope();
20210
20211 // Get user's information.
20212 $user = $api->get( '/', true );
20213
20214 if ( isset( $user->id ) ) {
20215 $this->_user->first = $user->first;
20216 $this->_user->last = $user->last;
20217 $this->_user->email = $user->email;
20218
20219 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
20220
20221 if ( $user->is_verified &&
20222 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
20223 ) {
20224 $this->_user->is_verified = true;
20225
20226 $this->do_action( 'account_email_verified', $user->email );
20227
20228 $this->_admin_notices->add(
20229 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
20230 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
20231 'success',
20232 // Make admin sticky if account menu item is invisible,
20233 // since the page will be auto redirected to the plugin's
20234 // main settings page, and the non-sticky message
20235 // will disappear.
20236 ! $is_menu_item_account_visible,
20237 'email_verified'
20238 );
20239 }
20240
20241 // Flush user details to DB.
20242 $this->_store_user();
20243
20244 $this->do_action( 'after_account_user_sync', $user );
20245
20246 /**
20247 * If account menu item is hidden, redirect to plugin's main settings page.
20248 *
20249 * @author Vova Feldman (@svovaf)
20250 * @since 1.1.6
20251 *
20252 * @link https://github.com/Freemius/wordpress-sdk/issues/6
20253 */
20254 if ( ! $is_menu_item_account_visible ) {
20255 fs_redirect( $this->_get_admin_page_url() );
20256 }
20257 }
20258 }
20259
20260 /**
20261 * @author Vova Feldman (@svovaf)
20262 * @since 1.0.9
20263 * @uses FS_Api
20264 *
20265 * @param number|bool $license_id
20266 *
20267 * @return FS_Subscription|object|bool
20268 */
20269 private function _fetch_site_license_subscription( $license_id = false ) {
20270 $this->_logger->entrance();
20271 $api = $this->get_api_site_scope();
20272
20273 if ( ! is_numeric( $license_id ) ) {
20274 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
20275 $this->_license->parent_license_id :
20276 $this->_license->id;
20277 }
20278
20279 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
20280
20281 return ! isset( $result->error ) ?
20282 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
20283 new FS_Subscription( $result->subscriptions[0] ) :
20284 false
20285 ) :
20286 $result;
20287 }
20288
20289 /**
20290 * @author Vova Feldman (@svovaf)
20291 * @since 1.0.4
20292 * @uses FS_Api
20293 *
20294 * @param number|bool $plan_id
20295 *
20296 * @return FS_Plugin_Plan|object
20297 */
20298 private function _fetch_site_plan( $plan_id = false ) {
20299 $this->_logger->entrance();
20300 $api = $this->get_api_site_scope();
20301
20302 if ( ! is_numeric( $plan_id ) ) {
20303 $plan_id = $this->_site->plan_id;
20304 }
20305
20306 $plan = $api->get( "/plans/{$plan_id}.json", true );
20307
20308 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
20309 }
20310
20311 /**
20312 * @author Vova Feldman (@svovaf)
20313 * @since 1.0.5
20314 * @uses FS_Api
20315 *
20316 * @return FS_Plugin_Plan[]|object
20317 */
20318 private function _fetch_plugin_plans() {
20319 $this->_logger->entrance();
20320 $api = $this->get_current_or_network_user_api_scope();
20321
20322 /**
20323 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
20324 */
20325 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
20326
20327 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
20328 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
20329 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
20330 }
20331
20332 $result = $result->plans;
20333 }
20334
20335 return $result;
20336 }
20337
20338 /**
20339 * @author Vova Feldman (@svovaf)
20340 * @since 2.0.0
20341 *
20342 * @param number $plan_id
20343 *
20344 * @return \FS_Plugin_Plan|object
20345 */
20346 private function fetch_plan_by_id( $plan_id ) {
20347 $this->_logger->entrance();
20348 $api = $this->get_current_or_network_user_api_scope();
20349
20350 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
20351
20352 return $this->is_api_result_entity( $result ) ?
20353 new FS_Plugin_Plan( $result ) :
20354 $result;
20355 }
20356
20357 /**
20358 * @author Vova Feldman (@svovaf)
20359 * @since 1.0.5
20360 * @uses FS_Api
20361 *
20362 * @param number|bool $plugin_id
20363 * @param number|bool $site_license_id
20364 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
20365 * @param number|null $blog_id
20366 *
20367 * @return FS_Plugin_License[]|object
20368 */
20369 private function _fetch_licenses(
20370 $plugin_id = false,
20371 $site_license_id = false,
20372 $foreign_licenses = array(),
20373 $blog_id = null
20374 ) {
20375 $this->_logger->entrance();
20376
20377 $api = $this->get_api_user_scope();
20378
20379 if ( ! is_numeric( $plugin_id ) ) {
20380 $plugin_id = $this->_plugin->id;
20381 }
20382
20383 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
20384 if ( ! empty ( $foreign_licenses ) ) {
20385 $foreign_licenses = array(
20386 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20387 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20388 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20389 );
20390
20391 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20392 }
20393
20394 $result = $api->get( $user_licenses_endpoint, true );
20395
20396 $is_site_license_synced = false;
20397
20398 $api_errors = array();
20399
20400 if ( $this->is_api_result_object( $result, 'licenses' ) &&
20401 is_array( $result->licenses )
20402 ) {
20403 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
20404 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
20405
20406 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
20407 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
20408 }
20409 }
20410
20411 $result = $result->licenses;
20412 } else {
20413 $api_errors[] = $result;
20414 $result = array();
20415 }
20416
20417 if ( ! $is_site_license_synced ) {
20418 if ( ! is_null( $blog_id ) ) {
20419 /**
20420 * If blog ID is not null, the request is for syncing of the license of a single site via the
20421 * network-level "Account" page.
20422 *
20423 * @author Leo Fajardo (@leorw)
20424 */
20425 $this->switch_to_blog( $blog_id );
20426 }
20427
20428 $api = $this->get_api_site_scope();
20429
20430 if ( is_numeric( $site_license_id ) ) {
20431 // Try to retrieve a foreign license that is linked to the install.
20432 $api_result = $api->call( '/licenses.json?is_enriched=true' );
20433
20434 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
20435 is_array( $api_result->licenses )
20436 ) {
20437 $licenses = $api_result->licenses;
20438
20439 if ( ! empty( $licenses ) ) {
20440 $result[] = new FS_Plugin_License( $licenses[0] );
20441 }
20442 } else {
20443 $api_errors[] = $api_result;
20444 }
20445 } else if (
20446 is_object( $this->_license ) &&
20447 /**
20448 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
20449 * the FS instance that does the syncing is the parent FS instance.
20450 *
20451 * @author Leo Fajardo (@leorw)
20452 * @since 2.3.0
20453 */
20454 $this->_license->plugin_id == $plugin_id
20455 ) {
20456 $is_license_in_result = false;
20457 if ( ! empty( $result ) ) {
20458 foreach ( $result as $license ) {
20459 if ( $license->id == $this->_license->id ) {
20460 $is_license_in_result = true;
20461 break;
20462 }
20463 }
20464 }
20465
20466 if ( ! $is_license_in_result ) {
20467 // Fetch foreign license by ID and license key.
20468 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
20469 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
20470
20471 if ( $this->is_api_result_entity( $license ) ) {
20472 $result[] = new FS_Plugin_License( $license );
20473 } else {
20474 $api_errors[] = $license;
20475 }
20476 }
20477 }
20478
20479 if ( ! is_null( $blog_id ) ) {
20480 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20481 }
20482 }
20483
20484 if ( is_array( $result ) && 0 < count( $result ) ) {
20485 // If found at least one license, return license collection even if there are errors.
20486 return $result;
20487 }
20488
20489 if ( ! empty( $api_errors ) ) {
20490 // If found any errors and no licenses, return first error.
20491 return $api_errors[0];
20492 }
20493
20494 // Fallback to empty licenses list.
20495 return $result;
20496 }
20497
20498 /**
20499 * @author Vova Feldman (@svovaf)
20500 * @since 2.0.0
20501 *
20502 * @param number $license_id
20503 * @param string $license_key
20504 *
20505 * @return \FS_Plugin_License|object
20506 */
20507 private function fetch_license_by_key( $license_id, $license_key ) {
20508 $this->_logger->entrance();
20509
20510 $api = $this->get_current_or_network_user_api_scope();
20511
20512 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
20513
20514 return $this->is_api_result_entity( $result ) ?
20515 new FS_Plugin_License( $result ) :
20516 $result;
20517 }
20518
20519 /**
20520 * @author Vova Feldman (@svovaf)
20521 * @since 1.2.0
20522 * @uses FS_Api
20523 *
20524 * @param number|bool $plugin_id
20525 * @param bool $flush
20526 *
20527 * @return FS_Payment[]|object
20528 */
20529 function _fetch_payments( $plugin_id = false, $flush = false ) {
20530 $this->_logger->entrance();
20531
20532 $api = $this->get_api_user_scope();
20533
20534 if ( ! is_numeric( $plugin_id ) ) {
20535 $plugin_id = $this->_plugin->id;
20536 }
20537
20538 $include_bundles = (
20539 is_object( $this->_plugin ) &&
20540 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
20541 );
20542
20543 $result = $api->get(
20544 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
20545 $flush
20546 );
20547
20548 if ( ! isset( $result->error ) ) {
20549 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
20550 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
20551 }
20552 $result = $result->payments;
20553 }
20554
20555 return $result;
20556 }
20557
20558 /**
20559 * @author Vova Feldman (@svovaf)
20560 * @since 1.2.1.5
20561 * @uses FS_Api
20562 *
20563 * @param bool $flush
20564 *
20565 * @return \FS_Billing|mixed
20566 */
20567 function _fetch_billing( $flush = false ) {
20568 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
20569
20570 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
20571
20572 if ( $this->is_api_result_entity( $billing ) ) {
20573 $billing = new FS_Billing( $billing );
20574 }
20575
20576 return $billing;
20577 }
20578
20579 /**
20580 * @author Vova Feldman (@svovaf)
20581 * @since 1.0.5
20582 *
20583 * @param FS_Plugin_License[] $licenses
20584 * @param number $module_id
20585 */
20586 private function _update_licenses( $licenses, $module_id ) {
20587 $this->_logger->entrance();
20588
20589 if ( is_array( $licenses ) ) {
20590 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
20591 $licenses[ $i ]->updated = time();
20592 }
20593 }
20594
20595 $this->_store_licenses( true, $module_id, $licenses );
20596 }
20597
20598 /**
20599 * @author Vova Feldman (@svovaf)
20600 * @since 1.0.4
20601 *
20602 * @param bool|number $plugin_id
20603 * @param bool $flush Since 1.1.7.3
20604 * @param int $expiration Since 1.2.2.7
20605 * @param bool|string $newer_than Since 2.2.1
20606 * @param bool $fetch_upgrade_notice Since 2.12.1
20607 *
20608 * @return object|false New plugin tag info if exist.
20609 */
20610 private function _fetch_newer_version(
20611 $plugin_id = false,
20612 $flush = true,
20613 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
20614 $newer_than = false,
20615 $fetch_upgrade_notice = true
20616 ) {
20617 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than, false, $fetch_upgrade_notice );
20618
20619 if ( ! is_object( $latest_tag ) ) {
20620 return false;
20621 }
20622
20623 $plugin_version = $this->get_plugin_version();
20624
20625 // Check if version is actually newer.
20626 $has_new_version =
20627 // If it's an non-installed add-on then always return latest.
20628 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
20629 // Compare versions.
20630 version_compare( $plugin_version, $latest_tag->version, '<' );
20631
20632 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
20633
20634 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
20635
20636 $this->_storage->beta_data = array(
20637 'is_beta' => $is_latest_version_beta,
20638 'version' => $latest_tag->version
20639 );
20640
20641 return $has_new_version ? $latest_tag : false;
20642 }
20643
20644 /**
20645 * @author Vova Feldman (@svovaf)
20646 * @since 1.0.5
20647 *
20648 * @param bool|number $plugin_id
20649 * @param bool $flush Since 1.1.7.3
20650 *
20651 * @return bool|FS_Plugin_Tag
20652 */
20653 function get_update( $plugin_id = false, $flush = true ) {
20654 $this->_logger->entrance();
20655
20656 if ( ! is_numeric( $plugin_id ) ) {
20657 $plugin_id = $this->_plugin->id;
20658 }
20659
20660 $this->check_updates( true, $plugin_id, $flush );
20661
20662 $updates = $this->get_all_updates();
20663
20664 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
20665 }
20666
20667 /**
20668 * Check if site assigned with active license.
20669 *
20670 * @author Vova Feldman (@svovaf)
20671 * @since 1.0.6
20672 *
20673 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
20674 */
20675 function has_active_license() {
20676 return (
20677 is_object( $this->_license ) &&
20678 is_numeric( $this->_license->id ) &&
20679 ! $this->_license->is_expired()
20680 );
20681 }
20682
20683 /**
20684 * Check if site assigned with active & valid (not expired) license.
20685 *
20686 * @author Vova Feldman (@svovaf)
20687 * @since 1.2.1
20688 *
20689 * @param bool $check_expiration
20690 */
20691 function has_active_valid_license( $check_expiration = true ) {
20692 return self::is_active_valid_license( $this->_license, $check_expiration );
20693 }
20694
20695 /**
20696 * @author Leo Fajardo (@leorw)
20697 * @since 2.3.1
20698 */
20699 function is_data_debug_mode() {
20700 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
20701 return false;
20702 }
20703
20704 $fs = $this->is_addon() ?
20705 $this->get_parent_instance() :
20706 $this;
20707
20708 if ( $fs->is_network_active() && fs_is_network_admin() ) {
20709 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
20710 } else {
20711 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
20712 }
20713
20714 return ( 'true' === $is_developer_license_debug_mode );
20715 }
20716
20717 /**
20718 * @author Leo Fajardo (@leorw)
20719 * @since 2.3.1
20720 */
20721 function _set_data_debug_mode() {
20722 if ( ! $this->is_whitelabeled( true ) ) {
20723 return;
20724 }
20725
20726 $license_or_user_key = fs_request_get_raw( 'license_or_user_key' );
20727
20728 $transient_value = ( ! empty( $license_or_user_key ) ) ?
20729 'true' :
20730 'false';
20731
20732 if ( 'true' === $transient_value ) {
20733 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
20734 'last_license_key' :
20735 'last_license_user_key'
20736 );
20737
20738 if ( md5( $license_or_user_key ) !== $stored_key ) {
20739 $this->shoot_ajax_failure( sprintf(
20740 '%s... %s',
20741 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
20742 $this->get_text_inline(
20743 'seems like the key you entered doesn\'t match our records.',
20744 'developer-or-license-not-found'
20745 )
20746 ) );
20747 }
20748 }
20749
20750 if ( $this->is_network_active() && fs_is_network_admin() ) {
20751 set_site_transient(
20752 "fs_{$this->get_id()}_data_debug_mode",
20753 $transient_value,
20754 WP_FS__TIME_24_HOURS_IN_SEC / 24
20755 );
20756 } else {
20757 set_transient(
20758 "fs_{$this->get_id()}_data_debug_mode",
20759 $transient_value,
20760 WP_FS__TIME_24_HOURS_IN_SEC / 24
20761 );
20762 }
20763
20764 if ( 'true' === $transient_value ) {
20765 $this->_admin_notices->add_sticky(
20766 $this->get_text_inline(
20767 '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.',
20768 'data_debug_mode_enabled'
20769 ),
20770 'data_debug_mode_enabled'
20771 );
20772 }
20773
20774 $this->shoot_ajax_success();
20775 }
20776
20777 /**
20778 * Check if a given license is active & valid (not expired).
20779 *
20780 * @author Vova Feldman (@svovaf)
20781 * @since 2.1.3
20782 *
20783 * @param FS_Plugin_License $license
20784 * @param bool $check_expiration
20785 *
20786 * @return bool
20787 */
20788 private static function is_active_valid_license( $license, $check_expiration = true ) {
20789 return (
20790 is_object( $license ) &&
20791 FS_Plugin_License::is_valid_id( $license->id ) &&
20792 $license->is_active() &&
20793 ( ! $check_expiration || $license->is_valid() )
20794 );
20795 }
20796
20797 /**
20798 * Checks if there's any site that is associated with an active & valid license.
20799 * This logic is used to determine if the admin can download the premium code base from a network level admin.
20800 *
20801 * @author Vova Feldman (@svovaf)
20802 * @since 2.1.3
20803 *
20804 * @return bool
20805 */
20806 function has_any_active_valid_license() {
20807 if ( ! fs_is_network_admin() ) {
20808 return $this->has_active_valid_license();
20809 }
20810
20811 $installs = $this->get_blog_install_map();
20812 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
20813
20814 foreach ( $installs as $blog_id => $install ) {
20815 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20816 continue;
20817 }
20818
20819 foreach ( $all_plugin_licenses as $license ) {
20820 if ( $license->id == $install->license_id ) {
20821 if ( self::is_active_valid_license( $license ) ) {
20822 return true;
20823 }
20824 }
20825 }
20826 }
20827
20828 return false;
20829 }
20830
20831 /**
20832 * Check if site assigned with license with enabled features.
20833 *
20834 * @author Vova Feldman (@svovaf)
20835 * @since 1.0.6
20836 *
20837 * @return bool
20838 */
20839 function has_features_enabled_license() {
20840 return (
20841 is_object( $this->_license ) &&
20842 is_numeric( $this->_license->id ) &&
20843 $this->_license->is_features_enabled()
20844 );
20845 }
20846
20847 /**
20848 * Checks if the product is activated with a bundle license.
20849 *
20850 * @author Leo Fajardo (@leorw)
20851 * @since 2.4.0
20852 *
20853 * @return bool
20854 */
20855 function is_activated_with_bundle_license() {
20856 if ( ! $this->has_features_enabled_license() ) {
20857 return false;
20858 }
20859
20860 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
20861 }
20862
20863 /**
20864 * Check if user is a trial or have feature enabled license.
20865 *
20866 * @author Vova Feldman (@svovaf)
20867 * @since 1.1.7
20868 *
20869 * @return bool
20870 */
20871 function can_use_premium_code() {
20872 return $this->is_trial() || $this->has_features_enabled_license();
20873 }
20874
20875 /**
20876 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
20877 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
20878 * the context user is authenticated.
20879 *
20880 * @author Leo Fajardo (@leorw)
20881 * @since 1.2.2
20882 *
20883 * @return bool
20884 */
20885 function is_user_admin() {
20886 /**
20887 * Require a super-admin when network activated, running from the network level OR if
20888 * running from the site level but not delegated the opt-in.
20889 *
20890 * @author Vova Feldman (@svovaf)
20891 * @since 2.0.0
20892 */
20893 if ( $this->_is_network_active &&
20894 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
20895 ) {
20896 return is_super_admin();
20897 }
20898
20899 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
20900 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
20901 }
20902
20903 /**
20904 * Sync site's plan.
20905 *
20906 * @author Vova Feldman (@svovaf)
20907 * @since 1.0.3
20908 *
20909 * @uses FS_Api
20910 *
20911 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
20912 * the admin.
20913 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
20914 * network-level "Account" page.
20915 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
20916 * `_sync_plugin_license` method in order to switch to the previous blog when sending
20917 * updates for a single site in case `execute_cron` has switched to a different blog.
20918 */
20919 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
20920 $this->_logger->entrance();
20921
20922 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
20923
20924 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
20925
20926 if ( $is_addon_sync ) {
20927 $this->_sync_addon_license( $plugin_id, $background );
20928 } else {
20929 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
20930 }
20931
20932 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
20933 }
20934
20935 /**
20936 * Sync plugin's add-on license.
20937 *
20938 * @author Vova Feldman (@svovaf)
20939 * @since 1.0.6
20940 * @uses FS_Api
20941 *
20942 * @param number $addon_id
20943 * @param bool $background
20944 */
20945 private function _sync_addon_license( $addon_id, $background ) {
20946 $this->_logger->entrance();
20947
20948 if ( $this->is_addon_activated( $addon_id ) ) {
20949 // If already installed, use add-on sync.
20950 $fs_addon = self::get_instance_by_id( $addon_id );
20951
20952 if (
20953 // Add-on is network activated and network integrated.
20954 $fs_addon->is_network_active() ||
20955 // Background sync cron.
20956 self::is_cron() ||
20957 // Add-on is not network activated or not network integrated.
20958 ! fs_is_network_admin()
20959 ) {
20960 $fs_addon->_sync_license( $background );
20961
20962 return;
20963 }
20964 }
20965
20966 // Validate add-on exists.
20967 $addon = $this->get_addon( $addon_id );
20968
20969 if ( ! is_object( $addon ) ) {
20970 return;
20971 }
20972
20973 // Add add-on into account add-ons.
20974 $account_addons = $this->get_account_addons();
20975 if ( ! is_array( $account_addons ) ) {
20976 $account_addons = array();
20977 }
20978 $account_addons[] = $addon->id;
20979 $account_addons = array_unique( $account_addons );
20980 $this->_store_account_addons( $account_addons );
20981
20982 // Load add-on licenses.
20983 $licenses = $this->_fetch_licenses( $addon->id );
20984
20985 // Sync add-on licenses.
20986 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
20987 $this->_update_licenses( $licenses, $addon->id );
20988
20989 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
20990 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
20991
20992 if ( ! isset( $plans_result->error ) ) {
20993 $plans = array();
20994 foreach ( $plans_result->plans as $plan ) {
20995 $plans[] = new FS_Plugin_Plan( $plan );
20996 }
20997
20998 $this->_admin_notices->add_sticky(
20999 sprintf(
21000 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
21001 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
21002 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
21003 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
21004 $addon->title
21005 ) . ' ' . $this->get_latest_download_link(
21006 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
21007 $addon_id
21008 ),
21009 'addon_plan_upgraded_' . $addon->slug,
21010 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
21011 );
21012 }
21013 }
21014 }
21015 }
21016
21017 /**
21018 * Sync site's plugin plan.
21019 *
21020 * @author Vova Feldman (@svovaf)
21021 * @since 1.0.6
21022 * @uses FS_Api
21023 *
21024 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
21025 * @param bool $send_installs_update Since 2.0.0
21026 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
21027 * syncing its license from the network-level "Account" page (e.g.: after
21028 * activating a license only for the single install).
21029 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
21030 * can be used here to switch to the previous blog in case `execute_cron`
21031 * has switched to a different blog.
21032 */
21033 private function _sync_plugin_license(
21034 $background = false,
21035 $send_installs_update = true,
21036 $is_context_single_site = false,
21037 $current_blog_id = null
21038 ) {
21039 $this->_logger->entrance();
21040
21041 $plan_change = 'none';
21042
21043 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
21044
21045 if ( ! $send_installs_update ) {
21046 $site = $this->_site;
21047 } else {
21048 /**
21049 * Sync site info.
21050 *
21051 * @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.
21052 */
21053 if ( $is_site_level_sync ) {
21054 /**
21055 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
21056 *
21057 * @author Leo Fajardo (@leorw)
21058 * @since 2.2.3
21059 */
21060 if ( is_numeric( $current_blog_id ) ) {
21061 $this->switch_to_blog( $current_blog_id );
21062 }
21063
21064 $result = $this->send_install_update( array(), true, true );
21065 $is_valid = $this->is_api_result_entity( $result );
21066 } else {
21067 $result = $this->send_installs_update( array(), true, true );
21068 $is_valid = $this->is_api_result_object( $result, 'installs' );
21069 }
21070
21071 if ( ! $is_valid ) {
21072 if ( $is_context_single_site ) {
21073 // Switch back to the main blog so that the following logic will have the right entities.
21074 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21075 }
21076
21077 // Show API message only if not background sync or if paying customer.
21078 if ( ! $background || $this->is_paying() ) {
21079 // Try to ping API to see if not blocked.
21080 if ( FS_Api::is_blocked( $result ) ) {
21081 /**
21082 * @author Vova Feldman (@svovaf)
21083 * @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.
21084 */
21085 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
21086 // Add notice immediately if not a background sync.
21087 $add_notice = ( ! $background );
21088
21089 if ( ! $add_notice ) {
21090 $counter = (int) get_transient( '_fs_api_connection_retry_counter' );
21091
21092 // We only want to add the notice after 3 consecutive failures.
21093 $add_notice = ( 3 <= $counter );
21094
21095 if ( ! $add_notice ) {
21096 /**
21097 * 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.
21098 *
21099 * 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.
21100 */
21101 set_transient( '_fs_api_connection_retry_counter', $counter + 1, WP_FS__TIME_WEEK_IN_SEC );
21102 }
21103 }
21104
21105 // Add notice instantly for not-background sync and only after 3 failed attempts for background sync.
21106 if ( $add_notice ) {
21107 self::$_global_admin_notices->add(
21108 $this->generate_api_blocked_notice_message_from_result( $result ),
21109 '',
21110 'error',
21111 $background,
21112 'api_blocked'
21113 );
21114
21115 add_action( 'admin_footer', array( 'Freemius', '_add_api_connectivity_notice_handler_js' ) );
21116
21117 // Notice was just shown, reset connectivity counter.
21118 delete_transient( '_fs_api_connection_retry_counter' );
21119 }
21120 }
21121 } else if ( is_object( $result ) ) {
21122 // Authentication params are broken.
21123 $this->_admin_notices->add(
21124 $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 ),
21125 '',
21126 'error'
21127 );
21128 }
21129 }
21130
21131 // No reason to continue with license sync while there are API issues.
21132 return;
21133 }
21134
21135 // API is working now. Delete the transient and start afresh.
21136 delete_transient('_fs_api_connection_retry_counter');
21137
21138 if ( $is_site_level_sync ) {
21139 $site = new FS_Site( $result );
21140 } else {
21141 // Map site addresses to their blog IDs.
21142 $address_to_blog_map = $this->get_address_to_blog_map();
21143
21144 // Find the current context install.
21145 $site = null;
21146 foreach ( $result->installs as $install ) {
21147 if ( $install->id == $this->_site->id ) {
21148 $site = new FS_Site( $install );
21149 } else {
21150 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
21151 $blog_id = $address_to_blog_map[ $address ];
21152
21153 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
21154 }
21155 }
21156 }
21157
21158 // Sync plans.
21159 $this->_sync_plans();
21160 }
21161
21162 // Remove sticky API connectivity message.
21163 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
21164
21165 if ( ! $this->has_paid_plan() ) {
21166 $this->_site = $site;
21167 $this->_store_site(
21168 true,
21169 $is_site_level_sync ?
21170 null :
21171 $this->get_network_install_blog_id()
21172 );
21173 } else {
21174 $context_blog_id = 0;
21175
21176 if ( $is_context_single_site ) {
21177 $context_blog_id = get_current_blog_id();
21178
21179 // Switch back to the main blog in order to properly sync the license.
21180 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21181 }
21182
21183 /**
21184 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
21185 * associated with that ID is not included in the user's licenses collection.
21186 * Save previous value to manage remote license renewals.
21187 */
21188 $was_license_expired_before_sync = is_object( $this->_license ) && $this->_license->is_expired();
21189 $this->_sync_licenses(
21190 $site->license_id,
21191 ( $is_context_single_site ?
21192 $context_blog_id :
21193 null
21194 )
21195 );
21196
21197 if ( $is_context_single_site ) {
21198 $this->switch_to_blog( $context_blog_id );
21199 }
21200
21201 // Check if plan / license changed.
21202 if ( $site->plan_id != $this->_site->plan_id ||
21203 // Check if trial started.
21204 $site->trial_plan_id != $this->_site->trial_plan_id ||
21205 $site->trial_ends != $this->_site->trial_ends ||
21206 // Check if license changed.
21207 $site->license_id != $this->_site->license_id
21208 ) {
21209 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
21210 // New trial started.
21211 $this->_site = $site;
21212 $plan_change = 'trial_started';
21213
21214 // For trial with subscription use-case.
21215 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
21216
21217 if ( is_object( $new_license ) && $new_license->is_valid() ) {
21218 $this->_site = $site;
21219 $this->_update_site_license( $new_license );
21220 $this->_store_licenses();
21221
21222 $this->_sync_site_subscription( $this->_license );
21223 }
21224 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
21225 // Was in trial, but now trial expired and no license ID.
21226 // New trial started.
21227 $this->_site = $site;
21228 $plan_change = 'trial_expired';
21229 } else {
21230 $is_free = $this->is_free_plan();
21231
21232 // Make sure license exist and not expired.
21233 $new_license = is_null( $site->license_id ) ?
21234 null :
21235 $this->_get_license_by_id( $site->license_id );
21236
21237 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
21238 // License cancelled.
21239 $this->_site = $site;
21240 $this->_update_site_license( $new_license );
21241 $this->_store_licenses();
21242
21243 $plan_change = 'cancelled';
21244 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
21245 // The license is expired, so ignore upgrade method.
21246 $this->_site = $site;
21247 } else {
21248 // License changed.
21249 $this->_site = $site;
21250
21251 /**
21252 * IMPORTANT:
21253 * 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.
21254 *
21255 * @author Vova Feldman (@svovaf)
21256 * @since 2.0.0
21257 */
21258 $this->_update_site_license( $new_license );
21259
21260 if ( ! $is_context_single_site &&
21261 fs_is_network_admin() &&
21262 $this->_is_network_active &&
21263 $new_license->quota > 1 &&
21264 get_blog_count() > 1
21265 ) {
21266 // See if license can activated on all sites.
21267 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
21268 if ( ! fs_request_get_bool( 'auto_install' ) ) {
21269 // Open the license activation dialog box on the account page.
21270 add_action( 'admin_footer', array(
21271 &$this,
21272 '_open_license_activation_dialog_box'
21273 ) );
21274 }
21275 }
21276 }
21277
21278 $this->_store_licenses();
21279
21280 $plan_change = $is_free ?
21281 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
21282 ( is_object( $new_license ) ?
21283 'changed' :
21284 'downgraded' );
21285 }
21286 }
21287
21288 // Store updated site info.
21289 $this->_store_site(
21290 true,
21291 $is_site_level_sync ?
21292 null :
21293 $this->get_network_install_blog_id()
21294 );
21295 } else {
21296 if ( ! is_object( $this->_license ) ) {
21297 $this->maybe_update_whitelabel_flag(
21298 FS_Plugin_License::is_valid_id( $site->license_id ) ?
21299 $this->get_license_by_id( $site->license_id ) :
21300 null
21301 );
21302 } else {
21303 $this->maybe_update_whitelabel_flag( $this->_license );
21304
21305 if ( $this->_license->is_expired() ) {
21306 if ( ! $this->has_features_enabled_license() ) {
21307 $this->_deactivate_license();
21308 $plan_change = 'downgraded';
21309 } else {
21310 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
21311
21312 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
21313 /**
21314 * Show the expired license notice every 14 days.
21315 *
21316 * @author Leo Fajardo (@leorw)
21317 * @since 2.3.1
21318 */
21319 $plan_change = 'expired';
21320 }
21321 }
21322 } else if ( $was_license_expired_before_sync ) {
21323 /**
21324 * If license was expired but it is not anymore.
21325 *
21326 *
21327 * @author Daniele Alessandra (@danielealessandra)
21328 */
21329 $plan_change = 'extended';
21330 }
21331 }
21332
21333 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
21334 $this->_sync_site_subscription( $this->_license );
21335 }
21336 }
21337
21338 if ( ! $this->is_addon() &&
21339 $this->_site->is_beta() !== $site->is_beta()
21340 ) {
21341 // Beta flag updated.
21342 $this->_site = $site;
21343
21344 $this->_store_site(
21345 true,
21346 $is_site_level_sync ?
21347 null :
21348 $this->get_network_install_blog_id()
21349 );
21350 }
21351
21352 if ( $this->is_addon() || $this->has_addons() ) {
21353 /**
21354 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21355 * an updated valid user licenses collection will be fetched from the server which is used to also
21356 * update the account add-ons (add-ons the user has licenses for).
21357 *
21358 * @author Leo Fajardo (@leorw)
21359 * @since 2.2.4
21360 */
21361 $this->purge_valid_user_licenses_cache();
21362 }
21363 }
21364
21365 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21366
21367 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
21368 switch ( $plan_change ) {
21369 case 'none':
21370 if ( ! $background && is_admin() ) {
21371 $plan = $this->is_trial() ?
21372 $this->get_trial_plan() :
21373 $this->get_plan();
21374
21375 if ( $plan->is_free() ) {
21376 $this->_admin_notices->add(
21377 sprintf(
21378 $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' ),
21379 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
21380 ) . ' ' . sprintf(
21381 '<a href="%s">%s</a>',
21382 $this->contact_url(
21383 'bug',
21384 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' ),
21385 strtoupper( $plan->name )
21386 )
21387 ),
21388 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21389 ),
21390 $hmm_text
21391 );
21392 }
21393 }
21394 break;
21395 case 'upgraded':
21396 case 'activated':
21397 $this->add_after_plan_activation_or_upgrade_instructions_notice( 'upgraded' === $plan_change );
21398
21399 $this->_admin_notices->remove_sticky( array(
21400 'trial_started',
21401 'trial_promotion',
21402 'trial_expired',
21403 'activation_complete',
21404 'license_expired',
21405 ) );
21406 break;
21407 case 'extended':
21408 $this->_admin_notices->remove_sticky( array(
21409 'trial_expired',
21410 'license_expired',
21411 ) );
21412 break;
21413 case 'changed':
21414 $this->_admin_notices->add_sticky(
21415 sprintf(
21416 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
21417 $this->get_plan_title()
21418 ),
21419 'plan_changed'
21420 );
21421
21422 $this->_admin_notices->remove_sticky( array(
21423 'trial_started',
21424 'trial_promotion',
21425 'trial_expired',
21426 'activation_complete',
21427 ) );
21428 break;
21429 case 'downgraded':
21430 $this->_admin_notices->add_sticky(
21431 ($this->has_free_plan() ?
21432 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 ) :
21433 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21434 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) ) ),
21435 'license_expired',
21436 $hmm_text
21437 );
21438 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21439 break;
21440 case 'cancelled':
21441 $this->_admin_notices->add(
21442 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
21443 sprintf(
21444 '<a href="%s">%s</a>',
21445 $this->contact_url( 'bug' ),
21446 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21447 ),
21448 $hmm_text,
21449 'error'
21450 );
21451 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21452 break;
21453 case 'expired':
21454 $this->_admin_notices->add_sticky(
21455 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 ),
21456 'license_expired',
21457 $hmm_text
21458 );
21459
21460 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
21461
21462 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21463 break;
21464 case 'trial_started':
21465 $this->add_complete_upgrade_instructions_notice(
21466 sprintf(
21467 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
21468 '<i>' . $this->get_plugin_name() . '</i>'
21469 ),
21470 'trial_started',
21471 $this->get_trial_plan()->title
21472 );
21473
21474 $this->_admin_notices->remove_sticky( array(
21475 'trial_promotion',
21476 ) );
21477 break;
21478 case 'trial_expired':
21479 $this->_admin_notices->add_sticky(
21480 ($this->has_free_plan() ?
21481 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
21482 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21483 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))),
21484 'trial_expired',
21485 $hmm_text
21486 );
21487 $this->_admin_notices->remove_sticky( array(
21488 'trial_started',
21489 'trial_promotion',
21490 'plan_upgraded',
21491 ) );
21492 break;
21493 }
21494 }
21495
21496 if ( 'none' !== $plan_change ) {
21497 if (
21498 ! is_object( $this->_license ) ||
21499 ! $this->_license->is_whitelabeled
21500 ) {
21501 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
21502 }
21503
21504 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
21505 }
21506 }
21507
21508 /**
21509 * @author Leo Fajardo (@leorw)
21510 * @since 2.5.4
21511 *
21512 * @param mixed $result
21513 *
21514 * @return string
21515 */
21516 private function generate_api_blocked_notice_message_from_result( $result ) {
21517 $api_domains = $this->apply_filters( 'api_domains', array(
21518 'api.freemius.com',
21519 'wp.freemius.com',
21520 ) );
21521
21522 $api_domains_list_items = '';
21523
21524 foreach( $api_domains as $api_domain ) {
21525 $api_domains_list_items .= "<li>{$api_domain}</li>";
21526 }
21527
21528 $error_message = sprintf(
21529 $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' ),
21530 $this->get_plugin_name(),
21531 "<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>"
21532 );
21533
21534 $error_message =
21535 "<div>{$error_message}</div>" .
21536 '<div class="fs-api-request-error-details" style="display: none">' .
21537 '<strong>' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . '</strong><br>' .
21538 $result->error->message .
21539 '</div>';
21540
21541 return $error_message;
21542 }
21543
21544 /**
21545 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
21546 *
21547 * @author Vova Feldman (@svovaf)
21548 * @since 2.0.0
21549 */
21550 public function _open_license_activation_dialog_box() {
21551 $vars = array( 'license_id' => $this->_site->license_id );
21552 fs_require_once_template( 'js/open-license-activation.php', $vars );
21553 }
21554
21555 /**
21556 * @author Vova Feldman (@svovaf)
21557 * @since 1.0.5
21558 *
21559 * @param bool $background
21560 * @param FS_Plugin_License|null $premium_license
21561 */
21562 protected function _activate_license( $background = false, $premium_license = null ) {
21563 $this->_logger->entrance();
21564
21565 if ( is_null( $premium_license ) ) {
21566 $license_id = fs_request_get( 'license_id' );
21567
21568 if ( is_object( $this->_site ) &&
21569 FS_Plugin_License::is_valid_id( $license_id ) &&
21570 $license_id == $this->_site->license_id
21571 ) {
21572 // License is already activated.
21573 return;
21574 }
21575
21576 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
21577 $this->_get_license_by_id( $license_id ) :
21578 $this->_get_available_premium_license();
21579 }
21580
21581 if ( ! is_object( $premium_license ) ) {
21582 return;
21583 }
21584
21585 if ( ! is_object( $this->_site ) ) {
21586 // Not yet opted-in.
21587 $user = $this->get_current_or_network_user();
21588 if ( ! is_object( $user ) ) {
21589 $user = self::_get_user_by_id( $premium_license->user_id );
21590 }
21591
21592 if ( is_object( $user ) ) {
21593 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
21594 } else {
21595 $this->opt_in(
21596 false,
21597 false,
21598 false,
21599 $premium_license->secret_key,
21600 false,
21601 false,
21602 false,
21603 null,
21604 array(),
21605 true,
21606 $premium_license->user_id
21607 );
21608
21609 return;
21610 }
21611 }
21612
21613
21614 /**
21615 * If the premium license is already associated with the install, just
21616 * update the license reference (activation is not required).
21617 *
21618 * @since 1.1.9
21619 */
21620 if ( $premium_license->id == $this->_site->license_id ) {
21621 // License is already activated.
21622 $this->_update_site_license( $premium_license );
21623 $this->_store_account();
21624
21625 return;
21626 }
21627
21628 if ( $this->_site->user_id != $premium_license->user_id ) {
21629 $api_request_params = array( 'license_key' => $premium_license->secret_key );
21630 } else {
21631 $api_request_params = array();
21632 }
21633
21634 $api = $this->get_api_site_scope();
21635 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
21636
21637 if ( ! $this->is_api_result_entity( $license ) ) {
21638 if ( ! $background ) {
21639 $this->_admin_notices->add( sprintf(
21640 '%s %s',
21641 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
21642 ( is_object( $license ) && isset( $license->error ) ?
21643 $license->error->message :
21644 sprintf( '%s<br><code>%s</code>',
21645 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
21646 var_export( $license, true )
21647 )
21648 )
21649 ),
21650 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
21651 'error'
21652 );
21653 }
21654
21655 return;
21656 }
21657
21658 $premium_license = new FS_Plugin_License( $license );
21659
21660 // Updated site plan.
21661 $site = $this->get_api_site_scope()->get( '/', true );
21662 if ( $this->is_api_result_entity( $site ) ) {
21663 $this->_site = new FS_Site( $site );
21664 }
21665 $this->_update_site_license( $premium_license );
21666
21667 $this->_store_account();
21668
21669 if ( $this->is_addon() || $this->has_addons() ) {
21670 /**
21671 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21672 * an updated valid user licenses collection will be fetched from the server which is used to also
21673 * update the account add-ons (add-ons the user has licenses for).
21674 *
21675 * @author Leo Fajardo (@leorw)
21676 * @since 2.2.4
21677 */
21678 $this->purge_valid_user_licenses_cache();
21679 }
21680
21681 if ( ! $background ) {
21682 $this->add_complete_upgrade_instructions_notice(
21683 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ),
21684 'license_activated'
21685 );
21686 }
21687
21688 $this->_admin_notices->remove_sticky( array(
21689 'trial_promotion',
21690 'license_expired',
21691 ) );
21692 }
21693
21694 /**
21695 * @author Vova Feldman (@svovaf)
21696 * @since 1.0.5
21697 *
21698 * @param bool $show_notice
21699 */
21700 protected function _deactivate_license( $show_notice = true ) {
21701 $this->_logger->entrance();
21702
21703 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21704
21705 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
21706 $this->_admin_notices->add(
21707 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() ),
21708 $hmm_text
21709 );
21710
21711 return;
21712 }
21713
21714 $api = $this->get_api_site_scope();
21715 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
21716
21717 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
21718 }
21719
21720 /**
21721 * @author Leo Fajardo (@leorw)
21722 * @since 2.2.1
21723 *
21724 * @param FS_Plugin_License $license
21725 * @param bool|string $hmm_text
21726 * @param bool $show_notice
21727 */
21728 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
21729 if ( isset( $license->error ) ) {
21730 $this->_admin_notices->add(
21731 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
21732 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
21733 $hmm_text,
21734 'error'
21735 );
21736
21737 return;
21738 }
21739
21740 // Update license cache.
21741 if ( is_array( $this->_licenses ) ) {
21742 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
21743 if ( $license->id == $this->_licenses[ $i ]->id ) {
21744 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
21745 }
21746 }
21747 }
21748
21749 // Update site plan to default.
21750 $this->_sync_plans();
21751 $this->_site->plan_id = $this->_plans[0]->id;
21752 // Unlink license from site.
21753 $this->_update_site_license( null );
21754
21755 $this->_store_account();
21756
21757 if ( $show_notice ) {
21758 $this->_admin_notices->add(
21759 sprintf( $this->is_only_premium() ?
21760 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
21761 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
21762 $this->get_plan_title()
21763 ),
21764 $this->get_text_inline( 'O.K', 'ok' )
21765 );
21766 }
21767
21768 $this->_admin_notices->remove_sticky( array(
21769 'plan_upgraded',
21770 'license_activated',
21771 ) );
21772 }
21773
21774 /**
21775 * Site plan downgrade.
21776 *
21777 * @author Vova Feldman (@svovaf)
21778 * @since 1.0.4
21779 *
21780 * @return object
21781 *
21782 * @uses FS_Api
21783 */
21784 private function _downgrade_site() {
21785 $this->_logger->entrance();
21786
21787 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
21788
21789 $api = $this->get_api_site_scope();
21790 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
21791
21792 $plan_downgraded = false;
21793 $plan = false;
21794 if ( $this->is_api_result_entity( $site ) ) {
21795 $prev_plan_id = $this->_site->plan_id;
21796
21797 // Update new site plan id.
21798 $this->_site->plan_id = $site->plan_id;
21799
21800 $plan = $this->get_plan();
21801 $subscription = $this->_sync_site_subscription( $this->_license );
21802
21803 // Plan downgraded if plan was changed or subscription was cancelled.
21804 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
21805 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
21806 } else {
21807 // handle different error cases.
21808 $this->handle_license_deactivation_result(
21809 $site,
21810 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
21811 );
21812 }
21813
21814 if ( ! $plan_downgraded ) {
21815 return (object) array(
21816 'error' => (object) array(
21817 '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' )
21818 )
21819 );
21820 }
21821
21822 // Remove previous sticky message about upgrade (if exist).
21823 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21824
21825 $this->_admin_notices->add(
21826 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
21827 $plan->title,
21828 human_time_diff( time(), strtotime( $this->_license->expiration ) )
21829 )
21830 );
21831
21832 // Store site updates.
21833 $this->_store_site();
21834
21835 if ( $deactivate_license &&
21836 ! FS_Plugin_License::is_valid_id( $site->license_id )
21837 ) {
21838 if ( $this->_site->is_localhost() ) {
21839 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
21840 } else {
21841 $this->_license->activated = max( 0, $this->_license->activated - 1 );
21842 }
21843
21844 // Handle successful license deactivation result.
21845 $this->handle_license_deactivation_result( $this->_license );
21846 }
21847
21848 return $site;
21849 }
21850
21851 /**
21852 * @author Vova Feldman (@svovaf)
21853 * @since 1.1.8.1
21854 *
21855 * @param bool|string $plan_name
21856 * @param bool $add_sticky_notice
21857 *
21858 * @return bool If trial was successfully started.
21859 */
21860 function start_trial( $plan_name = false, $add_sticky_notice = false ) {
21861 $this->_logger->entrance();
21862
21863 // Alias.
21864 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
21865
21866 if ( $this->is_trial() ) {
21867 // Already in trial mode.
21868 $this->_admin_notices->add(
21869 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
21870 $oops_text,
21871 'error',
21872 $add_sticky_notice
21873 );
21874
21875 return false;
21876 }
21877
21878 if ( $this->_site->is_trial_utilized() && ! $this->is_payments_sandbox() ) {
21879 // Trial was already utilized.
21880 $this->_admin_notices->add(
21881 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
21882 $oops_text,
21883 'error',
21884 $add_sticky_notice
21885 );
21886
21887 return false;
21888 }
21889
21890 if ( false !== $plan_name ) {
21891 $plan = $this->get_plan_by_name( $plan_name );
21892
21893 if ( false === $plan ) {
21894 // Plan doesn't exist.
21895 $this->_admin_notices->add(
21896 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
21897 $oops_text,
21898 'error',
21899 $add_sticky_notice
21900 );
21901
21902 return false;
21903 }
21904
21905 if ( ! $plan->has_trial() ) {
21906 // Plan doesn't exist.
21907 $this->_admin_notices->add(
21908 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
21909 $oops_text,
21910 'error',
21911 $add_sticky_notice
21912 );
21913
21914 return false;
21915 }
21916 } else {
21917 if ( ! $this->has_trial_plan() ) {
21918 // None of the plans have a trial.
21919 $this->_admin_notices->add(
21920 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
21921 $oops_text,
21922 'error',
21923 $add_sticky_notice
21924 );
21925
21926 return false;
21927 }
21928
21929 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
21930
21931 $plan = $plans_with_trial[0];
21932 }
21933
21934 $trial_params = array();
21935
21936 if ( $this->is_payments_sandbox() ) {
21937 $trial_params['trial_timestamp'] = time();
21938 $trial_params['trial_token'] = FS_Security::instance()->get_trial_token(
21939 $this->get_plugin(),
21940 $plan,
21941 $trial_params['trial_timestamp']
21942 );
21943 }
21944
21945 $api = $this->get_api_site_scope();
21946 $trial = $api->call( "plans/{$plan->id}/trials.json", 'post', $trial_params );
21947
21948 if ( ! $this->is_api_result_entity( $trial ) ) {
21949 // Some API error while trying to start the trial.
21950 $this->_admin_notices->add(
21951 $this->get_api_error_message( $trial ),
21952 $oops_text,
21953 'error',
21954 $add_sticky_notice
21955 );
21956
21957 return false;
21958 }
21959
21960 // Sync license.
21961 $this->_sync_license();
21962
21963 return $this->is_trial();
21964 }
21965
21966 /**
21967 * Cancel site trial.
21968 *
21969 * @author Vova Feldman (@svovaf)
21970 * @since 1.0.9
21971 *
21972 * @return object
21973 *
21974 * @uses FS_Api
21975 */
21976 private function _cancel_trial() {
21977 $this->_logger->entrance();
21978
21979 if ( ! $this->is_trial() ) {
21980 return (object) array(
21981 'error' => (object) array(
21982 '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' )
21983 )
21984 );
21985 }
21986
21987 $trial_plan = $this->get_trial_plan();
21988
21989 $api = $this->get_api_site_scope();
21990 $site = $api->call( 'trials.json', 'delete' );
21991
21992 $trial_cancelled = false;
21993
21994 if ( $this->is_api_result_entity( $site ) ) {
21995 $prev_trial_ends = $this->_site->trial_ends;
21996
21997 if ( $this->is_paid_trial() ) {
21998 $this->_license->expiration = $site->trial_ends;
21999 $this->_license->is_cancelled = true;
22000 $this->_update_site_license( $this->_license );
22001 $this->_store_licenses();
22002
22003 // Clear subscription reference.
22004 $this->_sync_site_subscription( null );
22005 }
22006
22007 // Update site info.
22008 $this->_site = new FS_Site( $site );
22009
22010 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
22011 } else {
22012 // @todo handle different error cases.
22013 }
22014
22015 if ( ! $trial_cancelled ) {
22016 return (object) array(
22017 'error' => (object) array(
22018 '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' )
22019 )
22020 );
22021 }
22022
22023 // Remove previous sticky messages about upgrade or trial (if exist).
22024 $this->_admin_notices->remove_sticky( array(
22025 'trial_started',
22026 'trial_promotion',
22027 'plan_upgraded',
22028 ) );
22029
22030 // Store site updates.
22031 $this->_store_site();
22032
22033 if ( ! $this->is_addon() ||
22034 ! $this->deactivate_premium_only_addon_without_license( true )
22035 ) {
22036 $this->_admin_notices->add(
22037 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
22038 );
22039 }
22040
22041 return $site;
22042 }
22043
22044 /**
22045 * @author Vova Feldman (@svovaf)
22046 * @since 1.0.6
22047 *
22048 * @param bool|number $plugin_id
22049 *
22050 * @return bool
22051 */
22052 private function _is_addon_id( $plugin_id ) {
22053 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
22054 }
22055
22056 /**
22057 * Check if user eligible to download premium version updates.
22058 *
22059 * @author Vova Feldman (@svovaf)
22060 * @since 1.0.6
22061 *
22062 * @return bool
22063 */
22064 private function _can_download_premium() {
22065 return $this->has_any_active_valid_license() ||
22066 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
22067 }
22068
22069 /**
22070 *
22071 * @author Vova Feldman (@svovaf)
22072 * @since 1.0.6
22073 *
22074 * @param bool|number $addon_id
22075 * @param string $type "json" or "zip"
22076 *
22077 * @return string
22078 */
22079 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
22080
22081 $is_addon = $this->_is_addon_id( $addon_id );
22082
22083 $is_premium = null;
22084 if ( ! $is_addon ) {
22085 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
22086 } else if ( $this->is_addon_activated( $addon_id ) ) {
22087 $fs_addon = self::get_instance_by_id( $addon_id );
22088 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
22089 }
22090
22091 // If add-on, then append add-on ID.
22092 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
22093 '/updates/latest.' . $type;
22094
22095 // If add-on and not yet activated, try to fetch based on server licensing.
22096 if ( is_bool( $is_premium ) ) {
22097 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
22098 }
22099
22100 if ( $this->has_secret_key() ) {
22101 $endpoint = add_query_arg( 'type', 'all', $endpoint );
22102 } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) {
22103 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
22104 }
22105
22106 return $endpoint;
22107 }
22108
22109 /**
22110 * @author Vova Feldman (@svovaf)
22111 * @since 1.0.4
22112 *
22113 * @param bool|number $addon_id
22114 * @param bool $flush Since 1.1.7.3
22115 * @param int $expiration Since 1.2.2.7
22116 * @param bool|string $newer_than Since 2.2.1
22117 * @param bool|string $fetch_readme Since 2.2.1
22118 * @param bool $fetch_upgrade_notice Since 2.12.1
22119 *
22120 * @return object|false Plugin latest tag info.
22121 */
22122 function _fetch_latest_version(
22123 $addon_id = false,
22124 $flush = true,
22125 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22126 $newer_than = false,
22127 $fetch_readme = true,
22128 $fetch_upgrade_notice = false
22129 ) {
22130 $this->_logger->entrance();
22131
22132 if ( $this->is_unresolved_clone( true ) ) {
22133 return false;
22134 }
22135
22136 $switch_to_blog_id = null;
22137
22138 /**
22139 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
22140 * @since 1.1.7.4 Also check updates for add-ons.
22141 */
22142 if (
22143 ( ! $this->is_registered() || ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed() ) &&
22144 ! $this->_is_addon_id( $addon_id )
22145 ) {
22146 if ( ! is_multisite() ) {
22147 return false;
22148 }
22149
22150 $installs_map = $this->get_blog_install_map();
22151
22152 foreach ( $installs_map as $blog_id => $install ) {
22153 if ( ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed( $blog_id ) ) {
22154 continue;
22155 }
22156
22157 /**
22158 * @var FS_Site $install
22159 */
22160 if ( $install->is_trial() ) {
22161 $switch_to_blog_id = $blog_id;
22162 break;
22163 }
22164
22165 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
22166 $license = $this->get_license_by_id( $install->license_id );
22167
22168 if ( is_object( $license ) && $license->is_features_enabled() ) {
22169 $switch_to_blog_id = $blog_id;
22170 break;
22171 }
22172 }
22173 }
22174
22175 if ( is_null( $switch_to_blog_id ) ) {
22176 return false;
22177 }
22178 }
22179
22180 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
22181 get_current_blog_id() :
22182 0;
22183
22184 if ( is_numeric( $switch_to_blog_id ) ) {
22185 $this->switch_to_blog( $switch_to_blog_id );
22186 }
22187
22188 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
22189
22190 if ( ! empty( $newer_than ) ) {
22191 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
22192 }
22193
22194 if ( true === $fetch_readme ) {
22195 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
22196
22197 // Don't cache the API response when fetching readme information.
22198 $expiration = null;
22199 }
22200
22201 if ( true === $fetch_upgrade_notice ) {
22202 $latest_version_endpoint = add_query_arg( 'include_upgrade_notice', 'true', $latest_version_endpoint );
22203 }
22204
22205 $tag = $this->get_api_site_or_plugin_scope()->get(
22206 $latest_version_endpoint,
22207 $flush,
22208 $expiration
22209 );
22210
22211 if ( is_numeric( $switch_to_blog_id ) ) {
22212 $this->switch_to_blog( $current_blog_id );
22213 }
22214
22215 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
22216
22217 $this->_logger->departure( 'Latest version ' . $latest_version );
22218
22219 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
22220 }
22221
22222 #----------------------------------------------------------------------------------
22223 #region Download Plugin
22224 #----------------------------------------------------------------------------------
22225
22226 /**
22227 * Download latest plugin version, based on plan.
22228 *
22229 * Not like _download_latest(), this will redirect the page
22230 * to secure download url to prevent dual download (from FS to WP server,
22231 * and then from WP server to the client / browser).
22232 *
22233 * @author Vova Feldman (@svovaf)
22234 * @since 1.0.9
22235 *
22236 * @param bool|number $plugin_id
22237 *
22238 * @uses FS_Api
22239 * @uses wp_redirect()
22240 */
22241 private function download_latest_directly( $plugin_id = false ) {
22242 $this->_logger->entrance();
22243
22244 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
22245 }
22246
22247 /**
22248 * Get latest plugin FS API download URL.
22249 *
22250 * @author Vova Feldman (@svovaf)
22251 * @since 1.0.9
22252 *
22253 * @param bool|number $plugin_id
22254 *
22255 * @return string
22256 */
22257 private function get_latest_download_api_url( $plugin_id = false ) {
22258 $this->_logger->entrance();
22259
22260 $download_api_url = $this->get_api_site_scope()->get_signed_url(
22261 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
22262 );
22263
22264 return str_replace( 'http:', 'https:', $download_api_url );
22265 }
22266
22267 /**
22268 * Get payment invoice URL.
22269 *
22270 * @author Vova Feldman (@svovaf)
22271 * @since 1.2.0
22272 *
22273 * @param bool|number $payment_id
22274 *
22275 * @return string
22276 */
22277 function _get_invoice_api_url( $payment_id = false ) {
22278 $this->_logger->entrance();
22279
22280 $url = $this->get_api_user_scope()->get_signed_url(
22281 "/payments/{$payment_id}/invoice.pdf"
22282 );
22283
22284 if ( ! fs_starts_with( $url, 'https://' ) ) {
22285 // Always use HTTPS for invoices.
22286 $url = 'https' . substr( $url, 4 );
22287 }
22288
22289 return $url;
22290 }
22291
22292 /**
22293 * Get latest plugin download link.
22294 *
22295 * @author Vova Feldman (@svovaf)
22296 * @since 1.0.9
22297 *
22298 * @param string $label
22299 * @param bool|number $plugin_id
22300 *
22301 * @return string
22302 */
22303 private function get_latest_download_link( $label, $plugin_id = false ) {
22304 return sprintf(
22305 '<a target="_blank" rel="noopener" href="%s">%s</a>',
22306 $this->_get_latest_download_local_url( $plugin_id ),
22307 $label
22308 );
22309 }
22310
22311 /**
22312 * Get latest plugin download local URL.
22313 *
22314 * @author Vova Feldman (@svovaf)
22315 * @since 1.0.9
22316 *
22317 * @param bool|number $plugin_id
22318 *
22319 * @return string
22320 */
22321 function _get_latest_download_local_url( $plugin_id = false ) {
22322 // Add timestamp to protect from caching.
22323 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
22324
22325 if ( ! empty( $plugin_id ) ) {
22326 $params['plugin_id'] = $plugin_id;
22327 } else if ( $this->is_addon() ) {
22328 $params['plugin_id'] = $this->get_id();
22329 }
22330
22331 $fs = $this->is_addon() ?
22332 $this->get_parent_instance() :
22333 $this;
22334
22335 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
22336 }
22337
22338 #endregion Download Plugin ------------------------------------------------------------------
22339
22340 /**
22341 * @author Vova Feldman (@svovaf)
22342 * @since 1.0.4
22343 *
22344 * @uses FS_Api
22345 *
22346 * @param bool $background Hints the method if it's a background updates check. If false, it means that
22347 * was initiated by the admin.
22348 * @param bool|number $plugin_id
22349 * @param bool $flush Since 1.1.7.3
22350 */
22351 private function check_updates( $background = false, $plugin_id = false, $flush = true ) {
22352 $this->_logger->entrance();
22353
22354 $newer_than = ( $this->is_premium() ? $this->get_plugin_version() : false );
22355
22356 // Check if there's a newer version for download.
22357 $new_version = $this->_fetch_newer_version(
22358 $plugin_id,
22359 $flush,
22360 FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION,
22361 $newer_than,
22362 ( false !== $newer_than )
22363 );
22364
22365 $update = null;
22366 if ( is_object( $new_version ) ) {
22367 $update = new FS_Plugin_Tag( $new_version );
22368
22369 if ( ! $background ) {
22370 $this->_admin_notices->add(
22371 sprintf(
22372 /* translators: %s: Numeric version number (e.g. '2.1.9' */
22373 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
22374 $update->version,
22375 sprintf(
22376 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22377 $this->get_account_url( 'download_latest' ),
22378 sprintf(
22379 /* translators: %s: plan name (e.g. latest "Professional" version) */
22380 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
22381 $this->get_plan_title()
22382 )
22383 )
22384 ),
22385 $this->get_text_inline( 'New', 'new' ) . '!'
22386 );
22387 }
22388 } else if ( false === $new_version && ! $background ) {
22389 $this->_admin_notices->add(
22390 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
22391 $this->get_text_inline( 'You are all good!', 'you-are-good' )
22392 );
22393 }
22394
22395 $this->_store_update( $update, true, $plugin_id );
22396 }
22397
22398 /**
22399 * @author Vova Feldman (@svovaf)
22400 * @since 1.0.4
22401 *
22402 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
22403 *
22404 * @return FS_Plugin[]
22405 *
22406 * @uses FS_Api
22407 */
22408 private function sync_addons( $flush = false ) {
22409 $this->_logger->entrance();
22410
22411 $api = $this->get_api_site_or_plugin_scope();
22412
22413 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
22414
22415 /**
22416 * @since 1.2.1
22417 *
22418 * If there's a cached version of the add-ons and not asking
22419 * for a flush, just use the currently stored add-ons.
22420 */
22421 if ( ! $flush && $api->is_cached( $path ) ) {
22422 $addons = self::get_all_addons();
22423
22424 return isset( $addons[ $this->_plugin->id ] ) ?
22425 $addons[ $this->_plugin->id ] :
22426 array();
22427 }
22428
22429 $result = $api->get( $path, $flush );
22430
22431 $addons = array();
22432 if ( $this->is_api_result_object( $result, 'plugins' ) &&
22433 is_array( $result->plugins )
22434 ) {
22435 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
22436 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
22437 }
22438
22439 $this->_store_addons( $addons, true );
22440 }
22441
22442 return $addons;
22443 }
22444
22445 /**
22446 * Handle user email update.
22447 *
22448 * @author Vova Feldman (@svovaf)
22449 * @since 1.0.3
22450 * @uses FS_Api
22451 *
22452 * @param string $new_email
22453 *
22454 * @return object
22455 */
22456 private function update_email( $new_email ) {
22457 $this->_logger->entrance();
22458
22459 $api = $this->get_api_user_scope();
22460 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
22461 'email' => $new_email,
22462 'after_email_confirm_url' => $this->_get_admin_page_url(
22463 'account',
22464 array( 'fs_action' => 'sync_user' )
22465 ),
22466 ) );
22467
22468 if ( ! isset( $user->error ) ) {
22469 $this->_user->email = $user->email;
22470 $this->_user->is_verified = $user->is_verified;
22471 $this->_store_user();
22472 } else {
22473 // handle different error cases.
22474 }
22475
22476 return $user;
22477 }
22478
22479 #----------------------------------------------------------------------------------
22480 #region API Error Handling
22481 #----------------------------------------------------------------------------------
22482
22483 /**
22484 * @author Vova Feldman (@svovaf)
22485 * @since 1.1.1
22486 *
22487 * @param mixed $result
22488 *
22489 * @return bool Is API result contains an error.
22490 */
22491 private function is_api_error( $result ) {
22492 return FS_Api::is_api_error( $result );
22493 }
22494
22495 /**
22496 * Checks if given API result is a non-empty and not an error object.
22497 *
22498 * @author Vova Feldman (@svovaf)
22499 * @since 1.2.1.5
22500 *
22501 * @param mixed $result
22502 * @param string|null $required_property Optional property we want to verify that is set.
22503 *
22504 * @return bool
22505 */
22506 function is_api_result_object( $result, $required_property = null ) {
22507 return FS_Api::is_api_result_object( $result, $required_property );
22508 }
22509
22510 /**
22511 * Checks if given API result is a non-empty entity object with non-empty ID.
22512 *
22513 * @author Vova Feldman (@svovaf)
22514 * @since 1.2.1.5
22515 *
22516 * @param mixed $result
22517 *
22518 * @return bool
22519 */
22520 private function is_api_result_entity( $result ) {
22521 return FS_Api::is_api_result_entity( $result );
22522 }
22523
22524 #endregion
22525
22526 /**
22527 * Make sure a given argument is an array of a specific type.
22528 *
22529 * @author Vova Feldman (@svovaf)
22530 * @since 1.2.1.5
22531 *
22532 * @param mixed $array
22533 * @param string $class
22534 *
22535 * @return bool
22536 */
22537 private function is_array_instanceof( $array, $class ) {
22538 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
22539 }
22540
22541 /**
22542 * Start install ownership change.
22543 *
22544 * @author Vova Feldman (@svovaf)
22545 * @since 1.1.1
22546 * @uses FS_Api
22547 *
22548 * @param string $candidate_email
22549 * @param string $transfer_type
22550 *
22551 * @return bool Is ownership change successfully initiated.
22552 */
22553 private function init_change_owner( $candidate_email, $transfer_type ) {
22554 $this->_logger->entrance();
22555
22556 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
22557 $install_ids = array();
22558
22559 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
22560 $install = $install_info['install'];
22561
22562 if ( $this->_user->id != $install->user_id ) {
22563 // Skip add-on installs that are not owned by the parent product's install's owner.
22564 continue;
22565 }
22566
22567 $install_ids[ $slug ] = $install->id;
22568 }
22569
22570 $api = $this->get_api_site_scope();
22571 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
22572 'email' => $candidate_email,
22573 'transfer_type' => $transfer_type,
22574 'install_ids' => implode( ',', array_values( $install_ids ) ),
22575 'after_confirm_url' => $this->_get_admin_page_url(
22576 'account',
22577 array( 'fs_action' => 'change_owner' )
22578 ),
22579 ) );
22580
22581 return ! $this->is_api_error( $result );
22582 }
22583
22584 /**
22585 * Handle install ownership change.
22586 *
22587 * @author Vova Feldman (@svovaf)
22588 * @since 1.1.1
22589 * @uses FS_Api
22590 *
22591 * @return bool Was ownership change successfully complete.
22592 */
22593 private function complete_change_owner() {
22594 $this->_logger->entrance();
22595
22596 $install_ids = fs_request_get( 'install_ids' );
22597
22598 if ( ! empty( $install_ids ) ) {
22599 $install_ids = explode( ',', $install_ids );
22600
22601 foreach ( $install_ids as $key => $install_id ) {
22602 if ( ! FS_Site::is_valid_id( $install_id ) ) {
22603 unset( $install_ids[ $key ] );
22604 }
22605 }
22606 }
22607
22608 if ( ! is_array( $install_ids ) ) {
22609 $install_ids = array();
22610 }
22611
22612 $user = new FS_User();
22613 $user->id = fs_request_get( 'user_id' );
22614 $user->public_key = fs_request_get_raw( 'user_public_key' );
22615 $user->secret_key = fs_request_get_raw( 'user_secret_key' );
22616
22617 $prev_user = $this->_user;
22618 $this->_user = $user;
22619
22620 $result = $this->get_api_user_scope( true )->get(
22621 "/installs.json?install_ids=" . implode( ',', $install_ids )
22622 );
22623
22624 $current_blog_sites = self::get_all_sites( $this->get_module_type() );
22625
22626 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22627 $site_id_slug_map = array();
22628
22629 foreach ( $current_blog_sites as $slug => $site ) {
22630 $site_id_slug_map[ $site->id ] = $slug;
22631 }
22632
22633 foreach ( $result->installs as $install ) {
22634 $site = new FS_Site( $install );
22635
22636 if ( ! isset( $site_id_slug_map[ $install->id ] ) ) {
22637 continue;
22638 }
22639
22640 $current_blog_sites[ $site_id_slug_map[ $install->id ] ] = clone $site;
22641
22642 if ( $this->_site->id == $site->id ) {
22643 $this->_site = $site;
22644 }
22645 }
22646 }
22647
22648 // Validate install's user and given user.
22649 if ( $user->id != $this->_site->user_id ) {
22650 $this->_user = $prev_user;
22651
22652 return false;
22653 }
22654
22655 $this->set_account_option( 'sites', $current_blog_sites, true );
22656
22657 // Fetch new user information.
22658 $user_result = $this->get_api_user_scope( true )->get();
22659 $user = new FS_User( $user_result );
22660 $this->_user = $user;
22661
22662 $this->_set_account( $user, $this->_site );
22663
22664 $remove_user = true;
22665 $all_modules_sites = FS_DebugManager::get_all_modules_sites();
22666
22667 foreach ( $all_modules_sites as $sites_by_module_type ) {
22668 foreach ( $sites_by_module_type as $sites_by_slug ) {
22669 foreach ( $sites_by_slug as $site ) {
22670 if ( $prev_user->id == $site->user_id ) {
22671 $remove_user = false;
22672 break;
22673 }
22674 }
22675
22676 if ( ! $remove_user ) {
22677 break;
22678 }
22679 }
22680
22681 if ( ! $remove_user ) {
22682 break;
22683 }
22684 }
22685
22686 if ( $remove_user ) {
22687 $users = self::get_all_users();
22688
22689 if ( isset( $users[ $prev_user->id ] ) ) {
22690 unset( $users[ $prev_user->id ] );
22691 } else {
22692 // If the prev user wasn't found by the key, iterate over the users collection.
22693 foreach ( $users as $key => $user ) {
22694 if ( $user->id == $prev_user->id ) {
22695 unset( $users[ $key ] );
22696 break;
22697 }
22698 }
22699 }
22700
22701 $this->set_account_option( 'users', $users, true );
22702 }
22703
22704 return true;
22705 }
22706
22707 /**
22708 * Completes ownership change by license.
22709 *
22710 * @author Leo Fajardo (@leorw)
22711 * @since 2.3.2
22712 *
22713 * @param number $user_id
22714 * @param array[string]number $install_ids_by_slug_map
22715 *
22716 */
22717 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
22718 $this->_logger->entrance();
22719
22720 $this->sync_user_by_current_install( $user_id );
22721
22722 $result = $this->get_api_user_scope( true )->get(
22723 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
22724 );
22725
22726 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22727 $sites = self::get_all_sites( $this->get_module_type() );
22728 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
22729
22730 foreach ( $result->installs as $install ) {
22731 $site = new FS_Site( $install );
22732
22733 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
22734 }
22735
22736 $this->set_account_option( 'sites', $sites, true );
22737 }
22738 }
22739
22740 /**
22741 * Handle user name update.
22742 *
22743 * @author Vova Feldman (@svovaf)
22744 * @since 1.0.9
22745 * @uses FS_Api
22746 *
22747 * @return object
22748 */
22749 private function update_user_name() {
22750 $this->_logger->entrance();
22751 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
22752
22753 $api = $this->get_api_user_scope();
22754 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
22755 'name' => $name,
22756 ) );
22757
22758 if ( ! isset( $user->error ) ) {
22759 $this->_user->first = $user->first;
22760 $this->_user->last = $user->last;
22761 $this->_store_user();
22762 } else {
22763 // handle different error cases.
22764
22765 }
22766
22767 return $user;
22768 }
22769
22770 /**
22771 * Verify user email.
22772 *
22773 * @author Vova Feldman (@svovaf)
22774 * @since 1.0.3
22775 * @uses FS_Api
22776 */
22777 private function verify_email() {
22778 $this->_handle_account_user_sync();
22779
22780 if ( $this->_user->is_verified() ) {
22781 return;
22782 }
22783
22784 $api = $this->get_api_site_scope();
22785 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
22786 'after_email_confirm_url' => $this->_get_admin_page_url(
22787 'account',
22788 array( 'fs_action' => 'sync_user' )
22789 )
22790 ) );
22791
22792 if ( ! isset( $result->error ) ) {
22793 $this->_admin_notices->add( sprintf(
22794 $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' ),
22795 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
22796 ) );
22797 } else {
22798 // handle different error cases.
22799
22800 }
22801 }
22802
22803 /**
22804 * @author Vova Feldman (@svovaf)
22805 * @since 1.1.2
22806 *
22807 * @param array $params
22808 * @param bool|null $network
22809 *
22810 * @return string
22811 */
22812 function get_activation_url( $params = array(), $network = null ) {
22813 if ( $this->is_addon() && $this->has_free_plan() ) {
22814 /**
22815 * @author Vova Feldman (@svovaf)
22816 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
22817 */
22818 return $this->get_parent_instance()->get_activation_url( $params );
22819 }
22820
22821 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
22822 }
22823
22824 /**
22825 * @author Vova Feldman (@svovaf)
22826 * @since 1.2.1.5
22827 *
22828 * @param array $params
22829 *
22830 * @return string
22831 */
22832 function get_reconnect_url( $params = array() ) {
22833 $params['fs_action'] = 'reset_anonymous_mode';
22834 $params['fs_unique_affix'] = $this->get_unique_affix();
22835
22836 return $this->get_activation_url( $params );
22837 }
22838
22839 /**
22840 * Get the URL of the page that should be loaded after the user connect
22841 * or skip in the opt-in screen.
22842 *
22843 * @author Vova Feldman (@svovaf)
22844 * @since 1.1.3
22845 *
22846 * @param string $filter Filter name.
22847 * @param array $params Since 1.2.2.7
22848 * @param bool|null $network
22849 *
22850 * @return string
22851 */
22852 function get_after_activation_url( $filter, $params = array(), $network = null ) {
22853 if ( $this->show_opt_in_on_themes_page() &&
22854 ( fs_request_has( 'pending_activation' ) ||
22855 // For cases when the first time path is set, even though it's a WP.org theme.
22856 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
22857 ) {
22858 $first_time_path = '';
22859 } else {
22860 $first_time_path = $this->_menu->get_first_time_path(
22861 fs_is_network_admin() && $this->_is_network_active
22862 );
22863 }
22864
22865 if ( $this->_is_network_active &&
22866 fs_is_network_admin() &&
22867 ! $this->_menu->has_network_menu() &&
22868 $this->is_network_registered()
22869 ) {
22870 $target_url = $this->get_account_url();
22871 } else {
22872 // Default plugin's page.
22873 $target_url = $this->_get_admin_page_url( '', array(), $network );
22874 }
22875
22876 return add_query_arg( $params, $this->apply_filters(
22877 $filter,
22878 empty( $first_time_path ) ?
22879 $target_url :
22880 $first_time_path
22881 ) );
22882 }
22883
22884 /**
22885 * Handle account page updates / edits / actions.
22886 *
22887 * @author Vova Feldman (@svovaf)
22888 * @since 1.0.2
22889 *
22890 */
22891 private function _handle_account_edits() {
22892 if ( ! $this->is_user_admin() ) {
22893 return;
22894 }
22895
22896 $action = fs_get_action();
22897
22898 if ( empty( $action ) ) {
22899 return;
22900 }
22901
22902 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
22903 $install_id = fs_request_get( 'install_id', '' );
22904
22905 // Alias.
22906 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
22907
22908 $is_network_action = $this->is_network_level_action();
22909 $blog_id = $this->is_network_level_site_specific_action();
22910 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
22911
22912 if ( is_numeric( $blog_id ) ) {
22913 $this->switch_to_blog( $blog_id );
22914 } else {
22915 $blog_id = '';
22916 }
22917
22918 switch ( $action ) {
22919 case 'opt_in':
22920 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22921
22922 if ( $is_parent_plugin_action ) {
22923 if ( $is_network_action && ! empty( $blog_id ) ) {
22924 if ( ! $this->is_registered() ) {
22925 $this->install_with_user(
22926 $this->get_network_user(),
22927 false,
22928 false,
22929 false,
22930 false
22931 );
22932
22933 $this->_admin_notices->add(
22934 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
22935 $this->get_text_inline( 'Awesome', 'awesome' )
22936 );
22937 }
22938 }
22939 }
22940 break;
22941
22942 case 'toggle_tracking':
22943 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22944
22945 if ( $is_parent_plugin_action ) {
22946 if ( $is_network_action && ! empty( $blog_id ) ) {
22947 if ( $this->is_registered( true ) ) {
22948 if ( $this->is_tracking_prohibited( $blog_id ) ) {
22949 if ( $this->toggle_site_tracking( true, $blog_id ) ) {
22950 $this->_admin_notices->add(
22951 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>" ),
22952 $this->get_text_inline( 'Thank you!', 'thank-you' )
22953 );
22954 }
22955 } else {
22956 if ( $this->toggle_site_tracking( false, $blog_id ) ) {
22957 $install = $this->get_install_by_blog_id( $blog_id );
22958
22959 $this->_admin_notices->add(
22960 sprintf(
22961 $this->get_text_inline( 'Diagnostic data will no longer be sent from %s to %s.', 'opted-out-successfully' ),
22962 self::get_unfiltered_site_url( $blog_id, true ),
22963 "<b>{$this->get_plugin_title()}</b>"
22964 )
22965 );
22966 }
22967 }
22968 }
22969 }
22970 }
22971
22972 break;
22973
22974 case 'delete_account':
22975 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22976
22977 $is_network_deletion = $is_network_action && empty( $blog_id );
22978
22979 if ( $is_parent_plugin_action ) {
22980 // Delete add-on installs if have any.
22981 $installed_addons = $this->get_installed_addons();
22982 foreach ( $installed_addons as $fs_addon ) {
22983 if ( $is_network_deletion ) {
22984 $fs_addon->delete_network_account_event();
22985 } else {
22986 $fs_addon->delete_account_event();
22987 }
22988 }
22989
22990 if ( $is_network_deletion ) {
22991 $this->delete_network_account_event();
22992 } else {
22993 $this->delete_account_event();
22994 }
22995
22996 // Clear user and site.
22997 $this->_site = null;
22998 $this->_user = null;
22999
23000 $this->maybe_set_slug_and_network_menu_exists_flag();
23001
23002 fs_redirect( $this->get_activation_url() );
23003 } else {
23004 if ( $this->is_addon_activated( $plugin_id ) ) {
23005 $fs_addon = self::get_instance_by_id( $plugin_id );
23006
23007 if ( $is_network_deletion ) {
23008 $fs_addon->delete_network_account_event();
23009 } else {
23010 $fs_addon->delete_account_event();
23011 }
23012
23013 fs_redirect( $this->_get_admin_page_url( 'account' ) );
23014 }
23015 }
23016
23017 return;
23018
23019 case 'downgrade_account':
23020 if ( is_numeric( $blog_id ) ) {
23021 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23022 } else {
23023 check_admin_referer( $action );
23024 }
23025
23026 $switch_to_network_install_blog_after_cancellation = (
23027 is_numeric( $blog_id ) &&
23028 $plugin_id == $this->get_id() &&
23029 ! $this->is_trial()
23030 );
23031
23032 $result = $this->cancel_subscription_or_trial( $plugin_id );
23033 if ( $this->is_api_error( $result ) ) {
23034 $this->_admin_notices->add(
23035 $result->error->message,
23036 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23037 'error'
23038 );
23039 }
23040
23041 if ( $switch_to_network_install_blog_after_cancellation ) {
23042 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23043 }
23044
23045 return;
23046
23047 case 'activate_license':
23048 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23049
23050 $fs = $this;
23051 if ( $plugin_id != $this->get_id() ) {
23052 $fs = $this->is_addon_activated( $plugin_id ) ?
23053 self::get_instance_by_id( $plugin_id ) :
23054 null;
23055 }
23056
23057 if ( is_object( $fs ) ) {
23058 $fs->_activate_license();
23059
23060 /**
23061 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
23062 *
23063 * @author Leo Fajardo (@leorw)
23064 * @since 2.4.0
23065 */
23066 unset( $_REQUEST['plugin_id'] );
23067
23068 if ( $this->is_bundle_license_auto_activation_enabled() ) {
23069 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
23070 }
23071 }
23072
23073 return;
23074
23075 case 'deactivate_license':
23076 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23077
23078 if ( $plugin_id == $this->get_id() ) {
23079 $this->_deactivate_license();
23080
23081 if ( $this->is_only_premium() ) {
23082 // Clear user and site.
23083 $this->_site = null;
23084 $this->_user = null;
23085
23086 if ( ! $is_network_action ) {
23087 fs_redirect( $this->get_activation_url() );
23088 } else if ( is_numeric( $blog_id ) ) {
23089 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23090 }
23091 }
23092 } else {
23093 if ( $this->is_addon_activated( $plugin_id ) ) {
23094 $fs_addon = self::get_instance_by_id( $plugin_id );
23095 $fs_addon->_deactivate_license();
23096 }
23097 }
23098
23099 return;
23100
23101 case 'check_updates':
23102 check_admin_referer( $action );
23103 $this->check_updates();
23104
23105 return;
23106
23107 case 'change_owner':
23108 $state = fs_request_get( 'state', 'init' );
23109 switch ( $state ) {
23110 case 'init':
23111 // The nonce is injected by the error handler in `_email_address_update_ajax_handler` function.
23112 check_admin_referer( 'change_owner' );
23113
23114 $candidate_email = fs_request_get( 'candidate_email' );
23115 $transfer_type = fs_request_get( 'transfer_type' );
23116
23117 if ( $this->init_change_owner( $candidate_email, $transfer_type ) ) {
23118 if ( 'transfer' === $transfer_type ) {
23119 $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>' ) );
23120 } else {
23121 $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>' ) );
23122 }
23123 }
23124 break;
23125 case 'owner_confirmed':
23126 // We cannot (or need not to) check the nonce and referer here, because the link comes from the email sent by our API.
23127 $candidate_email = fs_request_get( 'candidate_email', '' );
23128
23129 if ( ! is_email($candidate_email ) ) {
23130 return;
23131 }
23132
23133 $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>' ) );
23134 break;
23135 case 'candidate_confirmed':
23136 // We do not need to validate the authenticity of this request here, because the `complete_change_owner` does that for us through API calls.
23137 if ( $this->complete_change_owner() ) {
23138 $this->_admin_notices->add_sticky(
23139 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
23140 'ownership_changed',
23141 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
23142 );
23143 } else {
23144 // @todo Handle failed ownership change message.
23145 }
23146 break;
23147 }
23148
23149 return;
23150
23151 case 'update_user_name':
23152 check_admin_referer( 'update_user_name' );
23153
23154 $result = $this->update_user_name();
23155
23156 if ( isset( $result->error ) ) {
23157 $this->_admin_notices->add(
23158 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
23159 $oops_text,
23160 'error'
23161 );
23162 } else {
23163 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
23164 }
23165
23166 return;
23167
23168 #region Actions that might be called from external links (e.g. email)
23169
23170 /**
23171 * !!IMPORTANT!!: We cannot check for a valid nonce in this region, because the links could be coming from emails.
23172 */
23173
23174 case 'cancel_trial':
23175 $result = $this->cancel_subscription_or_trial( $plugin_id );
23176 if ( $this->is_api_error( $result ) ) {
23177 $this->_admin_notices->add(
23178 $result->error->message,
23179 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23180 'error'
23181 );
23182 }
23183
23184 return;
23185
23186 case 'verify_email':
23187 $this->verify_email();
23188
23189 return;
23190
23191 case 'sync_user':
23192 $this->_handle_account_user_sync();
23193
23194 return;
23195
23196 case $this->get_unique_affix() . '_sync_license':
23197 $this->_sync_license();
23198
23199 return;
23200
23201 case 'download_latest':
23202 $this->download_latest_directly( $plugin_id );
23203
23204 return;
23205
23206 #endregion
23207 }
23208
23209 if ( WP_FS__IS_POST_REQUEST ) {
23210 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
23211 foreach ( $properties as $p ) {
23212 if ( 'update_' . $p === $action ) {
23213 check_admin_referer( $action );
23214
23215 $this->_logger->log( $action );
23216
23217 $site_property = substr( $p, strlen( 'site_' ) );
23218 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
23219 $this->get_site()->{$site_property} = $site_property_value;
23220
23221 // Store account after modification.
23222 $this->_store_site();
23223
23224 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
23225
23226 $this->_admin_notices->add( sprintf(
23227 /* translators: %s: User's account property (e.g. email address, name) */
23228 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
23229 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
23230 ) );
23231
23232 return;
23233 }
23234 }
23235 }
23236 }
23237
23238 /**
23239 * Adds CSS classes for the body tag in the admin.
23240 *
23241 * @param string $classes Space-separated string of class names.
23242 *
23243 * @return string $classes FS Admin body tag class names.
23244 */
23245 public function fs_addons_body_class( $classes ) {
23246 $classes .= ' plugins-php';
23247 return $classes;
23248 }
23249
23250 /**
23251 * Account page resources load.
23252 *
23253 * @author Vova Feldman (@svovaf)
23254 * @since 1.0.6
23255 */
23256 function _account_page_load() {
23257 $this->_logger->entrance();
23258
23259 $this->_logger->info( var_export( $_REQUEST, true ) );
23260
23261 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
23262
23263 if ( $this->has_addons() ) {
23264 wp_enqueue_script( 'plugin-install' );
23265 add_thickbox();
23266 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23267 }
23268
23269 if ( $this->has_paid_plan() &&
23270 ! $this->has_any_license() &&
23271 ! $this->is_sync_executed() &&
23272 $this->is_tracking_allowed()
23273 ) {
23274 /**
23275 * If no licenses found and no sync job was executed during the last 24 hours,
23276 * just execute the sync job right away (blocking execution).
23277 *
23278 * @since 1.1.7.3
23279 */
23280 $this->run_manual_sync();
23281 }
23282
23283 $this->_handle_account_edits();
23284
23285 if (
23286 is_object( $this->_license ) &&
23287 $this->_license->user_id == $this->_user->id &&
23288 ! $this->is_whitelabeled( true )
23289 ) {
23290 $this->_admin_notices->add(
23291 sprintf(
23292 $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' ),
23293 sprintf(
23294 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
23295 $this->get_text_inline( 'Click here', 'click-here' )
23296 )
23297 ),
23298 '',
23299 'success',
23300 false,
23301 'license_not_whitelabeled'
23302 );
23303 }
23304
23305 $this->do_action( 'account_page_load_before_departure' );
23306 }
23307
23308 /**
23309 * Renders the "Affiliation" page.
23310 *
23311 * @author Leo Fajardo (@leorw)
23312 * @since 1.2.3
23313 */
23314 function _affiliation_page_render() {
23315 $this->_logger->entrance();
23316
23317 $this->fetch_affiliate_and_terms();
23318
23319 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
23320
23321 $is_bundle_context = $this->has_bundle_context();
23322
23323 $plugin_title = $this->get_plugin_title();
23324
23325 if ( $is_bundle_context ) {
23326 $plugin_title = $this->plugin_affiliate_terms->plugin_title;
23327
23328 // Add the suffix "Bundle" only if the word is not present in the title itself.
23329 if ( false === mb_stripos( $plugin_title, fs_text_inline( 'Bundle', 'bundle' ) ) ) {
23330 $plugin_title = $this->apply_filters(
23331 'formatted_bundle_title',
23332 $plugin_title . ' ' . fs_text_inline( 'Bundle', 'bundle' )
23333 );
23334 }
23335 }
23336
23337 $vars = array(
23338 'id' => $this->_module_id,
23339 'plugin_title' => $plugin_title,
23340 );
23341 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
23342 }
23343
23344
23345 /**
23346 * Render account page.
23347 *
23348 * @author Vova Feldman (@svovaf)
23349 * @since 1.0.0
23350 */
23351 function _account_page_render() {
23352 $this->_logger->entrance();
23353
23354 $template = 'account.php';
23355 $vars = array( 'id' => $this->_module_id );
23356
23357 /**
23358 * Added filter to the template to allow developers wrapping the template
23359 * in custom HTML (e.g. within a wizard/tabs).
23360 *
23361 * @author Vova Feldman (@svovaf)
23362 * @since 1.2.1.6
23363 */
23364 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
23365 }
23366
23367 /**
23368 * Render account connect page.
23369 *
23370 * @author Vova Feldman (@svovaf)
23371 * @since 1.0.7
23372 */
23373 function _connect_page_render() {
23374 $this->_logger->entrance();
23375
23376 $vars = array( 'id' => $this->_module_id );
23377
23378 /**
23379 * Added filter to the template to allow developers wrapping the template
23380 * in custom HTML (e.g. within a wizard/tabs).
23381 *
23382 * @author Vova Feldman (@svovaf)
23383 * @since 1.2.1.6
23384 */
23385 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
23386 }
23387
23388 /**
23389 * Load required resources before add-ons page render.
23390 *
23391 * @author Vova Feldman (@svovaf)
23392 * @since 1.0.6
23393 */
23394 function _addons_page_load() {
23395 $this->_logger->entrance();
23396
23397 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
23398
23399 wp_enqueue_script( 'plugin-install' );
23400 add_thickbox();
23401 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23402
23403 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
23404 $this->_admin_notices->add(
23405 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>' ),
23406 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
23407 'update-nag'
23408 );
23409 }
23410 }
23411
23412 /**
23413 * Render add-ons page.
23414 *
23415 * @author Vova Feldman (@svovaf)
23416 * @since 1.0.6
23417 */
23418 function _addons_page_render() {
23419 $this->_logger->entrance();
23420
23421 $vars = array( 'id' => $this->_module_id );
23422
23423 /**
23424 * Added filter to the template to allow developers wrapping the template
23425 * in custom HTML (e.g. within a wizard/tabs).
23426 *
23427 * @author Vova Feldman (@svovaf)
23428 * @since 1.2.1.6
23429 */
23430 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
23431 }
23432
23433 /* Pricing & Upgrade
23434 ------------------------------------------------------------------------------------------------------------------*/
23435 /**
23436 * Render pricing page.
23437 *
23438 * @author Vova Feldman (@svovaf)
23439 * @since 1.0.0
23440 */
23441 function _pricing_page_render() {
23442 $this->_logger->entrance();
23443
23444 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
23445 fs_enqueue_local_style( 'fs_checkout', '/admin/checkout.css' );
23446
23447 $vars = array( 'id' => $this->_module_id );
23448
23449 if ( 'true' === fs_request_get( 'checkout', false ) ) {
23450 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
23451 } else {
23452 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
23453 }
23454 }
23455
23456 /**
23457 * @author Leo Fajardo (@leorw)
23458 * @since 2.3.1
23459 */
23460 function _add_pricing_ajax_handler() {
23461 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
23462 }
23463
23464 /**
23465 * @author Leo Fajardo (@leorw)
23466 * @since 2.3.1
23467 */
23468 function _fs_pricing_ajax_action_handler() {
23469 $this->check_ajax_referer( 'pricing_ajax_action' );
23470
23471 $result = null;
23472 $pricing_action = fs_request_get( 'pricing_action' );
23473
23474 switch ( $pricing_action ) {
23475 case 'fetch_pricing_data':
23476 $params = array(
23477 'is_enriched' => true,
23478 'trial' => fs_request_get_bool( 'trial' ),
23479 'sandbox' => fs_request_get_raw( 'sandbox' ),
23480 's_ctx_type' => fs_request_get_raw( 's_ctx_type' ),
23481 's_ctx_id' => fs_request_get_raw( 's_ctx_id' ),
23482 's_ctx_ts' => fs_request_get_raw( 's_ctx_ts' ),
23483 's_ctx_secure' => fs_request_get_raw( 's_ctx_secure' ),
23484 );
23485
23486 $bundle_id = $this->get_bundle_id();
23487 $bundle_public_key = $this->get_bundle_public_key();
23488
23489 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
23490
23491 if ( ! $has_bundle_context ) {
23492 $api = $this->get_api_plugin_scope();
23493 } else {
23494 $api = FS_Api::instance(
23495 $bundle_id,
23496 'plugin',
23497 $bundle_id,
23498 $bundle_public_key,
23499 ! $this->is_live(),
23500 false,
23501 $this->get_sdk_version()
23502 );
23503
23504 $params['plugin_id'] = $this->get_id();
23505 $params['plugin_public_key'] = $this->get_public_key();
23506 }
23507
23508 $result = $api->get( 'pricing.json?' . http_build_query( $params ) );
23509 break;
23510 case 'start_trial':
23511 $trial_plan_id = fs_request_get( 'plan_id' );
23512
23513 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
23514 $plan = $this->_get_plan_by_id( $trial_plan_id );
23515
23516 if ( ! $plan ) {
23517 $this->shoot_ajax_failure( 'Invalid plan ID.' );
23518 return;
23519 }
23520
23521 $result = $this->start_trial( $plan->name, true );
23522 } else {
23523 // @todo - This fails for sandbox trial at the moment if the trial was already utilized.
23524 $result = $this->opt_in(
23525 false,
23526 false,
23527 false,
23528 false,
23529 false,
23530 $trial_plan_id
23531 );
23532 }
23533 }
23534
23535 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
23536 $this->_logger->api_error( $result );
23537
23538 self::shoot_ajax_failure(
23539 isset( $result->error ) ?
23540 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
23541 var_export( $result, true )
23542 );
23543 }
23544
23545 $this->shoot_ajax_success( $result );
23546 }
23547
23548 #----------------------------------------------------------------------------------
23549 #region Contact Us
23550 #----------------------------------------------------------------------------------
23551
23552 /**
23553 * Render contact-us page.
23554 *
23555 * @author Vova Feldman (@svovaf)
23556 * @since 1.0.3
23557 */
23558 function _contact_page_render() {
23559 $this->_logger->entrance();
23560
23561 $vars = array( 'id' => $this->_module_id );
23562
23563 /**
23564 * Added filter to the template to allow developers wrapping the template
23565 * in custom HTML (e.g. within a wizard/tabs).
23566 *
23567 * @author Vova Feldman (@svovaf)
23568 * @since 2.1.3
23569 */
23570 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
23571 }
23572
23573 #endregion ------------------------------------------------------------------------
23574
23575 /**
23576 * Hide all admin notices to prevent distractions.
23577 *
23578 * @author Vova Feldman (@svovaf)
23579 * @since 1.0.3
23580 *
23581 * @uses remove_all_actions()
23582 */
23583 private static function _hide_admin_notices() {
23584 remove_all_actions( 'admin_notices' );
23585 remove_all_actions( 'network_admin_notices' );
23586 remove_all_actions( 'all_admin_notices' );
23587 remove_all_actions( 'user_admin_notices' );
23588 }
23589
23590 static function _clean_admin_content_section_hook() {
23591 $hide_admin_notices = true;
23592
23593 if ( fs_request_is_action( 'allow_clone_resolution_notice' ) ) {
23594 check_admin_referer( 'fs_allow_clone_resolution_notice' );
23595
23596 $hide_admin_notices = false;
23597 }
23598
23599 if ( $hide_admin_notices ) {
23600 self::_hide_admin_notices();
23601 }
23602
23603 // Hide footer.
23604 echo '<style>#wpfooter { display: none !important; }</style>';
23605 }
23606
23607 /**
23608 * Attach to admin_head hook to hide all admin notices.
23609 *
23610 * @author Vova Feldman (@svovaf)
23611 * @since 1.0.3
23612 */
23613 static function _clean_admin_content_section() {
23614 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
23615 }
23616
23617 /* CSS & JavaScript
23618 ------------------------------------------------------------------------------------------------------------------*/
23619 /* function _enqueue_script($handle, $src) {
23620 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
23621
23622 $this->_logger->entrance( 'script = ' . $url );
23623
23624 wp_enqueue_script( $handle, $url );
23625 }*/
23626
23627 /* SDK
23628 ------------------------------------------------------------------------------------------------------------------*/
23629 private $_user_api;
23630
23631 /**
23632 *
23633 * @author Vova Feldman (@svovaf)
23634 * @since 1.0.2
23635 *
23636 * @param bool $flush
23637 *
23638 * @return FS_Api
23639 */
23640 function get_api_user_scope( $flush = false ) {
23641 if ( ! isset( $this->_user_api ) || $flush ) {
23642 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
23643 }
23644
23645 return $this->_user_api;
23646 }
23647
23648 /**
23649 * @author Vova Feldman (@svovaf)
23650 * @since 2.0.0
23651 *
23652 * @param \FS_User $user
23653 *
23654 * @return \FS_Api
23655 */
23656 private function get_api_user_scope_by_user( FS_User $user ) {
23657 return FS_Api::instance(
23658 $this->_module_id,
23659 'user',
23660 $user->id,
23661 $user->public_key,
23662 ! $this->is_live(),
23663 $user->secret_key,
23664 $this->get_sdk_version()
23665 );
23666 }
23667
23668 /**
23669 *
23670 * @author Leo Fajardo (@leorw)
23671 * @since 2.0.0
23672 *
23673 * @param bool $flush
23674 *
23675 * @return FS_Api
23676 */
23677 private function get_current_or_network_user_api_scope( $flush = false ) {
23678 if ( ! $this->_is_network_active ||
23679 ( isset( $this->_user ) && $this->_user instanceof FS_User )
23680 ) {
23681 return $this->get_api_user_scope( $flush );
23682 }
23683
23684 $user = $this->get_current_or_network_user();
23685
23686 $this->_user_api = FS_Api::instance(
23687 $this->_module_id,
23688 'user',
23689 $user->id,
23690 $user->public_key,
23691 ! $this->is_live(),
23692 $user->secret_key,
23693 $this->get_sdk_version()
23694 );
23695
23696 return $this->_user_api;
23697 }
23698
23699 private $_site_api;
23700
23701 /**
23702 *
23703 * @author Vova Feldman (@svovaf)
23704 * @since 1.0.2
23705 *
23706 * @param bool $flush
23707 *
23708 * @return FS_Api
23709 */
23710 private function get_api_site_scope( $flush = false ) {
23711 if ( ! isset( $this->_site_api ) || $flush ) {
23712 $this->_site_api = FS_Api::instance(
23713 $this->_module_id,
23714 'install',
23715 $this->_site->id,
23716 $this->_site->public_key,
23717 ! $this->is_live(),
23718 $this->_site->secret_key,
23719 $this->get_sdk_version(),
23720 self::get_unfiltered_site_url()
23721 );
23722 }
23723
23724 return $this->_site_api;
23725 }
23726
23727 /**
23728 * @author Leo Fajardo (@leorw)
23729 * @since 2.5.0
23730 *
23731 * @param string $path
23732 * @param string $method
23733 * @param array $params
23734 * @param bool $flush_instance
23735 *
23736 * @return array|mixed|string|void
23737 * @throws Freemius_Exception
23738 */
23739 private function api_site_call( $path, $method = 'GET', $params = array(), $flush_instance = false ) {
23740 $result = $this->get_api_site_scope( $flush_instance )->call( $path, $method, $params );
23741
23742 /**
23743 * 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.
23744 *
23745 * @author Leo Fajardo (@leorw)
23746 * @since 2.5.0
23747 */
23748 if (
23749 $this->is_registered() &&
23750 FS_Api::is_api_result_entity( $result ) &&
23751 isset( $result->url )
23752 ) {
23753 $stored_local_url = trailingslashit( $this->_site->url );
23754 $stored_remote_url = trailingslashit( $result->url );
23755
23756 if ( $stored_local_url !== $stored_remote_url ) {
23757 $this->_site->url = $result->url;
23758 $this->_store_site();
23759 }
23760
23761 if ( fs_strip_url_protocol( $stored_remote_url ) !== self::get_unfiltered_site_url( null, true, true ) ) {
23762 FS_Clone_Manager::instance()->maybe_run_clone_resolution();
23763 }
23764 }
23765
23766 return $result;
23767 }
23768
23769 private $_plugin_api;
23770
23771 /**
23772 * Get plugin public API scope.
23773 *
23774 * @author Vova Feldman (@svovaf)
23775 * @since 1.0.7
23776 *
23777 * @return FS_Api
23778 */
23779 function get_api_plugin_scope() {
23780 if ( ! isset( $this->_plugin_api ) ) {
23781 $this->_plugin_api = FS_Api::instance(
23782 $this->_module_id,
23783 'plugin',
23784 $this->_plugin->id,
23785 $this->_plugin->public_key,
23786 ! $this->is_live(),
23787 false,
23788 $this->get_sdk_version()
23789 );
23790 }
23791
23792 return $this->_plugin_api;
23793 }
23794
23795 /**
23796 * Get bundle public API scope.
23797 *
23798 * @author Vova Feldman (@svovaf)
23799 * @since 2.3.1
23800 *
23801 * @return FS_Api
23802 */
23803 function get_api_bundle_scope() {
23804 return FS_Api::instance(
23805 $this->get_bundle_id(),
23806 'plugin',
23807 $this->get_bundle_id(),
23808 $this->get_bundle_public_key(),
23809 ! $this->is_live(),
23810 false,
23811 $this->get_sdk_version()
23812 );
23813 }
23814
23815 /**
23816 * Get site API scope object (fallback to public plugin scope when not registered).
23817 *
23818 * @author Vova Feldman (@svovaf)
23819 * @since 1.0.7
23820 *
23821 * @return FS_Api
23822 */
23823 function get_api_site_or_plugin_scope() {
23824 return $this->is_registered() ?
23825 $this->get_api_site_scope() :
23826 $this->get_api_plugin_scope();
23827 }
23828
23829 /**
23830 * @author Leo Fajardo (@leorw)
23831 * @since 2.2.3.1
23832 *
23833 * @param object $result
23834 */
23835 private function maybe_modify_api_curl_error_message( $result ) {
23836 if (
23837 'cUrlMissing' !== $result->error->type &&
23838 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
23839 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
23840 ) {
23841 return;
23842 }
23843
23844 $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' ) .
23845 ' ' .
23846 $this->esc_html_inline(
23847 sprintf(
23848 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
23849 implode(
23850 ', ',
23851 $this->apply_filters( 'api_domains', array(
23852 'api.freemius.com',
23853 'wp.freemius.com'
23854 ) )
23855 )
23856 ),
23857 'connectivity-whitelist'
23858 ) .
23859 ' ' .
23860 sprintf(
23861 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
23862 $this->get_module_type()
23863 );
23864 }
23865
23866 /**
23867 * Show trial promotional notice (if any trial exist).
23868 *
23869 * @author Vova Feldman (@svovaf)
23870 * @since 1.0.9
23871 *
23872 * @param FS_Plugin_Plan[] $plans
23873 */
23874 function _check_for_trial_plans( $plans ) {
23875 /**
23876 * 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.
23877 *
23878 * @author Vova Feldman (@svovaf)
23879 * @since 2.1.2
23880 */
23881 if ( ! is_array( $plans ) && is_object( $plans ) ) {
23882 $plans = array( $plans );
23883 }
23884
23885 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
23886 $plans = array();
23887 }
23888
23889 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
23890 }
23891
23892 /**
23893 * During trial promotion the "upgrade" submenu item turns to
23894 * "start trial" to encourage the trial. Since we want to keep
23895 * the same menu item handler and there's no robust way to
23896 * add new arguments to the menu item link's querystring,
23897 * use JavaScript to find the menu item and update the href of
23898 * the link.
23899 *
23900 * @author Vova Feldman (@svovaf)
23901 * @since 1.2.1.5
23902 */
23903 function _fix_start_trial_menu_item_url() {
23904 $template_args = array( 'id' => $this->_module_id );
23905 fs_require_template( 'add-trial-to-pricing.php', $template_args );
23906 }
23907
23908 /**
23909 * Check if module is currently in a trial promotion mode.
23910 *
23911 * @author Vova Feldman (@svovaf)
23912 * @since 1.2.2.7
23913 *
23914 * @return bool
23915 */
23916 function is_in_trial_promotion() {
23917 return $this->_admin_notices->has_sticky( 'trial_promotion' );
23918 }
23919
23920 /**
23921 * Show trial promotional notice (if any trial exist).
23922 *
23923 * @author Vova Feldman (@svovaf)
23924 * @since 1.0.9
23925 *
23926 * @return bool If trial notice added.
23927 */
23928 function _add_trial_notice() {
23929 if ( ! $this->is_user_admin() ) {
23930 return false;
23931 }
23932
23933 if ( ! $this->is_user_in_admin() ) {
23934 return false;
23935 }
23936
23937 if ( $this->_is_network_active ) {
23938 if ( fs_is_network_admin() ) {
23939 // Network level trial is disabled at the moment.
23940 return false;
23941 }
23942
23943 if ( ! $this->is_delegated_connection() ) {
23944 // Only delegated sites should support trials.
23945 return false;
23946 }
23947 }
23948
23949 // Check if trial message is already shown.
23950 if ( $this->is_in_trial_promotion() ) {
23951 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
23952
23953 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
23954
23955 return false;
23956 }
23957
23958 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
23959 // Don't show trial if running the premium code, unless running in DEV mode.
23960 return false;
23961 }
23962
23963 if ( ! $this->has_trial_plan() ) {
23964 // No plans with trial.
23965 return false;
23966 }
23967
23968 if ( ! $this->apply_filters( 'show_trial', true ) ) {
23969 // Developer explicitly asked not to show the trial promo.
23970 return false;
23971 }
23972
23973 if ( $this->is_registered() ) {
23974 // Check if trial already utilized.
23975 if ( $this->_site->is_trial_utilized() ) {
23976 return false;
23977 }
23978
23979 if ( $this->is_paying_or_trial() ) {
23980 // Don't show trial if paying or already in trial.
23981 return false;
23982 }
23983 }
23984
23985 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
23986 // If not yet opted-in/skipped, or pending activation, don't show trial.
23987 return false;
23988 }
23989
23990 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
23991 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
23992
23993 // Show promotion if never shown before and 24 hours after initial activation with FS.
23994 if ( ! $was_promotion_shown_before &&
23995 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
23996 ) {
23997 return false;
23998 }
23999
24000 // OR if promotion was shown before, try showing it every 30 days.
24001 if ( $was_promotion_shown_before &&
24002 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
24003 ) {
24004 return false;
24005 }
24006
24007 $trial_period = $this->_trial_days;
24008 $require_payment = $this->_is_trial_require_payment;
24009 $trial_url = $this->get_trial_url();
24010 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
24011
24012 if ( $this->is_registered() ) {
24013 // If opted-in, override trial with up to date data from API.
24014 $trial_plans = FS_Plan_Manager::instance()->get_visible_trial_plans( $this->_plans );
24015 $trial_plans_count = count( $trial_plans );
24016
24017 if ( 0 === $trial_plans_count ) {
24018 // If there's no plans with a trial just exit.
24019 return false;
24020 }
24021
24022 /**
24023 * @var FS_Plugin_Plan $paid_plan
24024 */
24025 $paid_plan = $trial_plans[0];
24026 $require_payment = $paid_plan->is_require_subscription;
24027 $trial_period = $paid_plan->trial_period;
24028
24029 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
24030
24031 if ( $total_paid_plans !== $trial_plans_count ) {
24032 // Not all paid plans have a trial - generate a string of those that have it.
24033 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
24034 $plans_string .= sprintf(
24035 ' <a href="%s">%s</a>',
24036 $trial_url,
24037 $trial_plans[ $i ]->title
24038 );
24039
24040 if ( $i < $trial_plans_count - 2 ) {
24041 $plans_string .= ', ';
24042 } else if ( $i == $trial_plans_count - 2 ) {
24043 $plans_string .= ' and ';
24044 }
24045 }
24046 }
24047 }
24048
24049 $message = sprintf(
24050 $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' ),
24051 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
24052 $plans_string,
24053 $trial_period
24054 );
24055
24056 // "No Credit-Card Required" or "No Commitment for N Days".
24057 $cc_string = $require_payment ?
24058 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
24059 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
24060
24061
24062 // Start trial button.
24063 $button = ' ' . sprintf(
24064 '<div><a class="button button-primary" href="%s">%s &nbsp;&#10140;</a></div>',
24065 $trial_url,
24066 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
24067 );
24068
24069 $message_text = $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string}" );
24070
24071 $this->_admin_notices->add_sticky(
24072 "<div class=\"fs-trial-message-container\"><div>{$message_text}</div> {$button}</div>",
24073 'trial_promotion',
24074 '',
24075 'promotion'
24076 );
24077
24078 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
24079
24080 return true;
24081 }
24082
24083 /**
24084 * Lets users/customers know that the product has an affiliate program.
24085 *
24086 * @author Leo Fajardo (@leorw)
24087 * @since 1.2.2.11
24088 *
24089 * @return bool Returns true if the notice has been added.
24090 */
24091 function _add_affiliate_program_notice() {
24092 if ( ! $this->is_user_admin() ) {
24093 return false;
24094 }
24095
24096 if ( ! $this->is_user_in_admin() ) {
24097 return false;
24098 }
24099
24100 // Check if the notice is already shown.
24101 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
24102 return false;
24103 }
24104
24105 if (
24106 // Product has no affiliate program.
24107 ! $this->has_affiliate_program() ||
24108 // User has applied for an affiliate account.
24109 ! empty( $this->_storage->affiliate_application_data )
24110 ) {
24111 return false;
24112 }
24113
24114 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
24115 // Developer explicitly asked not to show the notice about the affiliate program.
24116 return false;
24117 }
24118
24119 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24120 // If not yet opted in/skipped, or pending activation, don't show the notice.
24121 return false;
24122 }
24123
24124 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
24125 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
24126
24127 /**
24128 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
24129 * activation with FS.
24130 */
24131 if ( $was_notice_shown_before ||
24132 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
24133 ) {
24134 return false;
24135 }
24136
24137 if ( ! $this->is_paying() &&
24138 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
24139 ) {
24140 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
24141 return false;
24142 }
24143
24144 $message = sprintf(
24145 $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' ),
24146 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
24147 $this->get_module_label( true )
24148 );
24149
24150 // HTML code for the "Learn more..." button.
24151 $button = ' ' . sprintf(
24152 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24153 $this->_get_admin_page_url( 'affiliation' ),
24154 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
24155 );
24156
24157 $this->_admin_notices->add_sticky(
24158 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
24159 'affiliate_program',
24160 '',
24161 'promotion'
24162 );
24163
24164 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
24165
24166 return true;
24167 }
24168
24169 /**
24170 * @author Vova Feldman (@svovaf)
24171 * @since 1.2.1.5
24172 */
24173 function _enqueue_common_css() {
24174 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
24175 // Add basic CSS for admin-notices and menu-item colors.
24176 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
24177 }
24178 }
24179
24180 /**
24181 * @author Leo Fajardo (@leorw)
24182 * @since 1.2.2
24183 */
24184 function _show_theme_activation_optin_dialog() {
24185 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
24186
24187 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
24188 }
24189
24190 /**
24191 * @author Leo Fajardo (@leorw)
24192 * @since 1.2.2
24193 */
24194 function _add_fs_theme_activation_dialog() {
24195 global $pagenow;
24196
24197 if ( 'themes.php' !== $pagenow ) {
24198 return;
24199 }
24200
24201 $vars = array( 'id' => $this->_module_id );
24202 fs_require_once_template( 'connect.php', $vars );
24203 }
24204
24205 /* Action Links
24206 ------------------------------------------------------------------------------------------------------------------*/
24207 private $_action_links_hooked = false;
24208 private $_action_links = array();
24209
24210 /**
24211 * Hook to plugin action links filter.
24212 *
24213 * @author Vova Feldman (@svovaf)
24214 * @since 1.0.0
24215 */
24216 private function hook_plugin_action_links() {
24217 $this->_logger->entrance();
24218
24219 $this->_action_links_hooked = true;
24220
24221 $this->_logger->log( 'Adding action links hooks.' );
24222
24223 // Add action link to settings page.
24224 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
24225 &$this,
24226 '_modify_plugin_action_links_hook'
24227 ), WP_FS__DEFAULT_PRIORITY, 2 );
24228 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
24229 &$this,
24230 '_modify_plugin_action_links_hook'
24231 ), WP_FS__DEFAULT_PRIORITY, 2 );
24232 }
24233
24234 /**
24235 * Add plugin action link.
24236 *
24237 * @author Vova Feldman (@svovaf)
24238 * @since 1.0.0
24239 *
24240 * @param $label
24241 * @param $url
24242 * @param bool $external
24243 * @param int $priority
24244 * @param bool $key
24245 */
24246 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
24247 $this->_logger->entrance();
24248
24249 if ( ! isset( $this->_action_links[ $priority ] ) ) {
24250 $this->_action_links[ $priority ] = array();
24251 }
24252
24253 if ( false === $key ) {
24254 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
24255 }
24256
24257 $this->_action_links[ $priority ][] = array(
24258 'label' => $label,
24259 'href' => $url,
24260 'key' => $key,
24261 'external' => $external
24262 );
24263 }
24264
24265 /**
24266 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
24267 *
24268 * @author Vova Feldman (@svovaf)
24269 * @since 1.0.0
24270 */
24271 function _add_upgrade_action_link() {
24272 $this->_logger->entrance();
24273
24274 $is_activation_mode = $this->is_activation_mode();
24275
24276 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
24277
24278 /**
24279 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
24280 * and "Add-Ons" menus should be added.
24281 *
24282 * @author Leo Fajardo (@leorw)
24283 * @since 2.3.0
24284 */
24285 $add_upgrade_link = (
24286 $add_action_links ||
24287 ( $is_activation_mode && $this->is_only_premium() )
24288 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
24289
24290 $add_addons_link = ( $add_action_links && $this->has_addons() );
24291
24292 if ( ! $add_upgrade_link && ! $add_addons_link ) {
24293 return;
24294 }
24295
24296 if (
24297 $add_upgrade_link &&
24298 $this->is_pricing_page_visible() &&
24299 $this->is_submenu_item_visible( 'pricing' )
24300 ) {
24301 $this->add_plugin_action_link(
24302 $this->get_text_inline( 'Upgrade', 'upgrade' ),
24303 $this->get_upgrade_url(),
24304 false,
24305 7,
24306 'upgrade'
24307 );
24308 }
24309
24310 if (
24311 $add_addons_link &&
24312 $this->has_addons() &&
24313 $this->is_submenu_item_visible( 'addons' )
24314 ) {
24315 $this->add_plugin_action_link(
24316 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
24317 $this->_get_admin_page_url( 'addons' ),
24318 false,
24319 9,
24320 'addons'
24321 );
24322 }
24323 }
24324
24325 /**
24326 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
24327 *
24328 * @author Leo Fajardo (@leorw)
24329 * @since 1.1.9
24330 */
24331 function _add_license_action_link() {
24332 $this->_logger->entrance();
24333
24334 if ( ! self::is_ajax() ) {
24335 // Inject license activation dialog UI and client side code.
24336 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
24337 }
24338
24339 $link_text = $this->is_free_plan() ?
24340 $this->get_text_inline( 'Activate License', 'activate-license' ) :
24341 $this->get_text_inline( 'Change License', 'change-license' );
24342
24343 $this->add_plugin_action_link(
24344 $link_text,
24345 '#',
24346 false,
24347 11,
24348 ( 'activate-license ' . $this->get_unique_affix() )
24349 );
24350 }
24351
24352 /**
24353 * @author Leo Fajardo (@leorw)
24354 * @since 2.0.2
24355 */
24356 function _add_premium_version_upgrade_selection_action() {
24357 $this->_logger->entrance();
24358
24359 if ( ! self::is_ajax() ) {
24360 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
24361 }
24362 }
24363
24364 /**
24365 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
24366 *
24367 * @author Leo Fajardo (@leorw)
24368 * @since 1.2.1.5
24369 */
24370 function _add_tracking_links() {
24371 if ( ! current_user_can( 'manage_options' ) ) {
24372 return;
24373 }
24374
24375 $this->_logger->entrance();
24376
24377 if ( $this->is_only_premium() && $this->is_free_plan() ) {
24378 // 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.
24379 return;
24380 }
24381
24382 if (
24383 $this->is_addon() &&
24384 ! $this->is_only_premium()
24385 ) {
24386 $parent = $this->get_parent_instance();
24387
24388 if ( is_object( $parent ) && $parent->is_anonymous() ) {
24389 return;
24390 }
24391 }
24392
24393 if ( fs_is_network_admin() ) {
24394 if ( ! $this->_is_network_active ) {
24395 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
24396 return;
24397 } else if ( $this->is_network_delegated_connection() ) {
24398 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
24399 return;
24400 }
24401 } else {
24402 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
24403 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
24404 return;
24405 }
24406 }
24407
24408 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
24409 if ( ! $this->is_registered() && $this->is_anonymous() ) {
24410 $this->connect_again();
24411
24412 return;
24413 }
24414 }
24415
24416 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
24417 ( $this->is_theme() && ! self::is_themes_page() )
24418 ) {
24419 // Only show tracking links on the plugins and themes pages.
24420 return;
24421 }
24422
24423 if (
24424 $this->is_activation_mode() &&
24425 $this->is_premium() &&
24426 ! $this->is_registered()
24427 ) {
24428 // If not yet registered and running the premium code base, a license activation link will already be shown.
24429 return;
24430 }
24431
24432 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24433 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
24434 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
24435 return;
24436 }
24437 }
24438
24439 if ( $this->add_ajax_action( 'toggle_permission_tracking', array( &$this, '_toggle_permission_tracking_callback' ) ) ) {
24440 return;
24441 }
24442
24443 $link_text_id = '';
24444 $url = '#';
24445
24446 if ( $this->is_registered( true ) ) {
24447 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24448 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
24449 } else {
24450 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24451 }
24452 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
24453 /**
24454 * Show opt-in link only if skipped or in activation mode.
24455 */
24456 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24457
24458 $params = ! $this->is_anonymous() ?
24459 array() :
24460 array(
24461 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
24462 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
24463 );
24464
24465 $url = $this->get_activation_url( $params );
24466 }
24467
24468 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
24469
24470 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
24471 $this->add_plugin_action_link(
24472 $link_text_id,
24473 $url,
24474 false,
24475 13,
24476 "opt-in-or-opt-out {$this->_slug}"
24477 );
24478 }
24479 }
24480
24481 /**
24482 * Get the URL of the page that should be loaded right after the plugin activation.
24483 *
24484 * @author Vova Feldman (@svovaf)
24485 * @since 1.1.7.4
24486 *
24487 * @return string
24488 */
24489 function get_after_plugin_activation_redirect_url() {
24490 $url = false;
24491
24492 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
24493 $first_time_path = $this->_menu->get_first_time_path(
24494 fs_is_network_admin() && $this->_is_network_active
24495 );
24496
24497 if ( $this->is_activation_mode() ) {
24498 $url = $this->get_activation_url();
24499 } else if ( ! empty( $first_time_path ) ) {
24500 $url = $first_time_path;
24501 } else {
24502 $page = '';
24503 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
24504 if ( $this->is_network_registered() ) {
24505 $page = 'account';
24506 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
24507 $this->maybe_set_slug_and_network_menu_exists_flag();
24508 }
24509 }
24510
24511 $url = $this->_get_admin_page_url( $page );
24512 }
24513 } else {
24514 $plugin_fs = false;
24515
24516 if ( $this->is_parent_plugin_installed() ) {
24517 $plugin_fs = self::get_parent_instance();
24518 }
24519
24520 if ( is_object( $plugin_fs ) ) {
24521 if ( ! $plugin_fs->is_registered() ) {
24522 // Forward to parent plugin connect when parent not registered.
24523 $url = $plugin_fs->get_activation_url();
24524 } else {
24525 // Forward to account page.
24526 $url = $plugin_fs->_get_admin_page_url( 'account' );
24527 }
24528 }
24529 }
24530
24531 return $url;
24532 }
24533
24534 /**
24535 * Forward page to activation page.
24536 *
24537 * @author Vova Feldman (@svovaf)
24538 * @since 1.0.3
24539 */
24540 function _redirect_on_activation_hook() {
24541 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
24542 $url = $this->get_after_plugin_activation_redirect_url();
24543
24544 if ( is_string( $url ) ) {
24545 fs_redirect( $url );
24546 }
24547 }
24548 }
24549
24550 /**
24551 * Modify plugin's page action links collection.
24552 *
24553 * @author Vova Feldman (@svovaf)
24554 * @since 1.0.0
24555 *
24556 * @param array $links
24557 * @param $file
24558 *
24559 * @return array
24560 */
24561 function _modify_plugin_action_links_hook( $links, $file ) {
24562 $this->_logger->entrance();
24563
24564 $passed_deactivate = false;
24565 $deactivate_link = '';
24566 $before_deactivate = array();
24567 $after_deactivate = array();
24568 foreach ( $links as $key => $link ) {
24569 if ( 'deactivate' === $key ) {
24570 $deactivate_link = $link;
24571 $passed_deactivate = true;
24572 continue;
24573 }
24574
24575 if ( ! $passed_deactivate ) {
24576 $before_deactivate[ $key ] = $link;
24577 } else {
24578 $after_deactivate[ $key ] = $link;
24579 }
24580 }
24581
24582 ksort( $this->_action_links );
24583
24584 foreach ( $this->_action_links as $new_links ) {
24585 foreach ( $new_links as $link ) {
24586 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
24587 }
24588 }
24589
24590 if ( ! empty( $deactivate_link ) ) {
24591 /**
24592 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
24593 *
24594 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
24595 */
24596 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
24597
24598 // Append deactivation link.
24599 $before_deactivate['deactivate'] = $deactivate_link;
24600 }
24601
24602 return array_merge( $before_deactivate, $after_deactivate );
24603 }
24604
24605 /**
24606 * Adds admin message.
24607 *
24608 * @author Vova Feldman (@svovaf)
24609 * @since 1.0.4
24610 *
24611 * @param string $message
24612 * @param string $title
24613 * @param string $type
24614 */
24615 function add_admin_message( $message, $title = '', $type = 'success' ) {
24616 $this->_admin_notices->add( $message, $title, $type );
24617 }
24618
24619 /**
24620 * Adds sticky admin message.
24621 *
24622 * @author Vova Feldman (@svovaf)
24623 * @since 1.1.0
24624 *
24625 * @param string $message
24626 * @param string $id
24627 * @param string $title
24628 * @param string $type
24629 */
24630 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
24631 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
24632 }
24633
24634 /**
24635 * Check if the paid version of the module is installed.
24636 *
24637 * @author Vova Feldman (@svovaf)
24638 * @since 2.2.0
24639 *
24640 * @return bool
24641 */
24642 private function is_premium_version_installed() {
24643 $premium_plugin_basename = $this->premium_plugin_basename();
24644
24645 if ( $this->is_theme() ) {
24646 return $this->can_activate_theme( $this->get_premium_slug() );
24647 }
24648
24649 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_plugin_basename ) );
24650 }
24651
24652 /**
24653 * Helper function that returns the final steps for the upgrade completion.
24654 *
24655 * If the module is already running the premium code, returns an empty string.
24656 *
24657 * @author Vova Feldman (@svovaf)
24658 * @since 1.2.1
24659 *
24660 * @param string $plan_title
24661 *
24662 * @return string
24663 */
24664 private function get_complete_upgrade_instructions( $plan_title = '' ) {
24665 $this->_logger->entrance();
24666
24667 $activate_license_string = $this->get_license_network_activation_notice();
24668
24669 if ( ! $this->has_premium_version() || $this->is_premium() ) {
24670 return '' . $activate_license_string;
24671 }
24672
24673 if ( empty( $plan_title ) ) {
24674 $plan_title = $this->get_plan_title();
24675 }
24676
24677 if ( $this->is_premium_version_installed() ) {
24678 /**
24679 * If the premium version is already installed, instead of showing the installation instructions,
24680 * tell the current user to activate it.
24681 *
24682 * @author Leo Fajardo (@leorw)
24683 * @since 2.2.1
24684 */
24685 $premium_theme_slug_or_plugin_basename = $this->is_theme() ?
24686 $this->get_premium_slug() :
24687 $this->premium_plugin_basename();
24688
24689 return sprintf(
24690 /* translators: %1$s: Product title; %2$s: Plan title */
24691 $this->get_text_inline( ' The paid version of %1$s is already installed. Please activate it to start benefiting the %2$s features. %3$s', 'activate-premium-version' ),
24692 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24693 $plan_title,
24694 sprintf(
24695 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
24696 ( $this->is_theme() ?
24697 wp_nonce_url( 'themes.php?action=activate&amp;stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) :
24698 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ),
24699 esc_html( sprintf(
24700 /* translators: %s: Plan title */
24701 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
24702 $plan_title
24703 ) )
24704 )
24705 );
24706 } else {
24707 // @since 1.2.1.5 The free version is auto deactivated.
24708 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
24709 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
24710 '';
24711
24712 return sprintf(
24713 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
24714 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
24715 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
24716 $this->get_latest_download_link( sprintf(
24717 /* translators: %s: Plan title */
24718 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
24719 $plan_title
24720 ) ),
24721 $deactivation_step,
24722 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
24723 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/wp-' . $this->_module_type . '-upload' ),
24724 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
24725 );
24726 }
24727 }
24728
24729 /**
24730 * @author Leo Fajardo (@leorw)
24731 * @since 2.5.3
24732 *
24733 * @param string $message_before_the_instructions
24734 * @param string $message_id
24735 * @param string $plan_title
24736 */
24737 private function add_complete_upgrade_instructions_notice(
24738 $message_before_the_instructions,
24739 $message_id,
24740 $plan_title = ''
24741 ) {
24742 $this->_admin_notices->add_sticky(
24743 $message_before_the_instructions .
24744 $this->get_complete_upgrade_instructions( $plan_title ),
24745 $message_id,
24746 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
24747 );
24748 }
24749
24750 /**
24751 * @author Leo Fajardo (@leorw)
24752 * @since 2.5.3
24753 *
24754 * @param bool $is_upgrade
24755 */
24756 private function add_after_plan_activation_or_upgrade_instructions_notice( $is_upgrade = true ) {
24757 $this->add_complete_upgrade_instructions_notice(
24758 $is_upgrade ?
24759 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) :
24760 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ),
24761 'plan_upgraded'
24762 );
24763 }
24764
24765 /**
24766 * @author Leo Fajardo (@leorw)
24767 * @since 2.1.0
24768 *
24769 * @param string $url
24770 * @param array $request
24771 * @param int $success_cache_expiration
24772 * @param int $failure_cache_expiration
24773 * @param bool $maybe_enrich_request_for_debug
24774 *
24775 * @return WP_Error|array
24776 */
24777 static function safe_remote_post(
24778 &$url,
24779 $request,
24780 $success_cache_expiration = 0,
24781 $failure_cache_expiration = 0,
24782 $maybe_enrich_request_for_debug = true
24783 ) {
24784 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
24785
24786 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
24787
24788 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
24789 get_transient( $cache_key ) :
24790 false;
24791
24792 if ( false === $response ) {
24793 if ( $maybe_enrich_request_for_debug ) {
24794 FS_DebugManager::enrich_request_for_debug( $url, $request );
24795 }
24796
24797 if ( ! isset( $request['method'] ) ) {
24798 $request['method'] = 'POST';
24799 }
24800
24801 $response = FS_Api::remote_request( $url, $request );
24802
24803 if (
24804 'https://' === substr( $url, 0, 8 ) &&
24805 FS_Api::is_ssl_error_response( $response )
24806 ) {
24807 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
24808 $url = 'http://' . substr( $url, 8 );
24809
24810 $request['timeout'] = 15;
24811 $response = FS_Api::remote_request( $url, $request );
24812 }
24813
24814 if ( false !== $cache_key ) {
24815 set_transient(
24816 $cache_key,
24817 $response,
24818 ( ( $response instanceof WP_Error ) ?
24819 $failure_cache_expiration :
24820 $success_cache_expiration )
24821 );
24822 }
24823 }
24824
24825 return $response;
24826 }
24827
24828 /**
24829 * This method is used to enrich the after upgrade notice instructions when the upgraded
24830 * license cannot be activated network wide (license quota isn't large enough).
24831 *
24832 * @author Vova Feldman (@svovaf)
24833 * @since 2.0.0
24834 *
24835 * @return string
24836 */
24837 private function get_license_network_activation_notice() {
24838 if ( ! $this->_is_network_active ) {
24839 // Module isn't network level activated.
24840 return '';
24841 }
24842
24843 if ( ! fs_is_network_admin() ) {
24844 // Not network level admin.
24845 return '';
24846 }
24847
24848 if ( get_blog_count() == 1 ) {
24849 // There's only a single site in the network so if there's a context license it was already activated.
24850 return '';
24851 }
24852
24853 if ( ! is_object( $this->_license ) ) {
24854 // No context license.
24855 return '';
24856 }
24857
24858 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
24859 // 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).
24860 return '';
24861 }
24862
24863 if ( $this->can_activate_license_on_network( $this->_license ) ) {
24864 // 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).
24865 return '';
24866 }
24867
24868 return sprintf(
24869 $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' ),
24870 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
24871 '</a>'
24872 );
24873 }
24874
24875 /**
24876 * @author Vova Feldman (@svovaf)
24877 * @since 1.2.1.7
24878 *
24879 * @param string $key
24880 *
24881 * @return string
24882 */
24883 function get_text( $key ) {
24884 return fs_text( $key, $this->_slug );
24885 }
24886
24887 /**
24888 * @author Vova Feldman (@svovaf)
24889 * @since 1.2.3
24890 *
24891 * @param string $text Translatable string.
24892 * @param string $key String key for overrides.
24893 *
24894 * @return string
24895 */
24896 function get_text_inline( $text, $key = '' ) {
24897 return _fs_text_inline( $text, $key, $this->_slug );
24898 }
24899
24900 /**
24901 * @author Vova Feldman (@svovaf)
24902 * @since 1.2.3
24903 *
24904 * @param string $text Translatable string.
24905 * @param string $context Context information for the translators.
24906 * @param string $key String key for overrides.
24907 *
24908 * @return string
24909 */
24910 function get_text_x_inline( $text, $context, $key ) {
24911 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
24912 }
24913
24914 /**
24915 * @author Vova Feldman (@svovaf)
24916 * @since 1.2.3
24917 *
24918 * @param string $text Translatable string.
24919 * @param string $key String key for overrides.
24920 *
24921 * @return string
24922 */
24923 function esc_html_inline( $text, $key ) {
24924 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
24925 }
24926
24927 #----------------------------------------------------------------------------------
24928 #region Versioning
24929 #----------------------------------------------------------------------------------
24930
24931 /**
24932 * Check if Freemius in SDK upgrade mode.
24933 *
24934 * @author Vova Feldman (@svovaf)
24935 * @since 1.0.9
24936 *
24937 * @return bool
24938 */
24939 function is_sdk_upgrade_mode() {
24940 return isset( $this->_storage->sdk_upgrade_mode ) ?
24941 $this->_storage->sdk_upgrade_mode :
24942 false;
24943 }
24944
24945 /**
24946 * Turn SDK upgrade mode off.
24947 *
24948 * @author Vova Feldman (@svovaf)
24949 * @since 1.0.9
24950 */
24951 function set_sdk_upgrade_complete() {
24952 $this->_storage->sdk_upgrade_mode = false;
24953 }
24954
24955 /**
24956 * Check if plugin upgrade mode.
24957 *
24958 * @author Vova Feldman (@svovaf)
24959 * @since 1.0.9
24960 *
24961 * @return bool
24962 */
24963 function is_plugin_upgrade_mode() {
24964 return isset( $this->_storage->plugin_upgrade_mode ) ?
24965 $this->_storage->plugin_upgrade_mode :
24966 false;
24967 }
24968
24969 /**
24970 * Turn plugin upgrade mode off.
24971 *
24972 * @author Vova Feldman (@svovaf)
24973 * @since 1.0.9
24974 */
24975 function set_plugin_upgrade_complete() {
24976 $this->_storage->plugin_upgrade_mode = false;
24977
24978 $license_migration = ! empty( $this->_storage->license_migration ) ?
24979 $this->_storage->license_migration :
24980 array();
24981
24982 $license_migration['is_migrating'] = false;
24983
24984 $this->_storage->license_migration = $license_migration;
24985 }
24986
24987 #endregion
24988
24989 #----------------------------------------------------------------------------------
24990 #region Permissions
24991 #----------------------------------------------------------------------------------
24992
24993 /**
24994 * Check if specific permission requested.
24995 *
24996 * @author Vova Feldman (@svovaf)
24997 * @since 1.1.6
24998 *
24999 * @param string $permission
25000 *
25001 * @return bool
25002 */
25003 function is_permission_requested( $permission ) {
25004 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
25005 }
25006
25007 #endregion
25008
25009 #----------------------------------------------------------------------------------
25010 #region Auto Activation
25011 #----------------------------------------------------------------------------------
25012
25013 /**
25014 * Hints the SDK if running an auto-installation.
25015 *
25016 * @var bool
25017 */
25018 private $_isAutoInstall = false;
25019
25020 /**
25021 * After upgrade callback to install and auto activate a plugin.
25022 * This code will only be executed on explicit request from the user,
25023 * following the practice Jetpack are using with their theme installations.
25024 *
25025 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
25026 *
25027 * @author Vova Feldman (@svovaf)
25028 * @since 1.2.1.7
25029 */
25030 function _install_premium_version_ajax_action() {
25031 $this->_logger->entrance();
25032
25033 $this->check_ajax_referer( 'install_premium_version' );
25034
25035 if ( ! $this->is_registered() ) {
25036 // Not registered.
25037 self::shoot_ajax_failure( array(
25038 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
25039 'code' => 'premium_installed',
25040 ) );
25041 }
25042
25043 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
25044
25045 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25046 // Invalid ID.
25047 self::shoot_ajax_failure( array(
25048 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25049 'code' => 'invalid_module_id',
25050 ) );
25051 }
25052
25053 if ( $plugin_id == $this->get_id() ) {
25054 if ( $this->is_premium() ) {
25055 // Already using the premium code version.
25056 self::shoot_ajax_failure( array(
25057 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
25058 'code' => 'premium_installed',
25059 ) );
25060 }
25061 if ( ! $this->can_use_premium_code() ) {
25062 // Don't have access to the premium code.
25063 self::shoot_ajax_failure( array(
25064 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
25065 'code' => 'invalid_license',
25066 ) );
25067 }
25068 if ( ! $this->has_release_on_freemius() ) {
25069 // Plugin is a serviceware, no premium code version.
25070 self::shoot_ajax_failure( array(
25071 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
25072 'code' => 'premium_version_missing',
25073 ) );
25074 }
25075 } else {
25076 $addon = $this->get_addon( $plugin_id );
25077
25078 if ( ! is_object( $addon ) ) {
25079 // Invalid add-on ID.
25080 self::shoot_ajax_failure( array(
25081 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25082 'code' => 'invalid_module_id',
25083 ) );
25084 }
25085
25086 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25087 // Premium add-on version is already activated.
25088 self::shoot_ajax_failure( array(
25089 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
25090 'code' => 'premium_installed',
25091 ) );
25092 }
25093 }
25094
25095 $this->_isAutoInstall = true;
25096
25097 // Try to install and activate.
25098 $updater = FS_Plugin_Updater::instance( $this );
25099 $result = $updater->install_and_activate_plugin( $plugin_id );
25100
25101 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
25102 self::shoot_ajax_failure( array(
25103 'message' => $result['message'],
25104 'code' => $result['code'],
25105 ) );
25106 }
25107
25108 self::shoot_ajax_success( $result );
25109 }
25110
25111 /**
25112 * Displays module activation dialog box after a successful upgrade
25113 * where the user explicitly requested to auto download and install
25114 * the premium version.
25115 *
25116 * @author Vova Feldman (@svovaf)
25117 * @since 1.2.1.7
25118 */
25119 function _add_auto_installation_dialog_box() {
25120 $this->_logger->entrance();
25121
25122 if ( ! $this->is_registered() ) {
25123 // Not registered.
25124 return;
25125 }
25126
25127 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
25128
25129 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25130 // Invalid module ID.
25131 return;
25132 }
25133
25134 if ( $plugin_id == $this->get_id() ) {
25135 if ( $this->is_premium() ) {
25136 // Already using the premium code version.
25137 return;
25138 }
25139 if ( ! $this->can_use_premium_code() ) {
25140 // Don't have access to the premium code.
25141 return;
25142 }
25143 if ( ! $this->has_release_on_freemius() ) {
25144 // Plugin is a serviceware, no premium code version.
25145 return;
25146 }
25147 } else {
25148 $addon = $this->get_addon( $plugin_id );
25149
25150 if ( ! is_object( $addon ) ) {
25151 // Invalid add-on ID.
25152 return;
25153 }
25154
25155 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25156 // Premium add-on version is already activated.
25157 return;
25158 }
25159 }
25160
25161 $vars = array(
25162 'id' => $this->_module_id,
25163 'target_module_id' => $plugin_id,
25164 'slug' => $this->_slug,
25165 );
25166
25167 fs_require_template( 'auto-installation.php', $vars );
25168 }
25169
25170 #endregion
25171
25172 #--------------------------------------------------------------------------------
25173 #region Tabs Integration
25174 #--------------------------------------------------------------------------------
25175
25176 #region Module's Original Tabs
25177
25178 /**
25179 * Inject a JavaScript logic to capture the theme tabs HTML.
25180 *
25181 * @author Vova Feldman (@svovaf)
25182 * @since 1.2.2.7
25183 */
25184 function _tabs_capture() {
25185 $this->_logger->entrance();
25186
25187 if (
25188 ! $this->is_product_settings_page() ||
25189 ! $this->should_page_include_tabs() ||
25190 ! $this->is_matching_url( $this->main_menu_url() )
25191 ) {
25192 return;
25193 }
25194
25195 $params = array(
25196 'id' => $this->_module_id,
25197 );
25198
25199 fs_require_once_template( 'tabs-capture-js.php', $params );
25200 }
25201
25202 /**
25203 * Cache theme's tabs HTML for a week. The cache will also be set as expired
25204 * after version and type (free/premium) changes, in addition to the week period.
25205 *
25206 * @author Vova Feldman (@svovaf)
25207 * @since 1.2.2.7
25208 */
25209 function _store_tabs_ajax_action() {
25210 $this->_logger->entrance();
25211
25212 $this->check_ajax_referer( 'store_tabs' );
25213
25214 // Init filesystem if not yet initiated.
25215 WP_Filesystem();
25216
25217 // Get POST body HTML data.
25218 global $wp_filesystem;
25219 $tabs_html = $wp_filesystem->get_contents( "php://input" );
25220
25221 if ( is_string( $tabs_html ) ) {
25222 $tabs_html = trim( $tabs_html );
25223 }
25224
25225 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
25226 self::shoot_ajax_failure();
25227 }
25228
25229 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25230
25231 self::shoot_ajax_success();
25232 }
25233
25234 /**
25235 * Cache theme's settings page custom styles. The cache will also be set as expired
25236 * after version and type (free/premium) changes, in addition to the week period.
25237 *
25238 * @author Vova Feldman (@svovaf)
25239 * @since 1.2.2.7
25240 */
25241 function _store_tabs_styles() {
25242 $this->_logger->entrance();
25243
25244 if (
25245 ! $this->is_product_settings_page() ||
25246 ! $this->should_page_include_tabs() ||
25247 ! $this->is_matching_url( $this->main_menu_url() )
25248 ) {
25249 return;
25250 }
25251
25252 $wp_styles = wp_styles();
25253
25254 $theme_styles_url = get_template_directory_uri();
25255
25256 $stylesheets = array();
25257 foreach ( $wp_styles->queue as $handler ) {
25258 if ( fs_starts_with( $handler, 'fs_' ) ) {
25259 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
25260 continue;
25261 }
25262
25263 /**
25264 * @var _WP_Dependency $stylesheet
25265 */
25266 $stylesheet = $wp_styles->registered[ $handler ];
25267
25268 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
25269 $stylesheets[] = $stylesheet->src;
25270 }
25271 }
25272
25273 if ( ! empty( $stylesheets ) ) {
25274 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25275 }
25276 }
25277
25278 /**
25279 * Check if module's original settings page has any tabs.
25280 *
25281 * @author Vova Feldman (@svovaf)
25282 * @since 1.2.2.7
25283 *
25284 * @return bool
25285 */
25286 private function has_tabs() {
25287 return $this->_cache->has( 'tabs' );
25288 }
25289
25290 /**
25291 * Get module's settings page HTML content, starting
25292 * from the beginning of the <div class="wrap"> element,
25293 * until the tabs HTML (including).
25294 *
25295 * @author Vova Feldman (@svovaf)
25296 * @since 1.2.2.7
25297 *
25298 * @return string
25299 */
25300 private function get_tabs_html() {
25301 $this->_logger->entrance();
25302
25303 return $this->_cache->get( 'tabs' );
25304 }
25305
25306 /**
25307 * Check if page should include tabs.
25308 *
25309 * @author Vova Feldman (@svovaf)
25310 * @since 1.2.2.7
25311 *
25312 * @return bool
25313 */
25314 private function should_page_include_tabs() {
25315 if ( ! $this->has_settings_menu() ) {
25316 // Don't add tabs if no settings at all.
25317 return false;
25318 }
25319
25320 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
25321 // Only add tabs to themes for now.
25322 return false;
25323 }
25324
25325 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
25326 // Only add tabs to monetizing themes.
25327 return false;
25328 }
25329
25330 if ( ! $this->is_product_settings_page() ) {
25331 // Only add tabs if browsing one of the product's setting pages.
25332 return false;
25333 }
25334
25335 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
25336 // Don't include tabs in the activation page.
25337 return false;
25338 }
25339
25340 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
25341 // Don't add tabs on checkout page, we want to reduce distractions
25342 // as much as possible.
25343 return false;
25344 }
25345
25346 return true;
25347 }
25348
25349 /**
25350 * Add the tabs HTML before the setting's page content and
25351 * enqueue any required stylesheets.
25352 *
25353 * @author Vova Feldman (@svovaf)
25354 * @since 1.2.2.7
25355 *
25356 * @return bool If tabs were included.
25357 */
25358 function _add_tabs_before_content() {
25359 $this->_logger->entrance();
25360
25361 if ( ! $this->should_page_include_tabs() ) {
25362 return false;
25363 }
25364
25365 $tabs_html = $this->get_tabs_html();
25366
25367 if ( empty( $tabs_html ) ) {
25368 return false;
25369 }
25370
25371 /**
25372 * Enqueue the original stylesheets that are included in the
25373 * theme settings page. That way, if the theme settings has
25374 * some custom _styled_ content above the tabs UI, this
25375 * will make sure that the styling is preserved.
25376 */
25377 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
25378 if ( is_array( $stylesheets ) ) {
25379 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
25380 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
25381 }
25382 }
25383
25384 // Cut closing </div> tag.
25385 echo substr( trim( $tabs_html ), 0, - 6 );
25386
25387 return true;
25388 }
25389
25390 /**
25391 * Add the tabs closing HTML after the setting's page content.
25392 *
25393 * @author Vova Feldman (@svovaf)
25394 * @since 1.2.2.7
25395 *
25396 * @return bool If tabs closing HTML was included.
25397 */
25398 function _add_tabs_after_content() {
25399 $this->_logger->entrance();
25400
25401 if ( ! $this->should_page_include_tabs() ) {
25402 return false;
25403 }
25404
25405 echo '</div>';
25406
25407 return true;
25408 }
25409
25410 #endregion
25411
25412 /**
25413 * Add in-page JavaScript to inject the Freemius tabs into
25414 * the module's setting tabs section.
25415 *
25416 * @author Vova Feldman (@svovaf)
25417 * @since 1.2.2.7
25418 */
25419 function _add_freemius_tabs() {
25420 $this->_logger->entrance();
25421
25422 if ( ! $this->should_page_include_tabs() ) {
25423 return;
25424 }
25425
25426 $params = array( 'id' => $this->_module_id );
25427 fs_require_once_template( 'tabs.php', $params );
25428 }
25429
25430 #endregion
25431
25432 #--------------------------------------------------------------------------------
25433 #region Customizer Integration for Themes
25434 #--------------------------------------------------------------------------------
25435
25436 /**
25437 * @author Vova Feldman (@svovaf)
25438 * @since 1.2.2.7
25439 *
25440 * @param WP_Customize_Manager $customizer
25441 */
25442 function _customizer_register( $customizer ) {
25443 $this->_logger->entrance();
25444
25445 if ( $this->is_pricing_page_visible() ) {
25446 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
25447
25448 $customizer->add_section( 'freemius_upsell', array(
25449 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
25450 'priority' => 1,
25451 ) );
25452 $customizer->add_setting( 'freemius_upsell', array(
25453 'sanitize_callback' => 'esc_html',
25454 ) );
25455
25456 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
25457 'fs' => $this,
25458 'section' => 'freemius_upsell',
25459 'priority' => 100,
25460 ) ) );
25461 }
25462
25463 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
25464 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
25465
25466 // Main Documentation Link In Customizer Root.
25467 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
25468 'fs' => $this,
25469 'priority' => 1000,
25470 ) ) );
25471 }
25472 }
25473
25474 #endregion
25475
25476 /**
25477 * If the theme has a paid version, add some custom
25478 * styling to the theme's premium version (if exists)
25479 * to highlight that it's the premium version of the
25480 * same theme, making it easier for identification
25481 * after the user upgrades and upload it to the site.
25482 *
25483 * @author Vova Feldman (@svovaf)
25484 * @since 1.2.2.7
25485 */
25486 function _style_premium_theme() {
25487 $this->_logger->entrance();
25488
25489 if ( ! self::is_themes_page() ) {
25490 // Only include in the themes page.
25491 return;
25492 }
25493
25494 if ( ! $this->has_paid_plan() ) {
25495 // Only include if has any paid plans.
25496 return;
25497 }
25498
25499 $params = null;
25500 fs_require_once_template( '/js/jquery.content-change.php', $params );
25501
25502 $params = array(
25503 'slug' => $this->_slug,
25504 'id' => $this->_module_id,
25505 );
25506
25507 fs_require_template( '/js/style-premium-theme.php', $params );
25508 }
25509
25510 /**
25511 * This method will return the absolute URL of the module's local icon.
25512 *
25513 * When you are running your plugin or theme on a **localhost** environment, if the icon
25514 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
25515 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
25516 * If an icon is found, this method will automatically attempt to download the icon and store it
25517 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
25518 *
25519 * It's important to mention that this method is NOT phoning home since the developer will deploy
25520 * the product with the local icon in the assets folder. The download process just simplifies
25521 * the process for the developer.
25522 *
25523 * @author Vova Feldman (@svovaf)
25524 * @since 2.0.0
25525 *
25526 * @return string
25527 */
25528 function get_local_icon_url() {
25529 global $fs_active_plugins;
25530
25531 /**
25532 * @since 1.1.7.5
25533 */
25534 $local_path = $this->apply_filters( 'plugin_icon', false );
25535
25536 if ( is_string( $local_path ) ) {
25537 $icons = array( $local_path );
25538 } else {
25539 $img_dir = WP_FS__DIR_IMG;
25540
25541 // Locate the main assets folder.
25542 if ( ! empty( $fs_active_plugins->plugins ) ) {
25543 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
25544
25545 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
25546 if ( $data->plugin_path == $this->get_plugin_basename() ) {
25547 $img_dir = $plugin_or_theme_img_dir
25548 . '/'
25549 /**
25550 * The basename will be `themes` or the basename of a custom themes directory.
25551 *
25552 * @author Leo Fajardo (@leorw)
25553 * @since 2.2.3
25554 */
25555 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
25556 . '/assets/img';
25557
25558 break;
25559 }
25560 }
25561 }
25562
25563 // Try to locate the icon in the assets folder.
25564 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
25565
25566 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
25567 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
25568 $icons = array(
25569 fs_normalize_path( $img_dir . '/theme-icon.png' )
25570 );
25571 } else {
25572 $icon_found = false;
25573 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
25574
25575 if ( ! function_exists( 'get_filesystem_method' ) ) {
25576 require_once ABSPATH . 'wp-admin/includes/file.php';
25577 }
25578
25579 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
25580
25581 /**
25582 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
25583 *
25584 * This code will only be executed once during the testing
25585 * of the plugin in a local environment. The plugin icon file WILL
25586 * already exist in the assets folder when the plugin is deployed to
25587 * the repository.
25588 */
25589 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
25590 // Fetch icon from Freemius.
25591 $icon = $this->fetch_remote_icon_url();
25592
25593 // Fetch icon from WordPress.org.
25594 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
25595 if ( ! function_exists( 'plugins_api' ) ) {
25596 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
25597 }
25598
25599 $plugin_information = plugins_api( 'plugin_information', array(
25600 'slug' => $this->_slug,
25601 'fields' => array(
25602 'sections' => false,
25603 'tags' => false,
25604 'icons' => true
25605 )
25606 ) );
25607
25608 if (
25609 ! is_wp_error( $plugin_information )
25610 && isset( $plugin_information->icons )
25611 && ! empty( $plugin_information->icons )
25612 ) {
25613 /**
25614 * Get the smallest icon.
25615 *
25616 * @author Leo Fajardo (@leorw)
25617 * @since 1.2.2
25618 */
25619 $icon = end( $plugin_information->icons );
25620 }
25621 }
25622
25623 if ( ! empty( $icon ) ) {
25624 if ( 0 !== strpos( $icon, 'http' ) ) {
25625 $icon = 'http:' . $icon;
25626 }
25627
25628 /**
25629 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
25630 *
25631 * @author Leo Fajardo (@leorw)
25632 * @since 1.2.2
25633 */
25634 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
25635
25636 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
25637
25638 // Try to download the icon.
25639 $icon_found = fs_download_image( $icon, $local_path );
25640 }
25641 }
25642
25643 if ( ! $icon_found ) {
25644 // No icons found, fallback to default icon.
25645 if ( $have_write_permissions ) {
25646 // If have write permissions, copy default icon.
25647 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
25648 } else {
25649 // If doesn't have write permissions, use default icon path.
25650 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
25651 }
25652 }
25653
25654 $icons = array( $local_path );
25655 }
25656 }
25657 }
25658
25659 $icon_dir = dirname( $icons[0] );
25660
25661 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
25662 }
25663
25664 /**
25665 * Fetch module's extended info.
25666 *
25667 * @author Vova Feldman (@svovaf)
25668 * @since 2.0.0
25669 *
25670 * @return object|mixed
25671 */
25672 private function fetch_module_info() {
25673 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
25674 }
25675
25676 /**
25677 * Fetch module's remote icon URL.
25678 *
25679 * @author Vova Feldman (@svovaf)
25680 * @since 2.0.0
25681 *
25682 * @return string
25683 */
25684 function fetch_remote_icon_url() {
25685 $info = $this->fetch_module_info();
25686
25687 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
25688 $info->icon :
25689 '';
25690 }
25691
25692 #--------------------------------------------------------------------------------
25693 #region GDPR
25694 #--------------------------------------------------------------------------------
25695
25696 /**
25697 * @author Leo Fajardo (@leorw)
25698 * @since 2.1.0
25699 *
25700 * @param array $user_plugins
25701 *
25702 * @return string
25703 */
25704 private function get_gdpr_admin_notice_string( $user_plugins ) {
25705 $this->_logger->entrance();
25706
25707 $addons = self::get_all_addons();
25708
25709 foreach ( $user_plugins as $user_plugin ) {
25710 $has_addons = isset( $addons[ $user_plugin->id ] );
25711
25712 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
25713 if ( $this->_module_id == $user_plugin->id ) {
25714 $addons = $this->get_addons();
25715 $has_addons = ( ! empty( $addons ) );
25716 } else {
25717 $plugin_api = FS_Api::instance(
25718 $user_plugin->id,
25719 'plugin',
25720 $user_plugin->id,
25721 $user_plugin->public_key,
25722 ! $user_plugin->is_live,
25723 false,
25724 $this->get_sdk_version()
25725 );
25726
25727 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
25728
25729 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
25730 is_array( $addons_result->plugins ) &&
25731 ! empty( $addons_result->plugins )
25732 ) {
25733 $has_addons = true;
25734 }
25735 }
25736 }
25737
25738 $user_plugin->has_addons = $has_addons;
25739 }
25740
25741 $is_single_parent_product = ( 1 === count( $user_plugins ) );
25742
25743 $multiple_products_text = '';
25744
25745 if ( $is_single_parent_product ) {
25746 $single_parent_product = reset( $user_plugins );
25747
25748 $thank_you = sprintf(
25749 "<span data-plugin-id='%d'>%s</span>",
25750 $single_parent_product->id,
25751 sprintf(
25752 $single_parent_product->has_addons ?
25753 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
25754 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
25755 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
25756 )
25757 );
25758
25759 $already_opted_in = sprintf(
25760 $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' ),
25761 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
25762 );
25763 } else {
25764 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
25765 $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' );
25766
25767 $products_and_add_ons = '';
25768 foreach ( $user_plugins as $user_plugin ) {
25769 if ( ! empty( $products_and_add_ons ) ) {
25770 $products_and_add_ons .= ', ';
25771 }
25772
25773 if ( ! $user_plugin->has_addons ) {
25774 $products_and_add_ons .= sprintf(
25775 "<span data-plugin-id='%d'>%s</span>",
25776 $user_plugin->id,
25777 $user_plugin->title
25778 );
25779 } else {
25780 $products_and_add_ons .= sprintf(
25781 "<span data-plugin-id='%d'>%s</span>",
25782 $user_plugin->id,
25783 sprintf(
25784 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
25785 $user_plugin->title
25786 )
25787 );
25788 }
25789 }
25790
25791 $multiple_products_text = sprintf(
25792 "<small class='products'><strong>%s:</strong> %s</small>",
25793 $this->get_text_inline( 'Products', 'products' ),
25794 $products_and_add_ons
25795 );
25796 }
25797
25798 $actions = sprintf(
25799 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
25800 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
25801 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
25802 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
25803 sprintf(
25804 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
25805 '<span class="underlined">',
25806 '</span>'
25807 )
25808 );
25809
25810 return sprintf(
25811 '%s %s %s',
25812 $thank_you,
25813 $already_opted_in,
25814 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>' ) .
25815 '<br><br>' .
25816 '<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>' .
25817 $actions .
25818 ( $is_single_parent_product ? '' : $multiple_products_text )
25819 );
25820 }
25821
25822 /**
25823 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
25824 * plugins and themes they've opted in to.
25825 *
25826 * @author Leo Fajardo (@leorw)
25827 * @since 2.1.0
25828 *
25829 * @param string $user_email
25830 * @param string $license_key
25831 * @param array $plugin_ids
25832 * @param string|null $license_key
25833 *
25834 * @return array|false
25835 */
25836 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
25837 $request = array(
25838 'method' => 'POST',
25839 'body' => array(),
25840 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
25841 );
25842
25843 if ( is_string( $user_email ) ) {
25844 $request['body']['email'] = $user_email;
25845 } else {
25846 $request['body']['license_key'] = $license_key;
25847 }
25848
25849 $result = array();
25850
25851 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
25852 $total_plugin_ids = count( $plugin_ids );
25853
25854 $plugin_ids_count_per_request = 10;
25855 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
25856 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
25857
25858 $request['body']['plugin_ids'] = $plugin_ids_set;
25859
25860 $response = self::safe_remote_post(
25861 $url,
25862 $request,
25863 WP_FS__TIME_24_HOURS_IN_SEC,
25864 WP_FS__TIME_12_HOURS_IN_SEC
25865 );
25866
25867 if ( ! is_wp_error( $response ) ) {
25868 $decoded = is_string( $response['body'] ) ?
25869 json_decode( $response['body'] ) :
25870 null;
25871
25872 if (
25873 !is_object($decoded) ||
25874 !isset($decoded->success) ||
25875 true !== $decoded->success ||
25876 !isset( $decoded->data ) ||
25877 !is_array( $decoded->data )
25878 ) {
25879 return false;
25880 }
25881
25882 $result = array_merge( $result, $decoded->data );
25883 }
25884 }
25885
25886 return $result;
25887 }
25888
25889 /**
25890 * @author Leo Fajardo (@leorw)
25891 * @since 2.1.0
25892 */
25893 function _maybe_show_gdpr_admin_notice() {
25894 if ( ! $this->is_user_in_admin() ) {
25895 return;
25896 }
25897
25898 if ( ! $this->should_handle_gdpr_admin_notice() ) {
25899 return;
25900 }
25901
25902 if ( ! $this->is_user_admin() ) {
25903 return;
25904 }
25905
25906 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25907
25908 $lock = FS_User_Lock::instance();
25909
25910 /**
25911 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
25912 */
25913 if ( ! $lock->try_lock( 60 ) ) {
25914 return;
25915 }
25916
25917 /**
25918 * @var $current_wp_user WP_User
25919 */
25920 $current_wp_user = self::_get_current_wp_user();
25921
25922 /**
25923 * @var FS_User $current_fs_user
25924 */
25925 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
25926
25927 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
25928
25929 if ( ! is_object( $current_fs_user ) ) {
25930 // 10-year lock.
25931 $lock->lock( $ten_years_in_sec );
25932
25933 return;
25934 }
25935
25936 $gdpr = FS_GDPR_Manager::instance();
25937
25938 if ( $gdpr->is_opt_in_notice_shown() ) {
25939 // 30-day lock.
25940 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
25941
25942 return;
25943 }
25944
25945 if ( ! $gdpr->should_show_opt_in_notice() ) {
25946 // 10-year lock.
25947 $lock->lock( $ten_years_in_sec );
25948
25949 return;
25950 }
25951
25952 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
25953 $was_notice_shown_before = ( false !== $last_time_notice_shown );
25954
25955 if ( $was_notice_shown_before &&
25956 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
25957 ) {
25958 // If the notice was shown before, show it again after 30 days from the last time it was shown.
25959 return;
25960 }
25961
25962 /**
25963 * Find all plugin IDs that were installed by the current admin.
25964 */
25965 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
25966
25967 if ( empty( $plugin_ids_map )) {
25968 $lock->lock( $ten_years_in_sec );
25969
25970 return;
25971 }
25972
25973 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
25974 $current_fs_user->email,
25975 null,
25976 array_keys( $plugin_ids_map )
25977 );
25978
25979 if ( empty( $user_plugins ) ) {
25980 $lock->lock(
25981 is_array($user_plugins) ?
25982 $ten_years_in_sec :
25983 // Lock for 24-hours on errors.
25984 WP_FS__TIME_24_HOURS_IN_SEC
25985 );
25986
25987 return;
25988 }
25989
25990 $has_unset_marketing_optin = false;
25991
25992 foreach ( $user_plugins as $user_plugin ) {
25993 if ( true == $user_plugin->is_marketing_allowed ) {
25994 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
25995 }
25996
25997 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
25998 $has_unset_marketing_optin = true;
25999 }
26000 }
26001
26002 if ( empty( $plugin_ids_map ) ||
26003 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
26004 ) {
26005 $lock->lock( $ten_years_in_sec );
26006
26007 return;
26008 }
26009
26010 $modules = array_merge(
26011 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26012 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26013 );
26014
26015 foreach ( $modules as $module ) {
26016 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
26017 $plugin_ids_map[ $module->id ] = $module;
26018 }
26019 }
26020
26021 $plugin_title = null;
26022 if ( 1 === count( $plugin_ids_map ) ) {
26023 $module = reset( $plugin_ids_map );
26024 $plugin_title = $module->title;
26025 }
26026
26027 $gdpr->add_opt_in_sticky_notice(
26028 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
26029 $plugin_title
26030 );
26031
26032 $this->add_gdpr_optin_ajax_handler_and_style();
26033
26034 $gdpr->notice_was_just_shown();
26035
26036 // 30-day lock.
26037 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26038 }
26039
26040 /**
26041 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
26042 * marketing.
26043 *
26044 * @author Leo Fajardo (@leorw)
26045 * @since 2.1.0
26046 */
26047 private function disable_opt_in_notice_and_lock_user() {
26048 FS_GDPR_Manager::instance()->disable_opt_in_notice();
26049
26050 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26051
26052 // 10-year lock.
26053 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26054 }
26055
26056 /**
26057 * @author Leo Fajardo (@leorw)
26058 * @since 2.5.4
26059 */
26060 static function _add_api_connectivity_notice_handler_js() {
26061 fs_require_once_template( 'api-connectivity-message-js.php' );
26062 }
26063
26064 /**
26065 * @author Leo Fajardo (@leorw)
26066 * @since 2.1.0
26067 */
26068 function _add_gdpr_optin_js() {
26069 $vars = array( 'id' => $this->_module_id );
26070
26071 fs_require_once_template( 'gdpr-optin-js.php', $vars );
26072 }
26073
26074 /**
26075 * @author Leo Fajardo (@leorw)
26076 * @since 2.1.0
26077 */
26078 function enqueue_gdpr_optin_notice_style() {
26079 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
26080 }
26081
26082 /**
26083 * @author Leo Fajardo (@leorw)
26084 * @since 2.1.0
26085 */
26086 function _maybe_add_gdpr_optin_ajax_handler() {
26087 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
26088
26089 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
26090 $this->add_gdpr_optin_ajax_handler_and_style();
26091 }
26092 }
26093
26094 /**
26095 * @author Leo Fajardo (@leorw)
26096 * @since 2.1.0
26097 */
26098 function _fetch_is_marketing_required_flag_value_ajax_action() {
26099 $this->_logger->entrance();
26100
26101 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
26102
26103 $license_key = fs_request_get_raw( 'license_key' );
26104
26105 if ( empty($license_key) ) {
26106 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
26107 }
26108
26109 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26110 null,
26111 $license_key,
26112 array( $this->_module_id )
26113 );
26114
26115 if ( ! is_array( $user_plugins ) ||
26116 empty($user_plugins) ||
26117 !isset($user_plugins[0]->plugin_id) ||
26118 $user_plugins[0]->plugin_id != $this->_module_id
26119 ) {
26120 /**
26121 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
26122 *
26123 * @author Vova Feldman (@svovaf)
26124 */
26125 self::shoot_ajax_success( array(
26126 'is_marketing_allowed' => null,
26127 'license_owner_id' => null
26128 ) );
26129 }
26130
26131 self::shoot_ajax_success( array(
26132 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
26133 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
26134 ) );
26135 }
26136
26137 /**
26138 * @author Leo Fajardo (@leorw)
26139 * @since 2.3.2
26140 *
26141 * @param number[] $install_ids
26142 *
26143 * @return array {
26144 * An array of objects containing the installs' licenses owners data.
26145 *
26146 * @property number $id User ID.
26147 * @property string $email User email (can be masked email).
26148 * }
26149 */
26150 private function fetch_installs_licenses_owners_data( $install_ids ) {
26151 $this->_logger->entrance();
26152
26153 $response = $this->get_api_user_scope()->get(
26154 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
26155 );
26156
26157 $license_owners = array();
26158
26159 if ( $this->is_api_result_object( $response, 'owners' ) ) {
26160 $license_owners = $response->owners;
26161 }
26162
26163 return $license_owners;
26164 }
26165
26166 /**
26167 * @author Leo Fajardo (@leorw)
26168 * @since 2.1.0
26169 */
26170 private function add_gdpr_optin_ajax_handler_and_style() {
26171 // Add GDPR action AJAX callback.
26172 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
26173
26174 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
26175 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
26176 }
26177
26178 /**
26179 * @author Leo Fajardo (@leorw)
26180 * @since 2.1.0
26181 */
26182 function _gdpr_optin_ajax_action() {
26183 $this->_logger->entrance();
26184
26185 $this->check_ajax_referer( 'gdpr_optin_action' );
26186
26187 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
26188 self::shoot_ajax_failure();
26189 }
26190
26191 $current_wp_user = self::_get_current_wp_user();
26192
26193 $plugin_ids = fs_request_get( 'plugin_ids', array() );
26194 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
26195 self::shoot_ajax_failure();
26196 }
26197
26198 $modules = array_merge(
26199 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26200 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26201 );
26202
26203 foreach ( $modules as $key => $module ) {
26204 if ( ! in_array( $module->id, $plugin_ids ) ) {
26205 unset( $modules[ $key ] );
26206 }
26207 }
26208
26209 if ( empty( $modules ) ) {
26210 self::shoot_ajax_failure();
26211 }
26212
26213 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
26214
26215 foreach ( $modules as $module ) {
26216 $user_api->call( "?plugin_id={$module->id}", 'put', array(
26217 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
26218 ) );
26219 }
26220
26221 FS_GDPR_Manager::instance()->remove_opt_in_notice();
26222
26223 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26224
26225 // 10-year lock.
26226 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26227
26228 self::shoot_ajax_success();
26229 }
26230
26231 /**
26232 * 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.
26233 *
26234 * @author Vova Feldman (@svovaf)
26235 * @since 2.1.0
26236 *
26237 * @return bool
26238 */
26239 private function should_handle_gdpr_admin_notice() {
26240 return $this->apply_filters(
26241 'handle_gdpr_admin_notice',
26242 // Default to false.
26243 false
26244 );
26245 }
26246
26247 #endregion
26248
26249 #----------------------------------------------------------------------------------
26250 #region Marketing
26251 #----------------------------------------------------------------------------------
26252
26253 /**
26254 * Check if current user purchased any other plugins before.
26255 *
26256 * @author Vova Feldman (@svovaf)
26257 * @since 1.0.9
26258 *
26259 * @return bool
26260 */
26261 function has_purchased_before() {
26262 // TODO: Implement has_purchased_before() method.
26263 throw new Exception( 'not implemented' );
26264 }
26265
26266 /**
26267 * Check if current user classified as an agency.
26268 *
26269 * @author Vova Feldman (@svovaf)
26270 * @since 1.0.9
26271 *
26272 * @return bool
26273 */
26274 function is_agency() {
26275 // TODO: Implement is_agency() method.
26276 throw new Exception( 'not implemented' );
26277 }
26278
26279 /**
26280 * Check if current user classified as a developer.
26281 *
26282 * @author Vova Feldman (@svovaf)
26283 * @since 1.0.9
26284 *
26285 * @return bool
26286 */
26287 function is_developer() {
26288 // TODO: Implement is_developer() method.
26289 throw new Exception( 'not implemented' );
26290 }
26291
26292 /**
26293 * Check if current user classified as a business.
26294 *
26295 * @author Vova Feldman (@svovaf)
26296 * @since 1.0.9
26297 *
26298 * @return bool
26299 */
26300 function is_business() {
26301 // TODO: Implement is_business() method.
26302 throw new Exception( 'not implemented' );
26303 }
26304
26305 #endregion
26306
26307 #----------------------------------------------------------------------------------
26308 #region Helper
26309 #----------------------------------------------------------------------------------
26310
26311 /**
26312 * If running with a secret key, assume it's the developer and show pending plans as well.
26313 *
26314 * @author Vova Feldman (@svovaf)
26315 * @since 2.1.2
26316 *
26317 * @param string $path
26318 *
26319 * @return string
26320 */
26321 function add_show_pending( $path ) {
26322 if ( ! $this->has_secret_key() ) {
26323 return $path;
26324 }
26325
26326 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
26327 }
26328
26329 #endregion
26330 }
26331