PluginProbe
StreamCast – bring live radio to your site with a sleek player / trunk
StreamCast – bring live radio to your site with a sleek player vtrunk
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 / vendor / freemius / includes / class-freemius.php

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

26,390 lines 993.5 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' => isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '',
3950 'server_ip' => WP_FS__REMOTE_ADDR,
3951 'is_active' => $is_active,
3952 'timestamp' => WP_FS__SCRIPT_START_TIME,
3953 // Last version with connectivity attempt.
3954 'version' => $version,
3955 );
3956
3957 $this->_has_api_connection = $is_connected;
3958 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3959 }
3960
3961 /**
3962 * @author Leo Fajardo (@leorw)
3963 * @since 2.5.4
3964 *
3965 * @param bool $is_connected
3966 */
3967 private function update_connectivity_info( $is_connected ) {
3968 $this->store_connectivity_info(
3969 // This is true since we update the connection info only after a successful opt-in or license activation which means that Freemius has already been on even before the process.
3970 (object) array( 'is_active' => true ),
3971 $is_connected
3972 );
3973 }
3974
3975 /**
3976 * Force turning Freemius on.
3977 *
3978 * @author Vova Feldman (@svovaf)
3979 * @since 1.1.8.1
3980 *
3981 * @return bool TRUE if successfully turned on.
3982 */
3983 private function turn_on() {
3984 $this->_logger->entrance();
3985
3986 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
3987 return false;
3988 }
3989
3990 $updated_connectivity = $this->_storage->connectivity_test;
3991 $updated_connectivity['is_active'] = true;
3992 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
3993 $this->_storage->connectivity_test = $updated_connectivity;
3994
3995 $this->_is_on = true;
3996
3997 return true;
3998 }
3999
4000 /**
4001 * Anonymous and unique site identifier (Hash).
4002 *
4003 * @author Vova Feldman (@svovaf)
4004 * @since 1.1.0
4005 *
4006 * @param null|int $blog_id Since 2.0.0
4007 *
4008 * @return string
4009 */
4010 function get_anonymous_id( $blog_id = null ) {
4011 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
4012
4013 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
4014 $key = self::get_unfiltered_site_url( $blog_id, true );
4015
4016 $secure_auth = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
4017 if ( empty( $secure_auth ) ||
4018 false !== strpos( $secure_auth, ' ' ) ||
4019 'put your unique phrase here' === $secure_auth
4020 ) {
4021 // Protect against default auth key.
4022 $secure_auth = md5( microtime() );
4023 }
4024
4025 /**
4026 * Base the unique identifier on the WP secure authentication key. Which
4027 * turns the key into a secret anonymous identifier. This will help us
4028 * to avoid duplicate installs generation on the backend upon opt-in.
4029 *
4030 * @author Vova Feldman (@svovaf)
4031 * @since 1.2.3
4032 */
4033 $unique_id = md5( $key . $secure_auth );
4034
4035 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
4036 }
4037
4038 $this->_logger->departure( $unique_id );
4039
4040 return $unique_id;
4041 }
4042
4043 /**
4044 * Returns anonymous network ID.
4045 *
4046 * @since 2.4.3
4047 *
4048 * @return string
4049 */
4050 function get_anonymous_network_id() {
4051 return $this->get_anonymous_id( get_network()->site_id );
4052 }
4053
4054 /**
4055 * @author Vova Feldman (@svovaf)
4056 * @since 1.1.7.4
4057 *
4058 * @return \WP_User
4059 */
4060 static function _get_current_wp_user() {
4061 self::require_pluggable_essentials();
4062 self::wp_cookie_constants();
4063
4064 return wp_get_current_user();
4065 }
4066
4067 /**
4068 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
4069 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
4070 * is network activated the cookie constants are only configured after the network
4071 * plugins activation, therefore, if we don't define those constants WP will throw
4072 * PHP warnings/notices.
4073 *
4074 * @author Vova Feldman (@svovaf)
4075 * @since 2.1.1
4076 */
4077 private static function wp_cookie_constants() {
4078 if ( defined( 'LOGGED_IN_COOKIE' ) &&
4079 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
4080 ) {
4081 return;
4082 }
4083
4084 /**
4085 * Used to guarantee unique hash cookies
4086 *
4087 * @since 1.5.0
4088 */
4089 if ( ! defined( 'COOKIEHASH' ) ) {
4090 $siteurl = get_site_option( 'siteurl' );
4091 if ( $siteurl ) {
4092 define( 'COOKIEHASH', md5( $siteurl ) );
4093 } else {
4094 define( 'COOKIEHASH', '' );
4095 }
4096 }
4097
4098 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
4099 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
4100 }
4101
4102 /**
4103 * @since 2.5.0
4104 */
4105 if ( ! defined( 'AUTH_COOKIE' ) ) {
4106 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
4107 }
4108
4109 /**
4110 * @since 2.6.0
4111 */
4112 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
4113 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
4114 }
4115 }
4116
4117 /**
4118 * @author Vova Feldman (@svovaf)
4119 * @since 2.1.0
4120 *
4121 * @return int
4122 */
4123 static function get_current_wp_user_id() {
4124 $wp_user = self::_get_current_wp_user();
4125
4126 return $wp_user->ID;
4127 }
4128
4129 /**
4130 * @author Vova Feldman (@svovaf)
4131 * @since 1.2.1.7
4132 *
4133 * @param string $email
4134 *
4135 * @return bool
4136 */
4137 static function is_valid_email( $email ) {
4138 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4139 return false;
4140 }
4141
4142 $parts = explode( '@', $email );
4143
4144 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4145 return false;
4146 }
4147
4148 $blacklist = array(
4149 'admin.',
4150 'webmaster.',
4151 'localhost.',
4152 'dev.',
4153 'development.',
4154 'test.',
4155 'stage.',
4156 'staging.',
4157 );
4158
4159 // Make sure domain is not one of the blacklisted.
4160 foreach ( $blacklist as $invalid ) {
4161 if ( 0 === strpos( $parts[1], $invalid ) ) {
4162 return false;
4163 }
4164 }
4165
4166 // Get the UTF encoded domain name.
4167 /**
4168 * @note - The check of `defined('...')` is there to account for PHP servers compiled with some older version of ICU where the constants are not defined.
4169 * @author - @swashata
4170 */
4171 $is_new_idn_available = (
4172 version_compare( PHP_VERSION, '5.6.40') > 0 &&
4173 defined( 'IDNA_DEFAULT' ) &&
4174 defined( 'INTL_IDNA_VARIANT_UTS46' )
4175 );
4176 if ( $is_new_idn_available ) {
4177 $domain = idn_to_ascii( $parts[1], IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 );
4178 } else {
4179 $domain = idn_to_ascii( $parts[1] ); // phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet
4180 }
4181
4182 $domain = $domain . '.';
4183
4184 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4185 }
4186
4187 #endregion
4188
4189 #----------------------------------------------------------------------------------
4190 #region Email
4191 #----------------------------------------------------------------------------------
4192
4193 /**
4194 * Generates and sends an HTML email with customizable sections.
4195 *
4196 * @author Leo Fajardo (@leorw)
4197 * @since 1.1.2
4198 *
4199 * @param string $to_address
4200 * @param string $subject
4201 * @param array $sections
4202 * @param array $headers
4203 *
4204 * @return bool Whether the email contents were sent successfully.
4205 */
4206 private function send_email(
4207 $to_address,
4208 $subject,
4209 $sections = array(),
4210 $headers = array()
4211 ) {
4212 $default_sections = $this->get_email_sections();
4213
4214 // Insert new sections or replace the default email sections.
4215 if ( is_array( $sections ) && ! empty( $sections ) ) {
4216 foreach ( $sections as $section_id => $custom_section ) {
4217 if ( ! isset( $default_sections[ $section_id ] ) ) {
4218 // If the section does not exist, add it.
4219 $default_sections[ $section_id ] = $custom_section;
4220 } else {
4221 // If the section already exists, override it.
4222 $current_section = $default_sections[ $section_id ];
4223
4224 // Replace the current section's title if a custom section title exists.
4225 if ( isset( $custom_section['title'] ) ) {
4226 $current_section['title'] = $custom_section['title'];
4227 }
4228
4229 // Insert new rows under the current section or replace the default rows.
4230 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4231 foreach ( $custom_section['rows'] as $row_id => $row ) {
4232 $current_section['rows'][ $row_id ] = $row;
4233 }
4234 }
4235
4236 $default_sections[ $section_id ] = $current_section;
4237 }
4238 }
4239 }
4240
4241 $vars = array( 'sections' => $default_sections );
4242 $message = fs_get_template( 'email.php', $vars );
4243
4244 // Set the type of email to HTML.
4245 $headers[] = 'Content-type: text/html; charset=UTF-8';
4246
4247 $header_string = implode( "\r\n", $headers );
4248
4249 return wp_mail(
4250 $to_address,
4251 $subject,
4252 $message,
4253 $header_string
4254 );
4255 }
4256
4257 /**
4258 * Generates the data for the sections of the email content.
4259 *
4260 * @author Leo Fajardo (@leorw)
4261 * @since 1.1.2
4262 *
4263 * @return array
4264 */
4265 private function get_email_sections() {
4266 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4267 $current_user = self::_get_current_wp_user();
4268
4269 // Retrieve the cURL version information so that we can get the version number below.
4270 $curl_version_information = curl_version();
4271
4272 $active_plugin = self::get_active_plugins();
4273
4274 // Generate the list of active plugins separated by new line.
4275 $active_plugin_string = '';
4276 foreach ( $active_plugin as $plugin ) {
4277 $active_plugin_string .= sprintf(
4278 '<a href="%s">%s</a> [v%s]<br>',
4279 $plugin['PluginURI'],
4280 $plugin['Name'],
4281 $plugin['Version']
4282 );
4283 }
4284
4285 $server_ip = WP_FS__REMOTE_ADDR;
4286
4287 // Add PHP info for deeper investigation.
4288 ob_start();
4289 phpinfo();
4290 $php_info = ob_get_clean();
4291
4292 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4293
4294 // Generate the default email sections.
4295 $sections = array(
4296 'sdk' => array(
4297 'title' => 'SDK',
4298 'rows' => array(
4299 'fs_version' => array( 'FS Version', $this->version ),
4300 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4301 )
4302 ),
4303 'plugin' => array(
4304 'title' => ucfirst( $this->get_module_type() ),
4305 'rows' => array(
4306 'name' => array( 'Name', $this->get_plugin_name() ),
4307 'version' => array( 'Version', $this->get_plugin_version() )
4308 )
4309 ),
4310 'api' => array(
4311 'title' => 'API Subdomain',
4312 'rows' => array(
4313 'dns' => array(
4314 'DNS_CNAME',
4315 function_exists( 'dns_get_record' ) ?
4316 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4317 'dns_get_record() disabled/blocked'
4318 ),
4319 'ip' => array(
4320 'IP',
4321 function_exists( 'gethostbyname' ) ?
4322 gethostbyname( $api_domain ) :
4323 'gethostbyname() disabled/blocked'
4324 ),
4325 ),
4326 ),
4327 'site' => array(
4328 'title' => 'Site',
4329 'rows' => array(
4330 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4331 'address' => array( 'Address', site_url() ),
4332 'host' => array(
4333 'HTTP_HOST',
4334 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4335 ),
4336 'hosting' => array(
4337 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4338 fs_request_get( 'hosting_company' ) :
4339 'Unknown',
4340 ),
4341 'server_addr' => array(
4342 'SERVER_ADDR',
4343 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4344 )
4345 )
4346 ),
4347 'user' => array(
4348 'title' => 'User',
4349 'rows' => array(
4350 'email' => array( 'Email', $current_user->user_email ),
4351 'first' => array( 'First', $current_user->user_firstname ),
4352 'last' => array( 'Last', $current_user->user_lastname )
4353 )
4354 ),
4355 'plugins' => array(
4356 'title' => 'Plugins',
4357 'rows' => array(
4358 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4359 )
4360 ),
4361 'php_info' => array(
4362 'title' => 'PHP Info',
4363 'rows' => array(
4364 'info' => array( $php_info )
4365 ),
4366 )
4367 );
4368
4369 // Allow the sections to be modified by other code.
4370 $sections = $this->apply_filters( 'email_template_sections', $sections );
4371
4372 return $sections;
4373 }
4374
4375 #endregion
4376
4377 #----------------------------------------------------------------------------------
4378 #region Initialization
4379 #----------------------------------------------------------------------------------
4380
4381 /**
4382 * Init plugin's Freemius instance.
4383 *
4384 * @author Vova Feldman (@svovaf)
4385 * @since 1.0.1
4386 *
4387 * @param number $id
4388 * @param string $public_key
4389 * @param bool $is_live
4390 * @param bool $is_premium
4391 */
4392 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4393 $this->_logger->entrance();
4394
4395 $this->dynamic_init( array(
4396 'id' => $id,
4397 'public_key' => $public_key,
4398 'is_live' => $is_live,
4399 'is_premium' => $is_premium,
4400 ) );
4401 }
4402
4403 /**
4404 * Dynamic initiator, originally created to support initiation
4405 * with parent_id for add-ons.
4406 *
4407 * @author Vova Feldman (@svovaf)
4408 * @since 1.0.6
4409 *
4410 * @param array $plugin_info
4411 *
4412 * @throws Freemius_Exception
4413 */
4414 function dynamic_init( array $plugin_info ) {
4415 $this->_logger->entrance();
4416
4417 $this->parse_settings( $plugin_info );
4418
4419 $this->register_after_settings_parse_hooks();
4420
4421 /**
4422 * If anonymous but there's already a user entity and the user's site is associated with a valid license or trial period, update the anonymous mode accordingly.
4423 *
4424 * @todo Remove this entire `if` block after several releases as starting from this version, the anonymous mode will already be updated accordingly after a purchase.
4425 */
4426 if ( $this->is_anonymous() ) {
4427 $is_network_level = ( $this->_is_network_active && fs_is_network_admin() );
4428
4429 if (
4430 ! $is_network_level ||
4431 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
4432 ) {
4433 if ( $this->is_paying_or_trial() ) {
4434 $this->reset_anonymous_mode( $is_network_level );
4435 }
4436 } else {
4437 $network = get_network();
4438
4439 if ( is_object( $network ) ) {
4440 $main_blog_id = $network->site_id;
4441 $first_install = $this->get_install_by_blog_id( $main_blog_id );
4442
4443 if ( is_object( $first_install ) ) {
4444 $this->_storage->network_install_blog_id = $main_blog_id;
4445 $this->_storage->network_user_id = $first_install->user_id;
4446 }
4447 }
4448 }
4449 }
4450
4451 if ( $this->should_stop_execution() ) {
4452 return;
4453 }
4454
4455 if ( ! $this->is_registered() ) {
4456 if ( $this->is_anonymous() ) {
4457 // If user skipped, no need to test connectivity.
4458 $this->_has_api_connection = true;
4459 $this->_is_on = true;
4460 } else {
4461 if ( false === $this->has_api_connectivity() ) {
4462 return;
4463 } else {
4464 if ( $this->_anonymous_mode ) {
4465 // Simulate anonymous mode.
4466 $this->_is_anonymous = true;
4467 }
4468 }
4469 }
4470 }
4471
4472 /**
4473 * This should be executed even if Freemius is off for the core module,
4474 * otherwise, the add-ons dialog box won't work properly. This is especially
4475 * relevant when the developer decided to turn FS off for existing users.
4476 *
4477 * @author Vova Feldman (@svovaf)
4478 */
4479 if ( $this->is_user_in_admin() &&
4480 'plugin-information' === fs_request_get( 'tab', false ) &&
4481 $this->should_use_freemius_updater_and_dialog() &&
4482 (
4483 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4484 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4485 )
4486 ) {
4487 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4488
4489 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4490 }
4491
4492 // Check if Freemius is on for the current plugin.
4493 // This MUST be executed after all the plugin variables has been loaded.
4494 if ( ! $this->is_registered() && ! $this->is_on() ) {
4495 return;
4496 }
4497
4498 $this->hook_callback_to_sync_cron();
4499
4500 if ( $this->has_api_connectivity() && ! self::is_cron() && $this->is_user_in_admin() ) {
4501 /**
4502 * Schedule daily data sync cron if:
4503 *
4504 * 1. User opted-in (for tracking).
4505 * 2. If skipped, but later upgraded (opted-in via upgrade).
4506 *
4507 * @author Vova Feldman (@svovaf)
4508 * @since 1.1.7.3
4509 *
4510 */
4511 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
4512 $this->maybe_schedule_sync_cron();
4513 }
4514
4515 /**
4516 * Check if requested for manual blocking background sync.
4517 */
4518 if ( fs_request_has( 'background_sync' ) ) {
4519 self::require_pluggable_essentials();
4520 self::wp_cookie_constants();
4521
4522 $this->run_manual_sync();
4523 }
4524 }
4525
4526 if ( $this->is_registered() ) {
4527 FS_Clone_Manager::instance()->maybe_resolve_new_subsite_install_automatically( $this );
4528
4529 $this->hook_callback_to_install_sync();
4530 }
4531
4532 if ( $this->is_addon() ) {
4533 if ( $this->is_parent_plugin_installed() ) {
4534 // Link to parent FS.
4535 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4536
4537 // Get parent plugin reference.
4538 $this->_parent_plugin = $this->_parent->get_plugin();
4539 }
4540 }
4541
4542 if ( $this->is_user_in_admin() ) {
4543 if ( $this->is_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 ( is_numeric( $next_schedule ) ) {
6562 return;
6563 }
6564
6565 $this->schedule_sync_cron();
6566 }
6567
6568 /**
6569 * Instead of running blocking install sync event, execute non blocking scheduled cron job.
6570 *
6571 * @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.
6572 *
6573 * @author Leo Fajardo (@leorw)
6574 * @since 2.9.1
6575 */
6576 private function maybe_schedule_install_sync_cron( $except_blog_id = 0 ) {
6577 if ( ! $this->is_user_in_admin() ) {
6578 return;
6579 }
6580
6581 if ( $this->is_clone() ) {
6582 return;
6583 }
6584
6585 if (
6586 // The event has been properly scheduled, so no need to reschedule it.
6587 is_numeric( $this->next_install_sync() )
6588 ) {
6589 return;
6590 }
6591
6592 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
6593 }
6594
6595 /**
6596 * @author Vova Feldman (@svovaf)
6597 * @since 1.1.7.3
6598 *
6599 * @param int $start_at Defaults to now.
6600 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6601 * @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.
6602 */
6603 private function schedule_sync_cron(
6604 $start_at = WP_FS__SCRIPT_START_TIME,
6605 $randomize_start = true,
6606 $except_blog_id = 0
6607 ) {
6608 $this->schedule_cron(
6609 'sync',
6610 'data_sync',
6611 'daily',
6612 $start_at,
6613 $randomize_start,
6614 $except_blog_id
6615 );
6616 }
6617
6618 /**
6619 * Add the actual sync function to the cron job hook.
6620 *
6621 * @author Vova Feldman (@svovaf)
6622 * @since 1.1.7.3
6623 */
6624 private function hook_callback_to_sync_cron() {
6625 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6626 }
6627
6628 /**
6629 * @author Vova Feldman (@svovaf)
6630 * @since 1.1.7.3
6631 *
6632 * @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.
6633 */
6634 private function clear_sync_cron( $is_network_clear = false ) {
6635 $this->_logger->entrance();
6636
6637 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6638 }
6639
6640 /**
6641 * Unix timestamp for next sync cron execution or false if not scheduled.
6642 *
6643 * @author Vova Feldman (@svovaf)
6644 * @since 1.1.7.3
6645 *
6646 * @return int|false
6647 */
6648 function next_sync_cron() {
6649 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6650 }
6651
6652 /**
6653 * Unix timestamp for previous sync cron execution or false if never executed.
6654 *
6655 * @author Vova Feldman (@svovaf)
6656 * @since 1.1.7.3
6657 *
6658 * @return int|false
6659 */
6660 function last_sync_cron() {
6661 return $this->cron_last_execution( 'sync' );
6662 }
6663
6664 #endregion Daily Sync Cron ------------------------------------------------------------------
6665
6666 #----------------------------------------------------------------------------------
6667 #region Async Install Sync
6668 #----------------------------------------------------------------------------------
6669
6670 /**
6671 * @author Vova Feldman (@svovaf)
6672 * @since 1.1.7.3
6673 *
6674 * @return bool
6675 */
6676 private function is_install_sync_scheduled() {
6677 return $this->is_cron_on( 'install_sync' );
6678 }
6679
6680 /**
6681 * Get the sync cron's executing blog ID.
6682 *
6683 * @author Vova Feldman (@svovaf)
6684 * @since 2.0.0
6685 *
6686 * @return int
6687 */
6688 private function get_install_sync_cron_blog_id() {
6689 return $this->get_cron_blog_id( 'install_sync' );
6690 }
6691
6692 /**
6693 * Unix timestamp for previous install sync cron execution or false if never executed.
6694 *
6695 * @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.
6696 *
6697 * @author Vova Feldman (@svovaf)
6698 * @since 1.1.7.3
6699 *
6700 * @return int|false
6701 */
6702 function last_install_sync() {
6703 return $this->cron_last_execution( 'install_sync' );
6704 }
6705
6706 /**
6707 * Unix timestamp for next install sync cron execution or false if not scheduled.
6708 *
6709 * @author Vova Feldman (@svovaf)
6710 * @since 1.1.7.3
6711 *
6712 * @return int|false
6713 */
6714 function next_install_sync() {
6715 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
6716 }
6717
6718 /**
6719 * Add the actual install sync function to the cron job hook.
6720 *
6721 * @author Vova Feldman (@svovaf)
6722 * @since 1.1.7.3
6723 */
6724 private function hook_callback_to_install_sync() {
6725 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
6726 }
6727
6728 /**
6729 * @author Vova Feldman (@svovaf)
6730 * @since 1.1.7.3
6731 *
6732 * @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.
6733 */
6734 private function clear_install_sync_cron( $is_network_clear = false ) {
6735 $this->_logger->entrance();
6736
6737 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
6738 }
6739
6740 /**
6741 * @author Vova Feldman (@svovaf)
6742 * @since 1.1.7.3
6743 * @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.
6744 */
6745 public function _run_sync_install() {
6746 $this->_logger->entrance();
6747
6748 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
6749 }
6750
6751 /**
6752 * The actual install(s) sync cron logic.
6753 *
6754 * @author Vova Feldman (@svovaf)
6755 * @since 2.0.0
6756 *
6757 * @param int[] $blog_ids
6758 * @param int|null $current_blog_id
6759 */
6760 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
6761 if ( $this->is_registered() ) {
6762 if ( 1 < count( $blog_ids ) ) {
6763 $this->sync_installs( array(), true );
6764 } else {
6765 $this->sync_install( array(), true );
6766 }
6767
6768 $this->maybe_sync_install_user();
6769 }
6770 }
6771
6772 #endregion Async Install Sync ------------------------------------------------------------------
6773
6774 /**
6775 * Show a notice that activation is currently pending.
6776 *
6777 * @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.
6778 *
6779 * @author Vova Feldman (@svovaf)
6780 * @since 1.0.7
6781 *
6782 * @param bool|string $email_address
6783 * @param bool $is_pending_trial Since 1.2.1.5
6784 * @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.
6785 * @param bool $has_upgrade_context Since 2.5.3
6786 * @param bool $support_email_address Since 2.5.3
6787 */
6788 function _add_pending_activation_notice(
6789 $email_address = false,
6790 $is_pending_trial = false,
6791 $is_suspicious_email = false,
6792 $has_upgrade_context = false,
6793 $support_email_address = false
6794 ) {
6795 if ( ! is_string( $email_address ) ) {
6796 $current_user = self::_get_current_wp_user();
6797 $email_address = $current_user->user_email;
6798 }
6799
6800 $formatted_message_args = array(
6801 "<b>{$this->get_plugin_name()}</b>",
6802 "<b>{$email_address}</b>",
6803 );
6804
6805 if ( ! $has_upgrade_context || ! fs_is_network_admin() ) {
6806 /* translators: %3$s: action (e.g.: "start the trial" or "complete the opt-in") */
6807 $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' );
6808
6809 $formatted_message_args[] = $is_pending_trial ?
6810 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
6811 $this->get_text_inline( 'complete the opt-in', 'complete-the-opt-in' );
6812
6813 $notice_title = $this->get_text_inline( 'Thanks!', 'thanks' );
6814 } else {
6815 /* translators: %3$s: What the user is expected to receive via email (e.g.: "the installation instructions" or "a license key") */
6816 $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.' );
6817
6818 if ( $this->has_release_on_freemius() ) {
6819 $formatted_message_args[] = $this->get_text_x_inline(
6820 'the installation instructions',
6821 'Part of the message telling the user what they should receive via email.',
6822 'the-installation-instructions-phrase'
6823 );
6824 } else {
6825 $formatted_message_args[] = $this->get_text_x_inline(
6826 'a license key',
6827 'Part of the message telling the user what they should receive via email.',
6828 'a-license-key-phrase'
6829 );
6830
6831 $formatted_message .= ( ' ' . sprintf(
6832 /* translators: %s: activation link (e.g.: <a>Click here</a>) */
6833 $this->get_text_inline( '%s to activate the license once you get it.', 'license-activation-link-message' ),
6834 sprintf(
6835 '<b><a href="%s">%s</a></b>',
6836 $this->get_activation_url( array(
6837 'fs_action' => 'reset_pending_activation_mode',
6838 'require_license' => 'true',
6839 'fs_unique_affix' => $this->get_unique_affix(),
6840 ) ),
6841 $this->get_text_x_inline( 'Click here', 'Part of an activation link message.', 'click-here' )
6842 )
6843 ) );
6844 }
6845
6846 $formatted_message_args[] = ( ! empty( $support_email_address ) ) ?
6847 ( "<b>{$support_email_address}</b>" ) :
6848 $this->get_text_x_inline(
6849 "the product's support email address",
6850 'Part of the message that tells the user to check their spam folder for a specific email.',
6851 'product-support-email-address-phrase'
6852 );
6853
6854 $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' ) );
6855
6856 $notice_title = $this->get_text_inline( 'Thanks for upgrading.', 'after-upgrade-thank-you-message' );
6857 }
6858
6859 $this->_admin_notices->add_sticky(
6860 vsprintf( $formatted_message, $formatted_message_args ),
6861 'activation_pending',
6862 $notice_title
6863 );
6864 }
6865
6866 /**
6867 * Check if currently in plugin activation.
6868 *
6869 * @author Vova Feldman (@svovaf)
6870 * @since 1.1.4
6871 *
6872 * @return bool
6873 */
6874 function is_plugin_activation() {
6875 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
6876
6877 return !empty($result);
6878 }
6879
6880 /**
6881 *
6882 * NOTE: admin_menu action executed before admin_init.
6883 *
6884 * @author Vova Feldman (@svovaf)
6885 * @since 1.0.7
6886 */
6887 function _admin_init_action() {
6888 $is_migration = $this->is_migration();
6889
6890 /**
6891 * Automatically redirect to connect/activation page after plugin activation.
6892 *
6893 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
6894 */
6895 if ( $this->is_plugin_activation() ) {
6896 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
6897
6898 if ( isset( $_GET['activate-multi'] ) ) {
6899 /**
6900 * Don't redirect if activating multiple plugins at once (bulk activation).
6901 */
6902 } else if (
6903 self::is_deactivation_snoozed() &&
6904 (
6905 // Either running the free code base.
6906 ! $this->is_premium() ||
6907 // Or if has a free version.
6908 ! $this->is_only_premium() ||
6909 // If premium only, don't redirect if license is activated.
6910 ( $this->is_registered() && ! $this->can_use_premium_code() )
6911 )
6912 ) {
6913 /**
6914 * 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.
6915 */
6916 } else if ( ! $is_migration ) {
6917 $this->_redirect_on_activation_hook();
6918 return;
6919 }
6920 }
6921
6922 if ( $is_migration ) {
6923 return;
6924 }
6925
6926 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
6927 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
6928
6929 $this->skip_connection( fs_is_network_admin() );
6930
6931 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
6932 }
6933
6934 if ( $this->is_network_activation_mode() &&
6935 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
6936 ) {
6937 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
6938
6939 $this->delegate_connection();
6940
6941 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
6942 }
6943
6944 $this->_add_upgrade_action_link();
6945
6946 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
6947 (
6948 ( true === $this->_storage->require_license_activation ) ||
6949 // Not registered nor anonymous.
6950 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
6951 // OR, network level and in network upgrade mode.
6952 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
6953 )
6954 ) {
6955 if ( ! $this->is_pending_activation() ) {
6956 if ( ! $this->is_activation_page() ) {
6957 /**
6958 * If a user visits any other admin page before activating the premium-only theme with a valid
6959 * license, reactivate the previous theme.
6960 *
6961 * @author Leo Fajardo (@leorw)
6962 * @since 1.2.2
6963 */
6964 if ( $this->is_theme() &&
6965 ! $this->has_settings_menu() &&
6966 ! isset( $_REQUEST['fs_action'] ) &&
6967 $this->can_activate_previous_theme()
6968 ) {
6969 if ( $this->is_only_premium() ) {
6970 $this->activate_previous_theme();
6971 return;
6972 }
6973
6974 if ( true === $this->_storage->require_license_activation ) {
6975 $this->_storage->require_license_activation = false;
6976 }
6977 }
6978
6979 if ( ! fs_is_network_admin() &&
6980 $this->is_network_activation_mode() &&
6981 ! $this->is_delegated_connection()
6982 ) {
6983 return;
6984 }
6985
6986 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
6987 if ( ! $this->_anonymous_mode &&
6988 ( ! $this->is_addon() || ! $this->_parent->is_anonymous() ) ) {
6989 // Show notice for new plugin installations.
6990 $this->_admin_notices->add(
6991 sprintf(
6992 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
6993 sprintf( '<b><a href="%s">%s</a></b>',
6994 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
6995 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
6996 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
6997 )
6998 ),
6999 '',
7000 'update-nag'
7001 );
7002 }
7003 } else {
7004 if ( $this->should_add_sticky_optin_notice() ) {
7005 $this->add_sticky_optin_admin_notice();
7006 }
7007
7008 if ( $this->has_filter( 'optin_pointer_element' ) ) {
7009 // Don't show admin nag if plugin update.
7010 wp_enqueue_script( 'wp-pointer' );
7011 wp_enqueue_style( 'wp-pointer' );
7012
7013 $this->_enqueue_connect_essentials();
7014
7015 add_action( 'admin_print_footer_scripts', array(
7016 $this,
7017 '_add_connect_pointer_script'
7018 ) );
7019 }
7020 }
7021 }
7022 }
7023
7024 if ( $this->show_opt_in_on_themes_page() &&
7025 $this->is_activation_page()
7026 ) {
7027 $this->_show_theme_activation_optin_dialog();
7028 }
7029 }
7030 }
7031
7032 /**
7033 * @author Vova Feldman (@svovaf)
7034 * @since 2.0.0
7035 *
7036 * @return bool
7037 */
7038 private function should_add_sticky_optin_notice() {
7039 if ( $this->is_addon() && $this->_parent->is_anonymous() ) {
7040 return false;
7041 }
7042
7043 if ( fs_is_network_admin() ) {
7044 if ( ! $this->_is_network_active ) {
7045 return false;
7046 }
7047
7048 if ( ! $this->is_network_activation_mode() ) {
7049 return false;
7050 }
7051
7052 return ! isset( $this->_storage->sticky_optin_added_ms );
7053 }
7054
7055 if ( ! $this->is_activation_mode() ) {
7056 return false;
7057 }
7058
7059 // If running from a blog admin and delegated the connection.
7060 return ! isset( $this->_storage->sticky_optin_added );
7061 }
7062
7063 /**
7064 * @author Leo Fajardo (@leorw)
7065 * @since 2.0.0
7066 */
7067 private function add_sticky_optin_admin_notice() {
7068 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7069 $this->_storage->sticky_optin_added = true;
7070 } else {
7071 $this->_storage->sticky_optin_added_ms = true;
7072 }
7073
7074 // Show notice for new plugin installations.
7075 $this->_admin_notices->add_sticky(
7076 sprintf(
7077 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7078 $this->_module_type,
7079 sprintf( '<b><a href="%s">%s</a></b>',
7080 $this->get_activation_url(),
7081 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7082 )
7083 ),
7084 'connect_account',
7085 '',
7086 'update-nag'
7087 );
7088 }
7089
7090 /**
7091 * Enqueue connect requires scripts and styles.
7092 *
7093 * @author Vova Feldman (@svovaf)
7094 * @since 1.1.4
7095 */
7096 function _enqueue_connect_essentials() {
7097 wp_enqueue_script( 'jquery' );
7098
7099 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.js' );
7100 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7101 }
7102
7103 /**
7104 * Add connect / opt-in pointer.
7105 *
7106 * @author Vova Feldman (@svovaf)
7107 * @since 1.1.4
7108 */
7109 function _add_connect_pointer_script() {
7110 $vars = array( 'id' => $this->_module_id );
7111 $pointer_content = fs_get_template( 'connect.php', $vars );
7112 ?>
7113 <script type="text/javascript">// <![CDATA[
7114 jQuery(document).ready(function ($) {
7115 if ('undefined' !== typeof(jQuery().pointer)) {
7116
7117 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7118
7119 if (element.length > 0) {
7120 var optin = $(element).pointer($.extend(true, {}, {
7121 content : <?php echo json_encode( $pointer_content ) ?>,
7122 position : {
7123 edge : 'left',
7124 align: 'center'
7125 },
7126 buttons : function () {
7127 // Don't show pointer buttons.
7128 return '';
7129 },
7130 pointerWidth: 482
7131 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7132
7133 <?php
7134 echo $this->apply_filters( 'optin_pointer_execute', "
7135
7136 optin.pointer('open');
7137
7138 // Tag the opt-in pointer with custom class.
7139 $('.wp-pointer #fs_connect')
7140 .parents('.wp-pointer.wp-pointer-top')
7141 .addClass('fs-opt-in-pointer');
7142
7143 ", 'element', 'optin' ) ?>
7144 }
7145 }
7146 });
7147 // ]]></script>
7148 <?php
7149 }
7150
7151 /**
7152 * Return current page's URL.
7153 *
7154 * @author Vova Feldman (@svovaf)
7155 * @since 1.0.7
7156 *
7157 * @return string
7158 */
7159 static function current_page_url() {
7160 $url = 'http';
7161
7162 if ( isset( $_SERVER["HTTPS"] ) ) {
7163 if ( $_SERVER["HTTPS"] == "on" ) {
7164 $url .= "s";
7165 }
7166 }
7167 $url .= "://";
7168 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7169 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7170 } else {
7171 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7172 }
7173
7174 return esc_url( $url );
7175 }
7176
7177 /**
7178 * Check if the current page is the plugin's main admin settings page.
7179 *
7180 * @author Vova Feldman (@svovaf)
7181 * @since 1.0.7
7182 *
7183 * @return bool
7184 */
7185 function _is_plugin_page() {
7186 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7187 fs_is_plugin_page( $this->_slug );
7188 }
7189
7190 /* Events
7191 ------------------------------------------------------------------------------------------------------------------*/
7192 /**
7193 * Delete site install from Database.
7194 *
7195 * @author Vova Feldman (@svovaf)
7196 * @since 1.0.1
7197 *
7198 * @param bool $store
7199 * @param int|null $blog_id Since 2.0.0
7200 *
7201 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7202 */
7203 function _delete_site( $store = true, $blog_id = null ) {
7204 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7205 }
7206
7207 /**
7208 * Delete site install from Database.
7209 *
7210 * @author Vova Feldman (@svovaf)
7211 * @since 1.2.2.7
7212 *
7213 * @param string $slug
7214 * @param string $module_type
7215 * @param bool $store
7216 * @param int|null $blog_id Since 2.0.0
7217 *
7218 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7219 */
7220 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7221 $sites = self::get_all_sites( $module_type, $blog_id );
7222
7223 $install_id = false;
7224
7225 if ( isset( $sites[ $slug ] ) ) {
7226 if ( is_object( $sites[ $slug ] ) ) {
7227 $install_id = $sites[ $slug ]->id;
7228 }
7229
7230 unset( $sites[ $slug ] );
7231
7232 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7233 }
7234
7235 return $install_id;
7236 }
7237
7238 /**
7239 * Delete plugin's plans information.
7240 *
7241 * @param bool $store Flush to Database if true.
7242 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7243 *
7244 * @author Vova Feldman (@svovaf)
7245 * @since 1.0.9
7246 */
7247 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7248 $this->_logger->entrance();
7249
7250 $plans = self::get_all_plans( $this->_module_type );
7251
7252 $plans_to_keep = array();
7253
7254 if ( $keep_associated_plans ) {
7255 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7256 foreach ( $plans_ids_to_keep as $plan_id ) {
7257 $plan = self::_get_plan_by_id( $plan_id );
7258 if ( is_object( $plan ) ) {
7259 $plans_to_keep[] = self::_encrypt_entity( $plan );
7260 }
7261 }
7262 }
7263
7264 if ( ! empty( $plans_to_keep ) ) {
7265 $plans[ $this->_slug ] = $plans_to_keep;
7266 } else {
7267 unset( $plans[ $this->_slug ] );
7268 }
7269
7270 $this->set_account_option( 'plans', $plans, $store );
7271 }
7272
7273 /**
7274 * Delete all plugin licenses.
7275 *
7276 * @author Vova Feldman (@svovaf)
7277 * @since 1.0.9
7278 *
7279 * @param bool $store
7280 */
7281 private function _delete_licenses( $store = true ) {
7282 $this->_logger->entrance();
7283
7284 $all_licenses = self::get_all_licenses();
7285
7286 unset( $all_licenses[ $this->_module_id ] );
7287
7288 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7289 }
7290
7291 /**
7292 * Check if Freemius was added on new plugin installation.
7293 *
7294 * @author Vova Feldman (@svovaf)
7295 * @since 1.1.5
7296 *
7297 * @return bool
7298 */
7299 function is_plugin_new_install() {
7300 return isset( $this->_storage->is_plugin_new_install ) &&
7301 $this->_storage->is_plugin_new_install;
7302 }
7303
7304 /**
7305 * Check if it's the first plugin release that is running Freemius.
7306 *
7307 * @author Vova Feldman (@svovaf)
7308 * @since 1.2.1.5
7309 *
7310 * @return bool
7311 */
7312 function is_first_freemius_powered_version() {
7313 return empty( $this->_storage->plugin_last_version );
7314 }
7315
7316 /**
7317 * @author Leo Fajardo (@leorw)
7318 * @since 1.2.2
7319 *
7320 * @return bool|string
7321 */
7322 private function get_previous_theme_slug() {
7323 return isset( $this->_storage->previous_theme ) ?
7324 $this->_storage->previous_theme :
7325 false;
7326 }
7327
7328 /**
7329 * @author Leo Fajardo (@leorw)
7330 * @since 1.2.2
7331 *
7332 * @return bool
7333 */
7334 private function can_activate_previous_theme() {
7335 return $this->can_activate_theme( $this->get_previous_theme_slug() );
7336 }
7337
7338 /**
7339 * @author Leo Fajardo (@leorw)
7340 * @since 2.5.0
7341 *
7342 * @return bool
7343 */
7344 private function can_activate_theme( $slug ) {
7345 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7346 $theme_instance = wp_get_theme( $slug );
7347
7348 return $theme_instance->exists();
7349 }
7350
7351 return false;
7352 }
7353
7354 /**
7355 * @author Leo Fajardo (@leorw)
7356 * @since 1.2.2
7357 */
7358 private function activate_previous_theme() {
7359 switch_theme( $this->get_previous_theme_slug() );
7360 unset( $this->_storage->previous_theme );
7361
7362 global $pagenow;
7363 if ( 'themes.php' === $pagenow ) {
7364 /**
7365 * Refresh the active theme information.
7366 *
7367 * @author Leo Fajardo (@leorw)
7368 * @since 1.2.2
7369 */
7370 fs_redirect( $this->admin_url( $pagenow ) );
7371 }
7372 }
7373
7374 /**
7375 * @author Leo Fajardo (@leorw)
7376 * @since 1.2.2
7377 *
7378 * @return string
7379 */
7380 function get_previous_theme_activation_url() {
7381 if ( ! $this->can_activate_previous_theme() ) {
7382 return '';
7383 }
7384
7385 /**
7386 * Activation URL
7387 *
7388 * @author Leo Fajardo (@leorw)
7389 * @since 1.2.2
7390 */
7391 return wp_nonce_url(
7392 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7393 'switch-theme_' . $this->get_previous_theme_slug()
7394 );
7395 }
7396
7397 /**
7398 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7399 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7400 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7401 * theme doesn't exist, then there will be no close button.
7402 *
7403 * @author Leo Fajardo (@leorw)
7404 * @since 1.2.2
7405 *
7406 * @param string $slug_or_name Old theme's slug or name.
7407 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7408 */
7409 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7410 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7411 $old_theme->get_stylesheet() :
7412 $slug_or_name;
7413
7414 $this->_activate_plugin_event_hook();
7415 }
7416
7417 /**
7418 * Plugin activated hook.
7419 *
7420 * @author Vova Feldman (@svovaf)
7421 * @since 1.0.1
7422 *
7423 * @uses FS_Api
7424 */
7425 function _activate_plugin_event_hook() {
7426 $this->_logger->entrance( 'slug = ' . $this->_slug );
7427
7428 if ( ! $this->is_user_admin() ) {
7429 return;
7430 }
7431
7432 $this->unregister_uninstall_hook();
7433
7434 // Clear API cache on activation.
7435 FS_Api::clear_cache();
7436
7437 $is_premium_version_activation = $this->is_plugin() ?
7438 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7439 $this->is_premium();
7440
7441 if ( $is_premium_version_activation && $this->is_pending_activation() ) {
7442 $this->clear_pending_activation_mode();
7443 }
7444
7445 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7446
7447 if ( $this->is_plugin() ) {
7448 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7449 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7450 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7451 $other_version_basename = $is_premium_version_activation ?
7452 $this->_free_plugin_basename :
7453 $this->premium_plugin_basename();
7454
7455 if ( ! $this->_is_network_active ) {
7456 /**
7457 * Themes are always network activated, but the ACTUAL activation is per site.
7458 *
7459 * During the activation, the plugin isn't yet active, therefore,
7460 * _is_network_active will be set to false even if it's a network level
7461 * activation. So we need to fix that by looking at the is_network_admin() value.
7462 *
7463 * @author Vova Feldman
7464 */
7465 $this->_is_network_active = (
7466 $this->_is_multisite_integrated &&
7467 fs_is_network_admin()
7468 );
7469 }
7470
7471 /**
7472 * If the other module version is active, deactivate it.
7473 *
7474 * is_plugin_active() checks if the plugin is active on the site or the network level and
7475 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7476 *
7477 * @author Leo Fajardo (@leorw)
7478 * @since 1.2.2
7479 */
7480 if (
7481 is_plugin_active( $other_version_basename ) &&
7482 $this->apply_filters( 'deactivate_on_activation', ! $this->is_parallel_activation() )
7483 ) {
7484 deactivate_plugins( $other_version_basename );
7485 }
7486 }
7487
7488 if ( $this->is_registered() ) {
7489 if ( $is_premium_version_activation ) {
7490 $this->reconnect_locally();
7491 }
7492
7493
7494 // Schedule re-activation event and sync.
7495 // $this->sync_install( array(), true );
7496 $this->maybe_schedule_install_sync_cron();
7497
7498 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7499 if ( $is_premium_version_activation ) {
7500 $this->_admin_notices->add(
7501 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7502 $this->get_text_x_inline( 'W00t',
7503 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7504 );
7505 }
7506 } else if ( $this->is_anonymous() ) {
7507 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7508 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7509 $network = true;
7510 } else {
7511 $plugin_version = isset( $this->_storage->is_anonymous ) ?
7512 $this->_storage->is_anonymous['version'] :
7513 null;
7514 $network = false;
7515 }
7516
7517 /**
7518 * Reset "skipped" click cache on the following:
7519 * 1. Freemius DEV mode.
7520 * 2. WordPress DEBUG mode.
7521 * 3. If a plugin and the user skipped the exact same version before.
7522 *
7523 * @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).
7524 *
7525 * @todo 4. If explicitly asked to retry after every activation.
7526 */
7527 if ( WP_FS__DEV_MODE ||
7528 (
7529 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7530 $this->get_plugin_version() == $plugin_version
7531 )
7532 ) {
7533 $this->reset_anonymous_mode( $network );
7534 }
7535 }
7536
7537 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7538
7539 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7540 /**
7541 * When activating an add-on, try to also activate a license.
7542 *
7543 * @author Leo Fajardo (@leorw)
7544 * @since 2.3.0
7545 */
7546 if ( ! $this->_is_network_active ) {
7547 $this->maybe_activate_addon_license();
7548 } else {
7549 $this->maybe_network_activate_addon_license();
7550 }
7551
7552 /**
7553 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7554 *
7555 * @author Leo Fajardo (@leorw)
7556 * @since 2.3.0
7557 */
7558 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7559
7560 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7561 $this->_storage->require_license_activation = false;
7562 }
7563 }
7564
7565 if (
7566 $is_premium_version_activation &&
7567 (
7568 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7569 (
7570 $this->is_registered() &&
7571 ! $is_trial_or_has_features_enabled_license
7572 )
7573 )
7574 ) {
7575 $this->_storage->require_license_activation = true;
7576 }
7577
7578 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7579 /**
7580 * If no previous version of plugin's version exist, it means that it's either
7581 * the first time that the plugin installed on the site, or the plugin was installed
7582 * before but didn't have Freemius integrated.
7583 *
7584 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7585 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7586 * only after immediate activation.
7587 *
7588 * @since 1.1.4
7589 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7590 */
7591 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7592 }
7593
7594 /**
7595 * Also flush when activating the premium version so that even if Freemius was off before, the API
7596 * connectivity test can be run again.
7597 *
7598 * @author Leo Fajardo (@leorw)
7599 * @since 2.2.3.1
7600 */
7601 $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation );
7602
7603 if ( ! $this->_anonymous_mode &&
7604 ( false !== $has_api_connectivity ) &&
7605 ! $this->_isAutoInstall
7606 ) {
7607 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7608 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7609 }
7610
7611 /**
7612 * Activation hook is executed after the plugin's main file is loaded, therefore,
7613 * after the plugin was loaded. The logic is located at activate_plugin()
7614 * ./wp-admin/includes/plugin.php.
7615 *
7616 * @author Vova Feldman (@svovaf)
7617 * @since 1.1.9
7618 */
7619 $this->_storage->was_plugin_loaded = true;
7620 }
7621
7622 /**
7623 * @author Leo Fajardo (@leorw)
7624 * @since 2.3.0
7625 */
7626 private function maybe_activate_addon_license() {
7627 $parent_fs = $this->get_parent_instance();
7628
7629 if (
7630 ! is_object( $parent_fs ) ||
7631 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7632 ) {
7633 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7634 return;
7635 }
7636
7637 $license = $this->get_active_parent_license();
7638 if ( ! is_object( $license ) ) {
7639 return;
7640 }
7641
7642 if (
7643 $this->is_bundle_license_auto_activation_enabled() &&
7644 ! empty( $license->products )
7645 ) {
7646 $this->activate_bundle_license( $license );
7647
7648 return;
7649 }
7650
7651 if ( ! $this->is_registered() ) {
7652 // Opt in with a license key.
7653 $this->opt_in(
7654 $parent_fs->get_current_or_network_user()->email,
7655 false,
7656 false,
7657 $license->secret_key,
7658 false,
7659 false,
7660 false,
7661 null,
7662 array(),
7663 true,
7664 $license->user_id
7665 );
7666 } else {
7667 // Activate the license.
7668 $install = $this->api_site_call(
7669 '/',
7670 'put',
7671 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7672 );
7673
7674 if ( ! FS_Api::is_api_error( $install ) ) {
7675 $this->_sync_addon_license( $this->get_id(), true );
7676 }
7677 }
7678 }
7679
7680 /**
7681 * @author Leo Fajardo (@leorw)
7682 * @since 2.3.0
7683 *
7684 * @param FS_Plugin_License $license
7685 */
7686 private function maybe_network_activate_addon_license( $license = null ) {
7687 $parent_fs = $this->get_parent_instance();
7688 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7689 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7690 return;
7691 }
7692
7693 $license = ( ! is_null( $license ) ) ?
7694 $license :
7695 $this->get_active_parent_license();
7696
7697 if ( ! is_object( $license ) ) {
7698 return;
7699 }
7700
7701 if (
7702 $this->is_bundle_license_auto_activation_enabled() &&
7703 ! empty( $license->products )
7704 ) {
7705 $this->activate_bundle_license( $license );
7706
7707 return;
7708 }
7709
7710 if ( ! $this->is_network_registered() ) {
7711 $sites = $this->get_sites_for_network_level_optin();
7712
7713 if ( count( $sites ) > $license->left() ) {
7714 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
7715 return;
7716 }
7717
7718 // Opt in with a license key.
7719 $this->opt_in(
7720 $parent_fs->get_user()->email,
7721 false,
7722 false,
7723 $license->secret_key,
7724 false,
7725 false,
7726 false,
7727 null,
7728 $sites,
7729 true,
7730 $license->user_id
7731 );
7732 } else {
7733 $blog_2_install_map = array();
7734 $site_ids = array();
7735
7736 $all_sites = Freemius::get_sites();
7737
7738 foreach ( $all_sites as $site ) {
7739 $blog_id = Freemius::get_site_blog_id( $site );
7740 $install = $this->get_install_by_blog_id( $blog_id );
7741
7742 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
7743 // Skip license activation for installs that are already associated with a license.
7744 continue;
7745 }
7746
7747 if ( is_object( $install ) ) {
7748 $blog_2_install_map[ $blog_id ] = $install;
7749 } else {
7750 $site_ids[] = $blog_id;
7751 }
7752 }
7753
7754 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
7755 return;
7756 }
7757
7758 $user = $this->get_current_or_network_user();
7759
7760 if ( ! empty( $blog_2_install_map ) ) {
7761 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
7762
7763 if ( true !== $result ) {
7764 return;
7765 }
7766 }
7767
7768 if ( ! empty( $site_ids ) ) {
7769 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
7770 }
7771 }
7772 }
7773
7774 /**
7775 * 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.
7776 *
7777 * @author Leo Fajardo (@leorw)
7778 * @since 2.4.0
7779 *
7780 * @param FS_Plugin_License $license
7781 * @param array $sites
7782 * @param int $blog_id
7783 */
7784 private function maybe_activate_bundle_license( $license = null, $sites = array(), $blog_id = 0 ) {
7785 if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
7786 $license = $this->_license;
7787 }
7788
7789 if ( ! is_object( $license ) ) {
7790 return;
7791 }
7792
7793 $parent_license = ( ! empty( $license->products ) ) ?
7794 $license :
7795 $this->get_active_parent_license( $license->secret_key );
7796
7797 if ( is_object( $parent_license ) ) {
7798 $this->activate_bundle_license( $parent_license, $sites, $blog_id );
7799 }
7800 }
7801
7802 /**
7803 * Try to activate a bundle license for all the bundle products installed on the site.
7804 * (1) If a child product install already has a license, the bundle license won't be activated.
7805 * (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.
7806 * (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.
7807 *
7808 * @author Leo Fajardo (@leorw)
7809 * @since 2.4.0
7810 *
7811 * @param FS_Plugin_License $license
7812 * @param array $sites
7813 * @param int $current_blog_id
7814 */
7815 private function activate_bundle_license( $license, $sites = array(), $current_blog_id = 0 ) {
7816 $is_network_admin = fs_is_network_admin();
7817
7818 $installs_by_blog_map = array();
7819 $site_info_by_blog_map = array();
7820
7821 /**
7822 * Try to activate the license for all supported products.
7823 *
7824 * @author Leo Fajardo
7825 */
7826 foreach ( $license->products as $product_id ) {
7827 $fs = self::get_instance_by_id( $product_id );
7828
7829 if ( ! is_object( $fs ) ) {
7830 continue;
7831 }
7832
7833 if ( ! $fs->has_paid_plan() ) {
7834 continue;
7835 }
7836
7837 if (
7838 ! $fs->is_addon() &&
7839 ! FS_Plan_Manager::instance()->has_paid_plan( $fs->_plans )
7840 ) {
7841 /**
7842 * The parent product can be free-only but can have its `has_paid_plan` flag set to `true` when
7843 * there is a context bundle.
7844 */
7845 continue;
7846 }
7847
7848 if ( $current_blog_id > 0 ) {
7849 $fs->switch_to_blog( $current_blog_id );
7850 }
7851
7852 if ( $fs->has_active_valid_license() ) {
7853 continue;
7854 }
7855
7856 if ( ! $is_network_admin || $current_blog_id > 0 ) {
7857 if ( $fs->is_network_active() && ! $fs->is_delegated_connection( $current_blog_id ) ) {
7858 // Do not try to activate the license in the site level if the product is network active and the connection was not delegated.
7859 continue;
7860 }
7861 } else {
7862 if ( ! $fs->is_network_active() ) {
7863 // Do not try to activate the license in the network level if the product is not network active.
7864 continue;
7865 }
7866
7867 if ( $fs->is_network_delegated_connection() ) {
7868 // Do not try to activate the license in the network level if the activation has been delegated to site admins.
7869 continue;
7870 }
7871
7872 $has_install_with_license = false;
7873
7874 // 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.
7875 $filtered_sites = array();
7876
7877 if ( empty( $sites ) ) {
7878 $all_sites = self::get_sites();
7879
7880 foreach ( $all_sites as $site ) {
7881 $sites[] = array( 'blog_id' => self::get_site_blog_id( $site ) );
7882 }
7883 } else {
7884 // Populate the map here to avoid calling `$fs->get_site_info( $site );` in the other `for` loop below.
7885 foreach ( $sites as $site ) {
7886 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
7887 continue;
7888 }
7889
7890 $site_info_by_blog_map[ $site['blog_id'] ] = $site;
7891 }
7892 }
7893
7894 foreach ( $sites as $site ) {
7895 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
7896 continue;
7897 }
7898
7899 $blog_id = $site['blog_id'];
7900
7901 if ( ! isset( $installs_by_blog_map[ $blog_id ] ) ) {
7902 $installs_by_blog_map[ $blog_id ] = self::get_all_sites( $fs->get_module_type(), $blog_id );
7903 }
7904
7905 $installs = $installs_by_blog_map[ $blog_id ];
7906 $install = null;
7907
7908 if ( isset( $installs[ $fs->get_slug() ] ) ) {
7909 $install = $installs[ $fs->get_slug() ];
7910
7911 if (
7912 is_object( $install ) &&
7913 (
7914 ! FS_Site::is_valid_id( $install->id ) ||
7915 ! FS_User::is_valid_id( $install->user_id ) ||
7916 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
7917 )
7918 ) {
7919 $install = null;
7920 }
7921 }
7922
7923 if (
7924 is_object( $install ) &&
7925 FS_Plugin_License::is_valid_id( $install->license_id )
7926 ) {
7927 $has_install_with_license = true;
7928 break;
7929 }
7930
7931 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
7932 // Site activation delegated, don't activate bundle license on the site in the network admin.
7933 continue;
7934 }
7935
7936 if ( ! isset( $site_info_by_blog_map[ $blog_id ] ) ) {
7937 $site_info_by_blog_map[ $blog_id ] = $fs->get_site_info( $site );
7938 }
7939
7940 $filtered_sites[] = $site_info_by_blog_map[ $blog_id ];
7941 }
7942
7943 if ( $has_install_with_license || empty( $filtered_sites ) ) {
7944 // 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.
7945 continue;
7946 }
7947
7948 $sites = $filtered_sites;
7949 }
7950
7951 $fs->activate_migrated_license(
7952 $license->secret_key,
7953 null,
7954 null,
7955 $sites,
7956 ( $current_blog_id > 0 ? $current_blog_id : null ),
7957 $license->user_id
7958 );
7959 }
7960 }
7961
7962 /**
7963 * Returns a parent license that can be activated for the context product.
7964 *
7965 * @author Leo Fajardo (@leorw)
7966 * @since 2.3.0
7967 *
7968 * @param string|null $license_key
7969 * @param bool $flush
7970 *
7971 * @return FS_Plugin_License
7972 */
7973 function get_active_parent_license( $license_key = null, $flush = true ) {
7974 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
7975
7976 $fs = $this;
7977
7978 if ( $this->is_addon() ) {
7979 $parent_instance = $this->get_parent_instance();
7980
7981 if ( is_object( $parent_instance ) && $parent_instance->is_registered() ) {
7982 $fs = $parent_instance;
7983 }
7984 }
7985
7986 $foreign_licenses = $fs->get_foreign_licenses_info(
7987 self::get_all_licenses( $this->get_parent_id() )
7988 );
7989
7990 if ( ! empty ( $foreign_licenses ) ) {
7991 $foreign_licenses = array(
7992 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
7993 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
7994 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
7995 );
7996
7997 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
7998 }
7999
8000 $result = $fs->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, $flush );
8001
8002 if (
8003 ! $this->is_api_result_object( $result, 'licenses' ) ||
8004 ! is_array( $result->licenses ) ||
8005 empty( $result->licenses )
8006 ) {
8007 return null;
8008 }
8009
8010 $parent_license = null;
8011
8012 if ( empty( $license_key ) ) {
8013 $parent_license = $result->licenses[0];
8014 } else {
8015 foreach ( $result->licenses as $license ) {
8016 if ( $license_key === $license->secret_key ) {
8017 $parent_license = $license;
8018 break;
8019 }
8020 }
8021 }
8022
8023 if ( ! is_null( $parent_license ) ) {
8024 $parent_license = new FS_Plugin_License( $parent_license );
8025 }
8026
8027 return $parent_license;
8028 }
8029
8030 /**
8031 * @author Leo Fajardo (@leorw)
8032 * @since 2.3.0
8033 *
8034 * @return array
8035 */
8036 function get_sites_for_network_level_optin() {
8037 $sites = array();
8038 $all_sites = self::get_sites();
8039
8040 foreach ( $all_sites as $site ) {
8041 $blog_id = self::get_site_blog_id( $site );
8042
8043 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
8044 ! $this->is_installed_on_site( $blog_id )
8045 ) {
8046 $sites[] = $this->get_site_info( $site );
8047 }
8048 }
8049
8050 return $sites;
8051 }
8052
8053 /**
8054 * Delete account.
8055 *
8056 * @author Vova Feldman (@svovaf)
8057 * @since 1.0.3
8058 *
8059 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8060 */
8061 function delete_account_event( $check_user = true ) {
8062 $this->_logger->entrance( 'slug = ' . $this->_slug );
8063
8064 if ( $check_user && ! $this->is_user_admin() ) {
8065 return;
8066 }
8067
8068 $this->do_action( 'before_account_delete' );
8069
8070 // Clear all admin notices.
8071 $this->_admin_notices->clear_all_sticky( false );
8072
8073 $this->_delete_site( false );
8074
8075 $delete_network_common_data = true;
8076
8077 if ( $this->_is_network_active ) {
8078 $installs = $this->get_blog_install_map();
8079
8080 // Don't delete common network data unless no other installs left.
8081 $delete_network_common_data = empty( $installs );
8082 }
8083
8084 if ( $delete_network_common_data ) {
8085 $this->_delete_plans( false );
8086
8087 $this->_delete_licenses( false );
8088
8089 // Delete add-ons related to plugin's account.
8090 $this->_delete_account_addons( false );
8091 }
8092
8093 // @todo Delete plans and licenses of add-ons.
8094
8095 self::$_accounts->store();
8096
8097 /**
8098 * IMPORTANT:
8099 * Clear crons must be executed before clearing all storage.
8100 * Otherwise, the cron will not be cleared.
8101 */
8102 if ( $delete_network_common_data ) {
8103 $this->clear_sync_cron();
8104 }
8105
8106 $this->clear_install_sync_cron();
8107
8108 // Clear all storage data.
8109 $this->_storage->clear_all( true, array(
8110 'is_delegated_connection',
8111 'connectivity_test',
8112 'is_on',
8113 ), false );
8114
8115 // Send delete event.
8116 $this->get_api_site_scope()->call( '/', 'delete' );
8117
8118 $this->do_action( 'after_account_delete' );
8119 }
8120
8121 /**
8122 * Delete network level account.
8123 *
8124 * @author Vova Feldman (@svovaf)
8125 * @since 2.0.0
8126 *
8127 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8128 */
8129 function delete_network_account_event( $check_user = true ) {
8130 $this->_logger->entrance( 'slug = ' . $this->_slug );
8131
8132 if ( $check_user && ! $this->is_user_admin() ) {
8133 return;
8134 }
8135
8136 $this->do_action( 'before_network_account_delete' );
8137
8138 // Clear all admin notices.
8139 $this->_admin_notices->clear_all_sticky();
8140
8141 $this->_delete_plans( false, false );
8142
8143 $this->_delete_licenses( false );
8144
8145 // Delete add-ons related to plugin's account.
8146 $this->_delete_account_addons( false );
8147
8148 // @todo Delete plans and licenses of add-ons.
8149
8150 self::$_accounts->store( true );
8151
8152 /**
8153 * IMPORTANT:
8154 * Clear crons must be executed before clearing all storage.
8155 * Otherwise, the cron will not be cleared.
8156 */
8157 $this->clear_sync_cron( true );
8158 $this->clear_install_sync_cron( true );
8159
8160 $sites = self::get_sites();
8161
8162 $install_ids = array();
8163 foreach ( $sites as $site ) {
8164 $blog_id = self::get_site_blog_id( $site );
8165
8166 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8167 continue;
8168 }
8169
8170 $install_id = $this->_delete_site( true, $blog_id );
8171
8172 // Clear all storage data.
8173 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8174
8175 if ( FS_Site::is_valid_id( $install_id ) ) {
8176 $install_ids[] = $install_id;
8177 }
8178
8179 switch_to_blog( $blog_id );
8180
8181 $this->do_action( 'after_account_delete' );
8182
8183 restore_current_blog();
8184 }
8185
8186 $this->_storage->clear_all( true, array(
8187 'connectivity_test',
8188 'is_on',
8189 ), true );
8190
8191 // Send delete event.
8192 if ( ! empty( $install_ids ) ) {
8193 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8194 }
8195
8196 $this->do_action( 'after_network_account_delete' );
8197 }
8198
8199 /**
8200 * Plugin deactivation hook.
8201 *
8202 * @author Vova Feldman (@svovaf)
8203 * @since 1.0.1
8204 */
8205 function _deactivate_plugin_hook() {
8206 $this->_logger->entrance( 'slug = ' . $this->_slug );
8207
8208 if ( ! $this->is_user_admin() ) {
8209 return;
8210 }
8211
8212 $is_network_deactivation = fs_is_network_admin();
8213 $storage_keys_for_removal = array();
8214
8215 $this->_admin_notices->clear_all_sticky();
8216
8217 $storage_keys_for_removal[] = 'sticky_optin_added';
8218 if ( isset( $this->_storage->sticky_optin_added ) ) {
8219 unset( $this->_storage->sticky_optin_added );
8220 }
8221
8222 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8223 // Remember that plugin was already installed.
8224 $this->_storage->is_plugin_new_install = false;
8225 }
8226
8227 // Hook to plugin uninstall.
8228 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8229
8230 $this->clear_module_main_file_cache();
8231 $this->clear_sync_cron( $this->_is_network_active );
8232 $this->clear_install_sync_cron();
8233
8234 if ( $this->is_registered() ) {
8235 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8236 FS_Plugin_Updater::instance( $this )->delete_update_data();
8237 }
8238
8239 if ( $is_network_deactivation ) {
8240 // Send deactivation event.
8241 $this->sync_installs( array(
8242 'is_active' => false,
8243 ) );
8244 } else {
8245 // Send deactivation event.
8246 $this->sync_install( array(
8247 'is_active' => false,
8248 ) );
8249 }
8250 } else {
8251 if ( false === $this->has_api_connectivity() && ! $this->is_premium() ) {
8252 // Reset connectivity test cache.
8253 $this->clear_connectivity_info();
8254
8255 $storage_keys_for_removal[] = 'connectivity_test';
8256 }
8257 }
8258
8259 if ( $is_network_deactivation ) {
8260 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8261 unset( $this->_storage->sticky_optin_added_ms );
8262 }
8263
8264 if ( ! empty( $storage_keys_for_removal ) ) {
8265 $sites = self::get_sites();
8266
8267 foreach ( $sites as $site ) {
8268 $blog_id = self::get_site_blog_id( $site );
8269
8270 foreach ( $storage_keys_for_removal as $key ) {
8271 $this->_storage->remove( $key, false, $blog_id );
8272 }
8273
8274 $this->_storage->save( $blog_id );
8275 }
8276 }
8277 }
8278
8279 // Clear API cache on deactivation.
8280 FS_Api::clear_cache();
8281
8282 $this->remove_sdk_reference();
8283 }
8284
8285 /**
8286 * @author Vova Feldman (@svovaf)
8287 * @since 1.1.6
8288 */
8289 private function remove_sdk_reference() {
8290 global $fs_active_plugins;
8291
8292 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8293 if ( $this->_plugin_basename == $data->plugin_path ) {
8294 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8295 break;
8296 }
8297 }
8298
8299 fs_fallback_to_newest_active_sdk();
8300 }
8301
8302 /**
8303 * @author Vova Feldman (@svovaf)
8304 * @since 1.1.3
8305 *
8306 * @param bool $is_anonymous
8307 * @param bool|int $network_or_blog_id Since 2.0.0
8308 */
8309 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8310 // Store information regarding skip to try and opt-in the user
8311 // again in the future.
8312 $skip_info = array(
8313 'is' => $is_anonymous,
8314 'timestamp' => WP_FS__SCRIPT_START_TIME,
8315 'version' => $this->get_plugin_version(),
8316 );
8317
8318 if ( true === $network_or_blog_id ) {
8319 $this->_storage->is_anonymous_ms = $skip_info;
8320 } else {
8321 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8322 }
8323
8324 $this->network_upgrade_mode_completed();
8325
8326 // Update anonymous mode cache.
8327 $this->_is_anonymous = $is_anonymous;
8328 }
8329
8330 /**
8331 * @author Vova Feldman (@svovaf)
8332 * @since 2.5.1
8333 *
8334 * @param bool|int $network_or_blog_id
8335 */
8336 private function unset_anonymous_mode( $network_or_blog_id = 0 ) {
8337 if ( true === $network_or_blog_id ) {
8338 unset( $this->_storage->is_anonymous_ms );
8339 } else {
8340 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8341 }
8342 }
8343
8344 /**
8345 * @author Vova Feldman (@svovaf)
8346 * @since 2.0.0
8347 *
8348 * @param int $blog_id Site ID.
8349 * @param int $user_id User ID.
8350 * @param string $domain Site domain.
8351 * @param string $path Site path.
8352 * @param int $network_id Network ID. Only relevant on multi-network installations.
8353 * @param array $meta Metadata. Used to set initial site options.
8354 *
8355 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8356 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8357 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8358 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8359 */
8360 public function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8361 $this->_logger->entrance();
8362
8363 if ( ! $this->_is_network_active ) {
8364 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
8365 return;
8366 }
8367
8368 $site = null;
8369 $new_blog_id = $blog_id;
8370
8371 if ( $this->is_premium() &&
8372 $this->is_network_connected() &&
8373 is_object( $this->_license ) &&
8374 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8375 $this->is_license_network_active( $blog_id )
8376 ) {
8377 /**
8378 * 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.
8379 */
8380 $current_blog_id = get_current_blog_id();
8381 $license = clone $this->_license;
8382
8383 $this->switch_to_blog( $blog_id );
8384
8385 // Opt-in with network user.
8386 $this->install_with_user(
8387 $this->get_network_user(),
8388 $license->secret_key,
8389 false,
8390 false,
8391 false
8392 );
8393
8394 if ( is_object( $this->_site ) ) {
8395 if ( $this->_site->license_id == $license->id ) {
8396 /**
8397 * If the license was activated successfully, sync the license data from the remote server.
8398 */
8399 $this->_license = $license;
8400 $this->sync_site_license();
8401 }
8402 }
8403
8404 $site = $this->_site;
8405
8406 $this->switch_to_blog( $current_blog_id );
8407
8408 if ( is_object( $site ) ) {
8409 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id, $site );
8410
8411 // Already connected (with or without a license), so no need to continue.
8412 return;
8413 }
8414 }
8415
8416 if ( $this->is_network_anonymous() ) {
8417 /**
8418 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8419 */
8420 $this->skip_site_connection( $blog_id );
8421 } else if ( $this->is_network_delegated_connection() ) {
8422 /**
8423 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8424 */
8425 $this->delegate_site_connection( $blog_id );
8426 } else if ( $this->is_network_connected() ) {
8427 /**
8428 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8429 */
8430 $current_blog_id = get_current_blog_id();
8431
8432 $this->switch_to_blog( $blog_id );
8433
8434 // Opt-in with network user.
8435 $this->install_with_user(
8436 $this->get_network_user(),
8437 false,
8438 false,
8439 false,
8440 false
8441 );
8442
8443 $site = $this->_site;
8444
8445 $this->switch_to_blog( $current_blog_id );
8446 } else {
8447 /**
8448 * If the super-admin mixed different options (connect, skip, delegated):
8449 * a) If at least one site connection was delegated, then automatically delegate connection.
8450 * 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.
8451 */
8452 $has_delegated_site = false;
8453
8454 $sites = self::get_sites();
8455 foreach ( $sites as $wp_site ) {
8456 $blog_id = self::get_site_blog_id( $wp_site );
8457
8458 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8459 $has_delegated_site = true;
8460 break;
8461 }
8462 }
8463
8464 if ( $has_delegated_site ) {
8465 $this->delegate_site_connection( $blog_id );
8466 } else {
8467 $this->skip_site_connection( $blog_id );
8468 }
8469 }
8470
8471 /**
8472 * 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.
8473 *
8474 * @author Leo Fajardo (@leorw)
8475 * @since 2.5.0
8476 */
8477 FS_Clone_Manager::instance()->store_blog_install_info( $new_blog_id, $site );
8478 }
8479
8480 /**
8481 * @author Vova Feldman (@svovaf)
8482 * @since 2.5.0
8483 *
8484 * @param \WP_Site $new_site
8485 * @param array $args
8486 */
8487 public function _after_wp_initialize_site_callback( WP_Site $new_site, $args ) {
8488 $this->_logger->entrance();
8489
8490 $this->_after_new_blog_callback(
8491 $new_site->id,
8492 // Dummy user ID (not in use).
8493 0,
8494 $new_site->domain,
8495 $new_site->path,
8496 $new_site->network_id,
8497 // Dummy meta, not in use.
8498 array()
8499 );
8500 }
8501
8502 /**
8503 * @author Vova Feldman (@svovaf)
8504 * @since 1.1.3
8505 *
8506 * @param bool|int|int[] $network_or_blog_ids Since 2.0.0.
8507 */
8508 private function reset_anonymous_mode( $network_or_blog_ids = false ) {
8509 if ( true === $network_or_blog_ids ) {
8510 $this->unset_anonymous_mode( true );
8511
8512 if ( fs_is_network_admin() ) {
8513 $this->_is_anonymous = null;
8514 }
8515
8516 // Rest anonymous mode for all non-delegated sub-sites.
8517 $blog_ids = $this->get_non_delegated_blog_ids();
8518 }
8519 else
8520 {
8521 if ( false === $network_or_blog_ids ) {
8522 $network_or_blog_ids = 0;
8523 }
8524
8525 $blog_ids = is_array( $network_or_blog_ids ) ?
8526 $network_or_blog_ids :
8527 array( $network_or_blog_ids );
8528
8529 foreach ( $blog_ids as $blog_id ) {
8530 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8531 $this->_is_anonymous = null;
8532 }
8533 }
8534 }
8535
8536 foreach ( $blog_ids as $blog_id ) {
8537 $this->unset_anonymous_mode( $blog_id );
8538 }
8539
8540 /**
8541 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8542 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8543 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8544 *
8545 * @author Leo Fajardo (@leorw)
8546 * @since 1.2.2
8547 */
8548 if ( ! $this->_is_network_active ) {
8549 $this->_is_anonymous = null;
8550 }
8551 }
8552
8553 /**
8554 * @author Leo Fajardo (@leorw)
8555 * @since 2.5.3
8556 */
8557 private function update_license_required_permissions_if_anonymous() {
8558 if ( ! $this->is_anonymous() ) {
8559 return;
8560 }
8561
8562 $this->reset_anonymous_mode( fs_is_network_admin() );
8563
8564 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
8565 'essentials' => true,
8566 'events' => true,
8567 'diagnostic' => false,
8568 'extensions' => false,
8569 'site' => false,
8570 ) );
8571 }
8572
8573 /**
8574 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8575 * deleting the account in the network level, the URL of the page to redirect to is correct.
8576 *
8577 * @author Leo Fajardo (@leorw)
8578 *
8579 * @since 2.1.3
8580 */
8581 private function maybe_set_slug_and_network_menu_exists_flag() {
8582 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8583 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8584 $this->_menu->get_slug() :
8585 $this->_slug
8586 );
8587 }
8588 }
8589
8590 /**
8591 * Clears the anonymous mode and redirects to the opt-in screen.
8592 *
8593 * @author Vova Feldman (@svovaf)
8594 * @since 1.1.7
8595 */
8596 function connect_again() {
8597 if ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) {
8598 return;
8599 }
8600
8601 if ( $this->is_anonymous() ) {
8602 $this->reset_anonymous_mode( fs_is_network_admin() );
8603 }
8604
8605 $activation_url_params = array();
8606
8607 if ( $this->is_pending_activation() ) {
8608 $this->clear_pending_activation_mode();
8609
8610 if ( fs_request_get_bool( 'require_license' ) ) {
8611 $activation_url_params['require_license'] = true;
8612 }
8613 }
8614
8615 $this->maybe_set_slug_and_network_menu_exists_flag();
8616
8617 fs_redirect( $this->get_activation_url( $activation_url_params ) );
8618 }
8619
8620 /**
8621 * Skip account connect, and set anonymous mode.
8622 *
8623 * @author Vova Feldman (@svovaf)
8624 * @since 1.1.1
8625 *
8626 * @param bool|int|int[] $network_or_blog_ids Since 2.5.1
8627 */
8628 function skip_connection( $network_or_blog_ids = false ) {
8629 $this->_logger->entrance();
8630
8631 $this->_admin_notices->remove_sticky( 'connect_account' );
8632
8633 if ( true === $network_or_blog_ids ) {
8634 $this->set_anonymous_mode( true, true );
8635
8636 if ( fs_is_network_admin() ) {
8637 $this->_is_anonymous = null;
8638 }
8639
8640 // Rest anonymous mode for all non-delegated sub-sites.
8641 $blog_ids = $this->get_non_delegated_blog_ids();
8642 }
8643 else
8644 {
8645 if ( false === $network_or_blog_ids ) {
8646 $network_or_blog_ids = 0;
8647 }
8648
8649 $blog_ids = is_array( $network_or_blog_ids ) ?
8650 $network_or_blog_ids :
8651 array( $network_or_blog_ids );
8652
8653 foreach ( $blog_ids as $blog_id ) {
8654 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8655 $this->_is_anonymous = null;
8656 }
8657 }
8658 }
8659
8660 foreach ( $blog_ids as $blog_id ) {
8661 $this->skip_site_connection( $blog_id );
8662 }
8663
8664 $this->network_upgrade_mode_completed();
8665 }
8666
8667 /**
8668 * Skip connection for specific site in the network.
8669 *
8670 * @author Vova Feldman (@svovaf)
8671 * @since 2.0.0
8672 *
8673 * @param int|null $blog_id
8674 * @param bool $send_skip
8675 */
8676 private function skip_site_connection( $blog_id = null ) {
8677 $this->_logger->entrance();
8678
8679 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8680
8681 $this->set_anonymous_mode( true, $blog_id );
8682 }
8683
8684 /**
8685 * Plugin version update hook.
8686 *
8687 * @author Vova Feldman (@svovaf)
8688 * @since 1.0.4
8689 */
8690 private function update_plugin_version_event() {
8691 $this->_logger->entrance();
8692
8693 if ( ! $this->is_registered() ) {
8694 return;
8695 }
8696
8697 $this->maybe_schedule_install_sync_cron();
8698 // $this->sync_install( array(), true );
8699 }
8700
8701 /**
8702 * Generate an MD5 signature of a plugins collection.
8703 * This helper methods used to identify changes in a plugins collection.
8704 *
8705 * @author Vova Feldman (@svovaf)
8706 * @since 2.0.0
8707 *
8708 * @param array [string]array $plugins
8709 *
8710 * @return string
8711 */
8712 private function get_plugins_thumbprint( $plugins ) {
8713 ksort( $plugins );
8714
8715 $thumbprint = '';
8716 foreach ( $plugins as $basename => $data ) {
8717 $thumbprint .= $data['slug'] . ',' .
8718 $data['Version'] . ',' .
8719 ( $data['is_active'] ? '1' : '0' ) . ';';
8720 }
8721
8722 return md5( $thumbprint );
8723 }
8724
8725 /**
8726 * Return a list of modified plugins since the last sync.
8727 *
8728 * Note:
8729 * There's no point to store a plugins counter since even if the number of
8730 * plugins didn't change, we still need to check if the versions are all the
8731 * same and the activity state is similar.
8732 *
8733 * @author Vova Feldman (@svovaf)
8734 * @since 1.1.8
8735 *
8736 * @return array|false
8737 */
8738 private function get_plugins_data_for_api() {
8739 // Alias.
8740 $site_active_plugins_option_name = 'active_plugins';
8741 $network_plugins_option_name = 'all_plugins';
8742
8743 /**
8744 * Collection of all site level active plugins.
8745 */
8746 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8747
8748 if ( ! is_object( $site_active_plugins_cache ) ) {
8749 $site_active_plugins_cache = (object) array(
8750 'timestamp' => '',
8751 'md5' => '',
8752 'plugins' => array(),
8753 );
8754 }
8755
8756 $time = time();
8757
8758 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
8759 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8760 ) {
8761 // Don't send plugin updates if last update was in the past 5 min.
8762 return false;
8763 }
8764
8765 // Write timestamp to lock the logic.
8766 $site_active_plugins_cache->timestamp = $time;
8767 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8768
8769 // Reload options from DB.
8770 self::$_accounts->load( true );
8771 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8772
8773 if ( $time != $site_active_plugins_cache->timestamp ) {
8774 // If timestamp is different, then another thread captured the lock.
8775 return false;
8776 }
8777
8778 /**
8779 * Collection of all plugins (network level).
8780 */
8781 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
8782
8783 if ( ! is_object( $network_plugins_cache ) ) {
8784 $network_plugins_cache = (object) array(
8785 'timestamp' => '',
8786 'md5' => '',
8787 'plugins' => array(),
8788 );
8789 }
8790
8791 // Check if there's a change in plugins.
8792 $network_plugins = self::get_network_plugins();
8793 $site_active_plugins = self::get_site_active_plugins();
8794
8795 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
8796 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
8797
8798 // Check if plugins status changed (version or active/inactive).
8799 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
8800 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
8801
8802 if ( ! $network_plugins_changed &&
8803 ! $site_active_plugins_changed
8804 ) {
8805 // No changes.
8806 return array();
8807 }
8808
8809 $plugins_update_data = array();
8810
8811 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
8812 if ( ! isset( $network_plugins[ $basename ] ) ) {
8813 // Plugin uninstalled.
8814 $uninstalled_plugin_data = $data;
8815 $uninstalled_plugin_data['is_active'] = false;
8816 $uninstalled_plugin_data['is_uninstalled'] = true;
8817 $plugins_update_data[] = $uninstalled_plugin_data;
8818
8819 unset( $network_plugins[ $basename ] );
8820
8821 unset( $network_plugins_cache->plugins[ $basename ] );
8822 unset( $site_active_plugins_cache->plugins[ $basename ] );
8823
8824 continue;
8825 }
8826
8827 $was_active = $data['is_active'] ||
8828 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8829 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
8830 $is_active = $network_plugins[ $basename ]['is_active'] ||
8831 ( isset( $site_active_plugins[ $basename ] ) &&
8832 $site_active_plugins[ $basename ]['is_active'] );
8833
8834 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8835 isset( $site_active_plugins[ $basename ] )
8836 ) {
8837 // Plugin was site level activated.
8838 $site_active_plugins_cache->plugins[ $basename ] = array(
8839 'slug' => $network_plugins[ $basename ]['slug'],
8840 'version' => $network_plugins[ $basename ]['Version'],
8841 'title' => $network_plugins[ $basename ]['Name'],
8842 'is_active' => $is_active,
8843 'is_uninstalled' => false,
8844 );
8845 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8846 ! isset( $site_active_plugins[ $basename ] )
8847 ) {
8848 // Plugin was site level deactivated.
8849 unset( $site_active_plugins_cache->plugins[ $basename ] );
8850 }
8851
8852 $prev_version = $data['version'];
8853 $current_version = $network_plugins[ $basename ]['Version'];
8854
8855 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
8856 // Plugin activated or deactivated, or version changed.
8857
8858 if ( $was_active !== $is_active ) {
8859 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
8860 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
8861 }
8862 }
8863
8864 if ( $prev_version !== $current_version ) {
8865 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
8866 }
8867
8868 $updated_plugin_data = $data;
8869 $updated_plugin_data['is_active'] = $is_active;
8870 $updated_plugin_data['version'] = $current_version;
8871 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
8872 $plugins_update_data[] = $updated_plugin_data;
8873 }
8874 }
8875
8876 // Find new plugins that weren't yet seen before.
8877 foreach ( $network_plugins as $basename => $data ) {
8878 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
8879 // New plugin.
8880 $new_plugin = array(
8881 'slug' => $data['slug'],
8882 'version' => $data['Version'],
8883 'title' => $data['Name'],
8884 'is_active' => $data['is_active'],
8885 'is_uninstalled' => false,
8886 );
8887
8888 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
8889
8890 $is_site_level_active = (
8891 isset( $site_active_plugins[ $basename ] ) &&
8892 $site_active_plugins[ $basename ]['is_active']
8893 );
8894
8895 /**
8896 * If not network active, set the activity status based on the site-level plugin status.
8897 */
8898 if ( ! $new_plugin['is_active'] ) {
8899 $new_plugin['is_active'] = $is_site_level_active;
8900 }
8901
8902 $plugins_update_data[] = $new_plugin;
8903
8904 if ( isset( $site_active_plugins[ $basename ] ) ) {
8905 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
8906 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
8907 }
8908 }
8909 }
8910
8911 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
8912 $site_active_plugins_cache->timestamp = $time;
8913 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8914
8915 $network_plugins_cache->md5 = $network_plugins_thumbprint;
8916 $network_plugins_cache->timestamp = $time;
8917 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
8918
8919 return $plugins_update_data;
8920 }
8921
8922 /**
8923 * Return a list of modified themes since the last sync.
8924 *
8925 * Note:
8926 * There's no point to store a themes counter since even if the number of
8927 * themes didn't change, we still need to check if the versions are all the
8928 * same and the activity state is similar.
8929 *
8930 * @author Vova Feldman (@svovaf)
8931 * @since 1.1.8
8932 *
8933 * @return array|false
8934 */
8935 private function get_themes_data_for_api() {
8936 // Alias.
8937 $option_name = 'all_themes';
8938
8939 $all_cached_themes = self::$_accounts->get_option( $option_name );
8940
8941 if ( ! is_object( $all_cached_themes ) ) {
8942 $all_cached_themes = (object) array(
8943 'timestamp' => '',
8944 'md5' => '',
8945 'themes' => array(),
8946 );
8947 }
8948
8949 $time = time();
8950
8951 if ( ! empty( $all_cached_themes->timestamp ) &&
8952 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8953 ) {
8954 // Don't send theme updates if last update was in the past 5 min.
8955 return false;
8956 }
8957
8958 // Write timestamp to lock the logic.
8959 $all_cached_themes->timestamp = $time;
8960 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
8961
8962 // Reload options from DB.
8963 self::$_accounts->load( true );
8964 $all_cached_themes = self::$_accounts->get_option( $option_name );
8965
8966 if ( $time != $all_cached_themes->timestamp ) {
8967 // If timestamp is different, then another thread captured the lock.
8968 return false;
8969 }
8970
8971 // Get active theme.
8972 $active_theme = wp_get_theme();
8973 $active_theme_stylesheet = $active_theme->get_stylesheet();
8974
8975 // Check if there's a change in themes.
8976 $all_themes = wp_get_themes();
8977
8978 // Check if themes changed.
8979 ksort( $all_themes );
8980
8981 $themes_signature = '';
8982 foreach ( $all_themes as $slug => $data ) {
8983 $is_active = ( $slug === $active_theme_stylesheet );
8984 $themes_signature .= $slug . ',' .
8985 $data->version . ',' .
8986 ( $is_active ? '1' : '0' ) . ';';
8987 }
8988
8989 // Check if themes status changed (version or active/inactive).
8990 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
8991
8992 $themes_update_data = array();
8993
8994 if ( $themes_changed ) {
8995 // Change in themes, report changes.
8996
8997 // Update existing themes info.
8998 foreach ( $all_cached_themes->themes as $slug => $data ) {
8999 $is_active = ( $slug === $active_theme_stylesheet );
9000
9001 if ( ! isset( $all_themes[ $slug ] ) ) {
9002 // Plugin uninstalled.
9003 $uninstalled_theme_data = $data;
9004 $uninstalled_theme_data['is_active'] = false;
9005 $uninstalled_theme_data['is_uninstalled'] = true;
9006 $themes_update_data[] = $uninstalled_theme_data;
9007
9008 unset( $all_themes[ $slug ] );
9009 unset( $all_cached_themes->themes[ $slug ] );
9010 } else if ( $data['is_active'] !== $is_active ||
9011 $data['version'] !== $all_themes[ $slug ]->version
9012 ) {
9013 // Plugin activated or deactivated, or version changed.
9014
9015 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
9016 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
9017
9018 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
9019 }
9020 }
9021
9022 // Find new themes that weren't yet seen before.
9023 foreach ( $all_themes as $slug => $data ) {
9024 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
9025 $is_active = ( $slug === $active_theme_stylesheet );
9026
9027 // New plugin.
9028 $new_plugin = array(
9029 'slug' => $slug,
9030 'version' => $data->version,
9031 'title' => $data->name,
9032 'is_active' => $is_active,
9033 'is_uninstalled' => false,
9034 );
9035
9036 $themes_update_data[] = $new_plugin;
9037 $all_cached_themes->themes[ $slug ] = $new_plugin;
9038 }
9039 }
9040
9041 $all_cached_themes->md5 = md5( $themes_signature );
9042 $all_cached_themes->timestamp = time();
9043 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9044 }
9045
9046 return $themes_update_data;
9047 }
9048
9049 /**
9050 * Get site data for API install request.
9051 *
9052 * @author Vova Feldman (@svovaf)
9053 * @since 1.1.2
9054 *
9055 * @param string[] $override
9056 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9057 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9058 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, title, and URL).
9059 *
9060 * @return array
9061 */
9062 private function get_install_data_for_api(
9063 array $override,
9064 $include_plugins = true,
9065 $include_themes = true,
9066 $include_blog_data = true
9067 ) {
9068 // Alias.
9069 $permissions = FS_Permission_Manager::instance( $this );
9070
9071 if ( $permissions->is_extensions_tracking_allowed() ) {
9072 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
9073 /**
9074 * @since 1.1.8 Also send plugin updates.
9075 */
9076 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9077 $plugins = $this->get_plugins_data_for_api();
9078 if ( ! empty( $plugins ) ) {
9079 $override['plugins'] = $plugins;
9080 }
9081 }
9082 }
9083
9084 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
9085 /**
9086 * @since 1.1.8 Also send themes updates.
9087 */
9088 if ( $include_themes && ! isset( $override['themes'] ) ) {
9089 $themes = $this->get_themes_data_for_api();
9090 if ( ! empty( $themes ) ) {
9091 $override['themes'] = $themes;
9092 }
9093 }
9094 }
9095 }
9096
9097 $versions = $this->get_versions();
9098
9099 $blog_data = array();
9100 if ( $include_blog_data ) {
9101 $blog_data['url'] = self::get_unfiltered_site_url();
9102
9103 if ( $permissions->is_diagnostic_tracking_allowed() ) {
9104 $blog_data = array_merge( $blog_data, array(
9105 'language' => self::get_sanitized_language(),
9106 'title' => get_bloginfo( 'name' ),
9107 ) );
9108 }
9109 }
9110
9111 return array_merge( $versions, $blog_data, array(
9112 'version' => $this->get_plugin_version(),
9113 'is_premium' => $this->is_premium(),
9114 // Special params.
9115 'is_active' => true,
9116 'is_uninstalled' => false,
9117 ), $override );
9118 }
9119
9120 /**
9121 * Update installs details.
9122 *
9123 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
9124 *
9125 * @author Vova Feldman (@svovaf)
9126 * @since 2.0.0
9127 *
9128 * @param string[] string $override
9129 * @param bool $only_diff
9130 * @param bool $is_keepalive
9131 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9132 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9133 *
9134 * @return array
9135 */
9136 private function get_installs_data_for_api(
9137 array $override,
9138 $only_diff = false,
9139 $is_keepalive = false,
9140 $include_plugins = true,
9141 $include_themes = true
9142 ) {
9143 /**
9144 * @since 1.1.8 Also send plugin updates.
9145 */
9146 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9147 // $plugins = $this->get_plugins_data_for_api();
9148 // if ( ! empty( $plugins ) ) {
9149 // $override['plugins'] = $plugins;
9150 // }
9151 // }
9152 /**
9153 * @since 1.1.8 Also send themes updates.
9154 */
9155 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9156 // $themes = $this->get_themes_data_for_api();
9157 // if ( ! empty( $themes ) ) {
9158 // $override['themes'] = $themes;
9159 // }
9160 // }
9161
9162 // Common properties.
9163 $versions = $this->get_versions();
9164 $common = array_merge( $versions, array(
9165 'version' => $this->get_plugin_version(),
9166 'is_premium' => $this->is_premium(),
9167 ), $override );
9168
9169
9170 $is_common_diff_for_any_site = false;
9171 $common_diff_union = array();
9172
9173 $installs_data = array();
9174
9175 $sites = self::get_sites();
9176
9177 $subsite_data_for_api_by_install_id = array();
9178 $install_url_by_install_id = array();
9179 $subsite_registration_date_by_install_id = array();
9180
9181 foreach ( $sites as $site ) {
9182 $blog_id = self::get_site_blog_id( $site );
9183
9184 $install = $this->get_install_by_blog_id( $blog_id );
9185
9186 if ( is_object( $install ) ) {
9187 if ( $install->user_id != $this->_user->id ) {
9188 // Install belongs to a different owner.
9189 continue;
9190 }
9191
9192 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
9193 // Don't send updates regarding opted-out installs.
9194 continue;
9195 }
9196
9197 $install_data = $this->get_site_info( $site, true );
9198
9199 if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $install_data['blog_id'] ) ) {
9200 continue;
9201 }
9202
9203 $uid = $install_data['uid'];
9204 $url = $install_data['url'];
9205 $registration_date = $install_data['registration_date'];
9206
9207 if ( isset( $subsite_data_for_api_by_install_id[ $install->id ] ) ) {
9208 $clone_subsite_data = $subsite_data_for_api_by_install_id[ $install->id ];
9209 $clone_install_url = $install_url_by_install_id[ $install->id ];
9210 $clone_subsite_registration_date = $subsite_registration_date_by_install_id[ $install->id ];
9211
9212 $skip = false;
9213
9214 if (
9215 ! empty( $install_data['registration_date'] ) &&
9216 ! empty( $clone_subsite_registration_date )
9217 ) {
9218 /**
9219 * 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.
9220 *
9221 * @author Leo Fajardo (@leorw)
9222 * @since 2.5.1
9223 */
9224 $skip = ( strtotime( $install_data['registration_date'] ) > strtotime( $clone_subsite_registration_date ) );
9225 } else if (
9226 /**
9227 * 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.
9228 *
9229 * @author Leo Fajardo (@leorw)
9230 * @since 2.5.0
9231 */
9232 fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) ||
9233 fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $url ) )
9234 ) {
9235 $skip = true;
9236 }
9237
9238 if ( $skip ) {
9239 // 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.
9240 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
9241 continue;
9242 }
9243 }
9244
9245 unset( $install_data['blog_id'] );
9246 unset( $install_data['uid'] );
9247 unset( $install_data['url'] );
9248 unset( $install_data['registration_date'] );
9249
9250 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9251 $install_data['is_uninstalled'] = $install->is_uninstalled;
9252
9253 $common_diff = null;
9254 $is_common_diff = false;
9255 if ( $only_diff ) {
9256 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9257 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9258
9259 $is_common_diff = ! empty( $common_diff );
9260
9261 if ( $is_common_diff ) {
9262 foreach ( $common_diff as $k => $v ) {
9263 if ( ! isset( $common_diff_union[ $k ] ) ) {
9264 $common_diff_union[ $k ] = $v;
9265 }
9266 }
9267 }
9268
9269 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9270 }
9271
9272 if ( ! empty( $install_data ) || $is_common_diff || $is_keepalive ) {
9273 // Add install ID and site unique ID.
9274 $install_data['id'] = $install->id;
9275 $install_data['uid'] = $uid;
9276 $install_data['url'] = $url;
9277
9278 $subsite_data_for_api_by_install_id[ $install->id ] = $install_data;
9279 $install_url_by_install_id[ $install->id ] = $install->url;
9280 $subsite_registration_date_by_install_id[ $install->id ] = $registration_date;
9281 }
9282 }
9283 }
9284
9285 restore_current_blog();
9286
9287 $installs_data = array_merge(
9288 $installs_data,
9289 array_values( $subsite_data_for_api_by_install_id )
9290 );
9291
9292 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9293 if ( ! $only_diff ) {
9294 $installs_data[] = $common;
9295 } else if ( ! empty( $common_diff_union ) ) {
9296 $installs_data[] = $common_diff_union;
9297 }
9298 }
9299
9300 foreach ( $installs_data as &$data ) {
9301 $data = (object) $data;
9302 }
9303
9304 return $installs_data;
9305 }
9306
9307 /**
9308 * Compare site actual data to the stored install data and return the differences for an API data sync.
9309 *
9310 * @author Vova Feldman (@svovaf)
9311 * @since 2.0.0
9312 *
9313 * @param array $site
9314 * @param FS_Site $install
9315 * @param string[] string $override
9316 *
9317 * @return array
9318 */
9319 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9320 $diff = array();
9321 $special = array();
9322 $special_override = false;
9323
9324 foreach ( $site as $p => $v ) {
9325 if ( property_exists( $install, $p ) ) {
9326 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9327 $install->{$p} != $v
9328 ) {
9329 $val = self::get_api_sanitized_property( $p, $v );
9330
9331 if ( $install->{$p} != $val ) {
9332 $install->{$p} = $val;
9333 $diff[ $p ] = $val;
9334 }
9335 }
9336 } else {
9337 $special[ $p ] = $v;
9338
9339 if ( isset( $override[ $p ] ) ||
9340 'plugins' === $p ||
9341 'themes' === $p
9342 ) {
9343 $special_override = true;
9344 }
9345 }
9346 }
9347
9348 if ( $special_override || 0 < count( $diff ) ) {
9349 // Add special params only if has at least one
9350 // standard param, or if explicitly requested to
9351 // override a special param or a param which is not exist
9352 // in the install object.
9353 $diff = array_merge( $diff, $special );
9354 }
9355
9356 return $diff;
9357 }
9358
9359 /**
9360 * @author Leo Fajardo (@leorw)
9361 * @since 2.5.1
9362 */
9363 private function send_pending_clone_update_once() {
9364 $this->_logger->entrance();
9365
9366 if ( ! empty( $this->_storage->clone_id ) ) {
9367 return;
9368 }
9369
9370 $install_clone = $this->get_api_site_scope()->call(
9371 '/clones',
9372 'post',
9373 array( 'site_url' => self::get_unfiltered_site_url() )
9374 );
9375
9376 if ( $this->is_api_result_entity( $install_clone ) ) {
9377 $this->_storage->clone_id = $install_clone->id;
9378 }
9379 }
9380
9381 /**
9382 * @author Leo Fajardo (@leorw)
9383 * @since 2.5.1
9384 *
9385 * @param string $resolution_type
9386 * @param FS_Site $clone_context_install
9387 */
9388 function send_clone_resolution_update( $resolution_type, $clone_context_install ) {
9389 $this->_logger->entrance();
9390
9391 if ( empty( $this->_storage->clone_id ) ) {
9392 return;
9393 }
9394
9395 $new_install_id = null;
9396 $current_site = null;
9397
9398 $flush = false;
9399
9400 /**
9401 * 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.
9402 */
9403 if ( $clone_context_install->id != $this->_site->id ) {
9404 $new_install_id = $this->_site->id;
9405 $current_site = $this->_site;
9406 $this->_site = $clone_context_install;
9407
9408 $flush = true;
9409 }
9410
9411 $this->get_api_site_scope( $flush )->call(
9412 "/clones/{$this->_storage->clone_id}",
9413 'put',
9414 array(
9415 'resolution' => $resolution_type,
9416 'new_install_id' => $new_install_id,
9417 )
9418 );
9419
9420 if ( is_object( $current_site ) ) {
9421 /**
9422 * Ensure that the install scope entity is updated back to the previous install entity.
9423 */
9424 $this->_site = $current_site;
9425
9426 // Restore the previous install scope entity of the API.
9427 $this->get_api_site_scope( true );
9428 }
9429 }
9430
9431 /**
9432 * Update install only if changed.
9433 *
9434 * @author Vova Feldman (@svovaf)
9435 * @since 1.0.9
9436 *
9437 * @param string[] string $override
9438 * @param bool $flush
9439 * @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.
9440 *
9441 * @return false|object|string
9442 */
9443 private function send_install_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9444 $this->_logger->entrance();
9445
9446 $check_properties = $this->get_install_data_for_api( $override );
9447
9448 if ( $flush ) {
9449 $params = $check_properties;
9450 } else {
9451 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9452 }
9453
9454 if ( empty( $params ) ) {
9455 $keepalive_only_update = $this->should_send_keepalive_update();
9456
9457 if ( ! $keepalive_only_update ) {
9458 /**
9459 * There are no updates to send including keepalive.
9460 *
9461 * @author Leo Fajardo (@leorw)
9462 * @since 2.2.3
9463 */
9464 return false;
9465 }
9466 }
9467
9468 if ( $is_two_way_sync ) {
9469 /**
9470 * Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9471 *
9472 * @author Leo Fajardo (@leorw)
9473 * @since 2.2.3
9474 */
9475 if ( ! is_multisite() ) {
9476 // Update last install sync timestamp.
9477 $this->set_cron_execution_timestamp( 'install_sync' );
9478 }
9479
9480 $params['uid'] = $this->get_anonymous_id();
9481 }
9482
9483 $this->set_keepalive_timestamp();
9484
9485 // Send updated values to FS.
9486 $site = $this->api_site_call( '/', 'put', $params, true );
9487
9488 if ( $is_two_way_sync && $this->is_api_result_entity( $site ) ) {
9489 /**
9490 * Clear scheduled install sync after a two-way sync call.
9491 *
9492 * @author Leo Fajardo (@leorw)
9493 * @since 2.2.3
9494 */
9495 if ( ! is_multisite() ) {
9496 // I successfully sent install update, clear scheduled sync if exist.
9497 $this->clear_install_sync_cron();
9498 }
9499 }
9500
9501 return $site;
9502 }
9503
9504 /**
9505 * Update installs only if changed.
9506 *
9507 * @author Vova Feldman (@svovaf)
9508 * @since 2.0.0
9509 *
9510 * @param string[] string $override
9511 * @param bool $flush
9512 * @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.
9513 *
9514 * @return false|object|string
9515 */
9516 private function send_installs_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9517 $this->_logger->entrance();
9518
9519 /**
9520 * Pass `true` to use the network level storage since the update is for many installs.
9521 *
9522 * @author Leo Fajardo (@leorw)
9523 * @since 2.2.3
9524 */
9525 $should_send_keepalive = $this->should_send_keepalive_update( true );
9526
9527 $installs_data = $this->get_installs_data_for_api( $override, ! $flush, $should_send_keepalive );
9528
9529 if ( empty( $installs_data ) ) {
9530 return false;
9531 }
9532
9533 if ( $is_two_way_sync ) {
9534 // Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9535 $this->set_cron_execution_timestamp( 'install_sync' );
9536 }
9537
9538 /**
9539 * Pass `true` to use the network level storage since the update is for many installs.
9540 *
9541 * @author Leo Fajardo (@leorw)
9542 * @since 2.2.3
9543 */
9544 $this->set_keepalive_timestamp( true );
9545
9546 // Send updated values to FS.
9547 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9548
9549 if ( $is_two_way_sync && $this->is_api_result_object( $result, 'installs' ) ) {
9550 // I successfully sent a two-way installs update, clear the scheduled install sync if it exists.
9551 $this->clear_install_sync_cron();
9552 }
9553
9554 return $result;
9555 }
9556
9557 /**
9558 * @author Leo Fajardo (@leorw)
9559 *
9560 * @param bool|null $use_network_level_storage
9561 *
9562 * @return bool
9563 */
9564 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9565 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9566
9567 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9568 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9569 return true;
9570 } else {
9571 // 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.
9572 return ( 7 == rand( 1, 7 ) );
9573 }
9574 }
9575
9576 /**
9577 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9578 *
9579 * @author Leo Fajardo (@leorw)
9580 * @since 2.3.2
9581 */
9582 private function maybe_sync_install_user() {
9583 if ( $this->_user->id == $this->_site->user_id ) {
9584 return;
9585 }
9586
9587 // Fetch user data and store if found.
9588 $this->sync_user_by_current_install();
9589 }
9590
9591 /**
9592 * Update install only if changed.
9593 *
9594 * @author Vova Feldman (@svovaf)
9595 * @since 1.0.9
9596 *
9597 * @param string[] string $override
9598 * @param bool $flush
9599 */
9600 function sync_install( $override = array(), $flush = false ) {
9601 $this->_logger->entrance();
9602
9603 $site = $this->send_install_update( $override, $flush, true );
9604
9605 if ( false === $site ) {
9606 // No sync required.
9607 return;
9608 }
9609
9610 if ( ! $this->is_api_result_entity( $site ) ) {
9611 // Failed to sync, don't update locally.
9612 return;
9613 }
9614
9615 $this->_site = new FS_Site( $site );
9616
9617 $this->_store_site( true );
9618 }
9619
9620 /**
9621 * Update install only if changed.
9622 *
9623 * @author Vova Feldman (@svovaf)
9624 * @since 1.0.9
9625 *
9626 * @param string[] string $override
9627 * @param bool $flush
9628 */
9629 private function sync_installs( $override = array(), $flush = false ) {
9630 $this->_logger->entrance();
9631
9632 $result = $this->send_installs_update( $override, $flush, true );
9633
9634 if ( false === $result ) {
9635 // No sync required.
9636 return;
9637 }
9638
9639 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9640 // Failed to sync, don't update locally.
9641 return;
9642 }
9643
9644 $address_to_blog_map = $this->get_address_to_blog_map();
9645
9646 foreach ( $result->installs as $install ) {
9647 $this->_site = new FS_Site( $install );
9648
9649 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9650 $blog_id = $address_to_blog_map[ $address ];
9651
9652 $this->_store_site( true, $blog_id );
9653 }
9654 }
9655
9656 /**
9657 * Track install's custom event.
9658 *
9659 * IMPORTANT:
9660 * Custom event tracking is currently only supported for specific clients.
9661 * If you are not one of them, please don't use this method. If you will,
9662 * the API will simply ignore your request based on the plugin ID.
9663 *
9664 * Need custom tracking for your plugin or theme?
9665 * If you are interested in custom event tracking please contact yo@freemius.com
9666 * for further details.
9667 *
9668 * @author Vova Feldman (@svovaf)
9669 * @since 1.2.1
9670 *
9671 * @param string $name Event name.
9672 * @param array $properties Associative key/value array with primitive values only
9673 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9674 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9675 *
9676 * @return object|false Event data or FALSE on failure.
9677 *
9678 * @throws \Freemius_InvalidArgumentException
9679 */
9680 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9681 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9682
9683 if ( ! $this->is_registered() ) {
9684 return false;
9685 }
9686
9687 $event = array( 'type' => $name );
9688
9689 if ( is_numeric( $process_at ) && $process_at > time() ) {
9690 $event['process_at'] = $process_at;
9691 }
9692
9693 if ( $once ) {
9694 $event['once'] = true;
9695 }
9696
9697 if ( ! empty( $properties ) ) {
9698 // Verify associative array values are primitive.
9699 foreach ( $properties as $k => $v ) {
9700 if ( ! is_scalar( $v ) ) {
9701 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9702 }
9703 }
9704
9705 $event['properties'] = $properties;
9706 }
9707
9708 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9709
9710 return $this->is_api_error( $result ) ?
9711 false :
9712 $result;
9713 }
9714
9715 /**
9716 * Track install's custom event only once, but it still triggers the API call.
9717 *
9718 * IMPORTANT:
9719 * Custom event tracking is currently only supported for specific clients.
9720 * If you are not one of them, please don't use this method. If you will,
9721 * the API will simply ignore your request based on the plugin ID.
9722 *
9723 * Need custom tracking for your plugin or theme?
9724 * If you are interested in custom event tracking please contact yo@freemius.com
9725 * for further details.
9726 *
9727 * @author Vova Feldman (@svovaf)
9728 * @since 1.2.1
9729 *
9730 * @param string $name Event name.
9731 * @param array $properties Associative key/value array with primitive values only
9732 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9733 *
9734 * @return object|false Event data or FALSE on failure.
9735 *
9736 * @throws \Freemius_InvalidArgumentException
9737 *
9738 * @user Freemius::track_event()
9739 */
9740 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9741 return $this->track_event( $name, $properties, $process_at, true );
9742 }
9743
9744 /**
9745 * Plugin uninstall hook.
9746 *
9747 * @author Vova Feldman (@svovaf)
9748 * @since 1.0.1
9749 *
9750 * @param bool $check_user Enforce checking if user have plugins activation privileges.
9751 */
9752 function _uninstall_plugin_event( $check_user = true ) {
9753 $this->_logger->entrance( 'slug = ' . $this->_slug );
9754
9755 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
9756 return;
9757 }
9758
9759 $params = array();
9760 $uninstall_reason = null;
9761 if ( isset( $this->_storage->uninstall_reason ) ) {
9762 $uninstall_reason = $this->_storage->uninstall_reason;
9763 $params['reason_id'] = $uninstall_reason->id;
9764 $params['reason_info'] = $uninstall_reason->info;
9765 }
9766
9767 if ( ! $this->is_registered() ) {
9768 // Send anonymous uninstall event only if user submitted a feedback.
9769 if ( isset( $uninstall_reason ) ) {
9770 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
9771 $this->opt_in( false, false, false, false, true );
9772 } else {
9773 $params['uid'] = $this->get_anonymous_id();
9774 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
9775 }
9776 }
9777 } else {
9778 $params = array_merge( $params, array(
9779 'is_active' => false,
9780 'is_uninstalled' => true,
9781 ) );
9782
9783 if ( $this->_is_network_active ) {
9784 // Send uninstall event.
9785 $this->send_installs_update( $params );
9786 } else {
9787 // Send uninstall event and handle the result.
9788 $this->sync_install( $params );
9789 }
9790 }
9791
9792 // @todo Decide if we want to delete plugin information from db.
9793 }
9794
9795 /**
9796 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
9797 *
9798 * @author Vova Feldman (@svovaf)
9799 * @since 2.2.1
9800 *
9801 * @param string $is_premium
9802 * @param string $caller
9803 *
9804 * @return void
9805 */
9806 function set_basename( $is_premium, $caller ) {
9807 $basename = plugin_basename( $caller );
9808
9809 $current_basename = $is_premium ?
9810 $this->_premium_plugin_basename :
9811 $this->_free_plugin_basename;
9812
9813 if ( $current_basename == $basename ) {
9814 // Basename value set correctly.
9815 return;
9816 }
9817
9818 if ( $is_premium ) {
9819 $this->_premium_plugin_basename = $basename;
9820 } else {
9821 $this->_free_plugin_basename = $basename;
9822 }
9823
9824 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
9825
9826 register_activation_hook(
9827 $plugin_dir . $basename,
9828 array( &$this, '_activate_plugin_event_hook' )
9829 );
9830 }
9831
9832 /**
9833 * @author Vova Feldman (@svovaf)
9834 * @since 1.1.1
9835 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
9836 *
9837 * @return string
9838 */
9839 function premium_plugin_basename() {
9840 if ( ! isset( $this->_premium_plugin_basename ) ) {
9841 $this->_premium_plugin_basename = $this->is_premium() ?
9842 // The product is premium, so use the current basename.
9843 $this->_plugin_basename :
9844 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
9845 }
9846
9847 return $this->_premium_plugin_basename;
9848 }
9849
9850 /**
9851 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
9852 *
9853 * @author Vova Feldman (@svovaf)
9854 * @since 1.0.2
9855 */
9856 public static function _uninstall_plugin_hook() {
9857 self::_load_required_static();
9858
9859 self::$_static_logger->entrance();
9860
9861 if ( ! current_user_can( 'activate_plugins' ) ) {
9862 return;
9863 }
9864
9865 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
9866
9867 self::$_static_logger->info( 'plugin = ' . $plugin_file );
9868
9869 define( 'WP_FS__UNINSTALL_MODE', true );
9870
9871 $fs = self::get_instance_by_file( $plugin_file );
9872
9873 if ( is_object( $fs ) ) {
9874 $fs->remove_sdk_reference();
9875
9876 self::require_plugin_essentials();
9877
9878 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
9879 is_plugin_active( $fs->premium_plugin_basename() )
9880 ) {
9881 // Deleting Free or Premium plugin version while the other version still installed.
9882 return;
9883 }
9884
9885 if (
9886 ! $fs->is_clone() &&
9887 /**
9888 * 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).
9889 *
9890 * @author Leo Fajardo
9891 */
9892 ( ! is_object( $fs->_site ) || $fs->is_registered() )
9893 ) {
9894 $fs->_uninstall_plugin_event();
9895 }
9896
9897 $fs->do_action( 'after_uninstall' );
9898 }
9899 }
9900
9901 #----------------------------------------------------------------------------------
9902 #region Plugin Information
9903 #----------------------------------------------------------------------------------
9904
9905 /**
9906 * Load WordPress core plugin.php essential module.
9907 *
9908 * @author Vova Feldman (@svovaf)
9909 * @since 1.1.1
9910 */
9911 private static function require_plugin_essentials() {
9912 if ( ! function_exists( 'get_plugins' ) ) {
9913 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
9914
9915 require_once ABSPATH . 'wp-admin/includes/plugin.php';
9916 }
9917 }
9918
9919 /**
9920 * Load WordPress core pluggable.php module.
9921 *
9922 * @author Vova Feldman (@svovaf)
9923 * @since 1.1.2
9924 */
9925 private static function require_pluggable_essentials() {
9926 if ( ! function_exists( 'wp_get_current_user' ) ) {
9927 require_once ABSPATH . 'wp-includes/pluggable.php';
9928 }
9929 }
9930
9931 /**
9932 * Return plugin data.
9933 *
9934 * @author Vova Feldman (@svovaf)
9935 * @since 1.0.1
9936 *
9937 * @param bool $reparse_plugin_metadata
9938 *
9939 * @return array
9940 */
9941 function get_plugin_data( $reparse_plugin_metadata = false ) {
9942 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
9943 self::require_plugin_essentials();
9944
9945 if ( $this->is_plugin() ) {
9946 /**
9947 * @author Vova Feldman (@svovaf)
9948 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
9949 *
9950 * @link https://github.com/Freemius/wordpress-sdk/issues/77
9951 */
9952 $plugin_data = get_plugin_data(
9953 $this->_plugin_main_file_path,
9954 false,
9955 false
9956 );
9957 } else {
9958 $theme_data = wp_get_theme();
9959
9960 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
9961 $parent_theme = $theme_data->parent();
9962
9963 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
9964 $theme_data = $parent_theme;
9965 }
9966 }
9967
9968 $plugin_data = array(
9969 'Name' => $theme_data->get( 'Name' ),
9970 'Version' => $theme_data->get( 'Version' ),
9971 'Author' => $theme_data->get( 'Author' ),
9972 'Description' => $theme_data->get( 'Description' ),
9973 'PluginURI' => $theme_data->get( 'ThemeURI' ),
9974 );
9975 }
9976
9977 $this->_plugin_data = $plugin_data;
9978 }
9979
9980 return $this->_plugin_data;
9981 }
9982
9983 /**
9984 * @author Vova Feldman (@svovaf)
9985 * @since 1.0.1
9986 * @since 1.2.2.5 If slug not set load slug by module ID.
9987 *
9988 * @return string Plugin slug.
9989 */
9990 function get_slug() {
9991 if ( ! isset( $this->_slug ) ) {
9992 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
9993 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
9994 }
9995
9996 return $this->_slug;
9997 }
9998
9999 /**
10000 * @author Leo Fajardo (@leorw)
10001 * @since 2.2.1
10002 *
10003 * @return string
10004 */
10005 function get_premium_slug() {
10006 return ( is_object( $this->_plugin ) && ! empty( $this->_plugin->premium_slug ) ) ?
10007 $this->_plugin->premium_slug :
10008 "{$this->_slug}-premium";
10009 }
10010
10011 /**
10012 * Retrieve the desired folder name for the product.
10013 *
10014 * @author Vova Feldman (@svovaf)
10015 * @since 1.2.1.7
10016 *
10017 * @return string Plugin slug.
10018 */
10019 function get_target_folder_name() {
10020 return $this->can_use_premium_code() ?
10021 $this->_plugin->premium_slug :
10022 $this->_slug;
10023 }
10024
10025 /**
10026 * @author Vova Feldman (@svovaf)
10027 * @since 1.0.1
10028 *
10029 * @return number Plugin ID.
10030 */
10031 function get_id() {
10032 return $this->_plugin->id;
10033 }
10034
10035 /**
10036 * @author Leo Fajardo (@leorw)
10037 * @since 2.2.4
10038 *
10039 * @return number|null Bundle ID.
10040 */
10041 function get_bundle_id() {
10042 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
10043 $this->_plugin->bundle_id :
10044 null;
10045 }
10046
10047 /**
10048 * @author Vova Feldman (@svovaf)
10049 * @since 2.3.1
10050 *
10051 * @return string|null Bundle public key.
10052 */
10053 function get_bundle_public_key() {
10054 return isset( $this->_plugin->bundle_public_key ) ?
10055 $this->_plugin->bundle_public_key :
10056 null;
10057 }
10058
10059 /**
10060 * Get whether the SDK has been initiated in the context of a Bundle.
10061 *
10062 * This will return true, if `bundle_id` is present in the SDK init parameters.
10063 *
10064 * ```php
10065 * $my_fs = fs_dynamic_init( array(
10066 * // ...
10067 * 'bundle_id' => 'XXXX', // Will return true since we have bundle id.
10068 * 'bundle_public_key' => 'pk_XXXX',
10069 * ) );
10070 * ```
10071 *
10072 * @author Swashata Ghosh (@swashata)
10073 * @since 2.5.0
10074 *
10075 * @return bool True if we are running in bundle context, false otherwise.
10076 */
10077 private function has_bundle_context() {
10078 return ! is_null( $this->get_bundle_id() );
10079 }
10080
10081 /**
10082 * @author Vova Feldman (@svovaf)
10083 * @since 1.2.1.5
10084 *
10085 * @return string Freemius SDK version
10086 */
10087 function get_sdk_version() {
10088 return $this->version;
10089 }
10090
10091 /**
10092 * @author Vova Feldman (@svovaf)
10093 * @since 1.2.1.5
10094 *
10095 * @return number Parent plugin ID (if parent exist).
10096 */
10097 function get_parent_id() {
10098 return $this->is_addon() ?
10099 $this->get_parent_instance()->get_id() :
10100 $this->_plugin->id;
10101 }
10102
10103 /**
10104 * @author Vova Feldman (@svovaf)
10105 * @since 2.3.1
10106 *
10107 * @return string
10108 */
10109 function get_usage_tracking_terms_url() {
10110 return $this->apply_filters(
10111 'usage_tracking_terms_url',
10112 "https://freemius.com/product/opt-in/{$this->_plugin->id}/{$this->_slug}/"
10113 );
10114 }
10115
10116 /**
10117 * @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.
10118 * @since 2.5.8
10119 *
10120 * @return string
10121 */
10122 function get_license_activation_terms_url() {
10123 return $this->apply_filters(
10124 'license_activation_terms_url',
10125 "https://freemius.com/product/license-activation/{$this->_plugin->id}/{$this->_slug}/"
10126 );
10127 }
10128
10129 /**
10130 * @author Vova Feldman (@svovaf)
10131 * @since 2.3.1
10132 *
10133 * @return string
10134 */
10135 function get_eula_url() {
10136 return $this->apply_filters(
10137 'eula_url',
10138 "https://freemius.com/product/{$this->_plugin->id}/{$this->_slug}/legal/eula/"
10139 );
10140 }
10141
10142 /**
10143 * @author Vova Feldman (@svovaf)
10144 * @since 1.0.1
10145 *
10146 * @return string Plugin public key.
10147 */
10148 function get_public_key() {
10149 return $this->_plugin->public_key;
10150 }
10151
10152 /**
10153 * Will be available only on sandbox mode.
10154 *
10155 * @author Vova Feldman (@svovaf)
10156 * @since 1.0.4
10157 *
10158 * @return mixed Plugin secret key.
10159 */
10160 function get_secret_key() {
10161 return $this->_plugin->secret_key;
10162 }
10163
10164 /**
10165 * @author Vova Feldman (@svovaf)
10166 * @since 1.1.1
10167 *
10168 * @return bool
10169 */
10170 function has_secret_key() {
10171 return ! empty( $this->_plugin->secret_key );
10172 }
10173
10174 /**
10175 * @author Vova Feldman (@svovaf)
10176 * @since 1.0.9
10177 *
10178 * @param string|bool $premium_suffix
10179 *
10180 * @return string
10181 */
10182 function get_plugin_name( $premium_suffix = false ) {
10183 $this->_logger->entrance();
10184
10185 /**
10186 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10187 *
10188 * @author Vova Feldman
10189 */
10190 if ( ! isset( $this->_plugin_name ) ) {
10191 // Name is not yet set.
10192 $this->set_name( $premium_suffix );
10193 } else if (
10194 ! empty( $premium_suffix ) &&
10195 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10196 ) {
10197 // Name is already set, but there's a change in the premium suffix.
10198 $this->set_name( $premium_suffix );
10199 }
10200
10201 return $this->_plugin_name;
10202 }
10203
10204 /**
10205 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10206 *
10207 * @author Vova Feldman (@svovaf)
10208 * @since 2.2.1
10209 *
10210 * @param string $premium_suffix
10211 */
10212 private function set_name( $premium_suffix = '' ) {
10213 $plugin_data = $this->get_plugin_data();
10214
10215 // Get name.
10216 $this->_plugin_name = $plugin_data['Name'];
10217
10218 if ( is_string( $premium_suffix ) ) {
10219 $premium_suffix = trim( $premium_suffix );
10220
10221 if ( ! empty( $premium_suffix ) ) {
10222 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10223 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10224 $suffix_len = strlen( $suffix );
10225
10226 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10227 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10228 ) {
10229 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10230 }
10231 }
10232 }
10233
10234 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10235 }
10236
10237 /**
10238 * @author Vova Feldman (@svovaf)
10239 * @since 1.0.0
10240 *
10241 * @param bool $reparse_plugin_metadata
10242 *
10243 * @return string
10244 */
10245 function get_plugin_version( $reparse_plugin_metadata = false ) {
10246 $this->_logger->entrance();
10247
10248 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10249
10250 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10251
10252 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10253 }
10254
10255 /**
10256 * @author Vova Feldman (@svovaf)
10257 * @since 1.2.1.7
10258 *
10259 * @return string
10260 */
10261 function get_plugin_title() {
10262 $this->_logger->entrance();
10263
10264 $title = $this->_plugin->title;
10265
10266 return $this->apply_filters( 'plugin_title', $title );
10267 }
10268
10269 /**
10270 * @author Vova Feldman (@svovaf)
10271 * @since 1.2.2.7
10272 *
10273 * @param bool $lowercase
10274 *
10275 * @return string
10276 */
10277 function get_module_label( $lowercase = false ) {
10278 $label = $this->is_addon() ?
10279 $this->get_text_inline( 'Add-On', 'addon' ) :
10280 ( $this->is_plugin() ?
10281 $this->get_text_inline( 'Plugin', 'plugin' ) :
10282 $this->get_text_inline( 'Theme', 'theme' ) );
10283
10284 if ( $lowercase ) {
10285 $label = strtolower( $label );
10286 }
10287
10288 return $label;
10289 }
10290
10291 /**
10292 * @author Vova Feldman (@svovaf)
10293 * @since 1.0.4
10294 *
10295 * @return string
10296 */
10297 function get_plugin_basename() {
10298 if ( ! isset( $this->_plugin_basename ) ) {
10299 if ( $this->is_plugin() ) {
10300 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10301 } else {
10302 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10303 }
10304 }
10305
10306 return $this->_plugin_basename;
10307 }
10308
10309 function get_plugin_folder_name() {
10310 $this->_logger->entrance();
10311
10312 $plugin_folder = $this->_plugin_basename;
10313
10314 while ( '.' !== dirname( $plugin_folder ) ) {
10315 $plugin_folder = dirname( $plugin_folder );
10316 }
10317
10318 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10319
10320 return $plugin_folder;
10321 }
10322
10323 #endregion ------------------------------------------------------------------
10324
10325 /* Account
10326 ------------------------------------------------------------------------------------------------------------------*/
10327
10328 /**
10329 * Find plugin's slug by plugin's basename.
10330 *
10331 * @author Vova Feldman (@svovaf)
10332 * @since 1.0.9
10333 *
10334 * @param string $plugin_base_name
10335 *
10336 * @return false|string
10337 */
10338 private static function find_slug_by_basename( $plugin_base_name ) {
10339 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10340
10341 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10342 return false;
10343 }
10344
10345 return $file_slug_map[ $plugin_base_name ];
10346 }
10347
10348 /**
10349 * Store the map between the plugin's basename to the slug.
10350 *
10351 * @author Vova Feldman (@svovaf)
10352 * @since 1.0.9
10353 */
10354 private function store_file_slug_map() {
10355 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10356
10357 if ( ! array( $file_slug_map ) ) {
10358 $file_slug_map = array();
10359 }
10360
10361 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10362 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10363 ) {
10364 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10365 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10366 }
10367 }
10368
10369 /**
10370 * @return array[number]FS_User
10371 */
10372 static function get_all_users() {
10373 $users = self::maybe_get_entities_account_option( 'users', array() );
10374
10375 if ( ! is_array( $users ) ) {
10376 $users = array();
10377 }
10378
10379 return $users;
10380 }
10381
10382 /**
10383 * @param string $module_type
10384 * @param null|int $blog_id Since 2.0.0
10385 *
10386 * @return array[string]FS_Site
10387 */
10388 public static function get_all_sites(
10389 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10390 $blog_id = null,
10391 $is_backup = false
10392 ) {
10393 $sites = self::get_account_option(
10394 ( $is_backup ? 'prev_' : '' ) . 'sites',
10395 $module_type,
10396 $blog_id
10397 );
10398
10399 if ( ! is_array( $sites ) ) {
10400 $sites = array();
10401 }
10402
10403 return $sites;
10404 }
10405
10406 /**
10407 * @author Leo Fajardo (@leorw)
10408 *
10409 * @since 1.2.2
10410 *
10411 * @param string $option_name
10412 * @param string $module_type
10413 * @param null|int $network_level_or_blog_id Since 2.0.0
10414 *
10415 * @return mixed
10416 */
10417 public static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10418 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10419 $option_name = $module_type . '_' . $option_name;
10420 }
10421
10422 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10423 }
10424
10425 /**
10426 * @author Leo Fajardo (@leorw)
10427 *
10428 * @since 1.2.2
10429 *
10430 * @param string $option_name
10431 * @param mixed $option_value
10432 * @param bool $store
10433 * @param null|int $network_level_or_blog_id Since 2.0.0
10434 */
10435 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10436 self::set_account_option_by_module(
10437 $this->_module_type,
10438 $option_name,
10439 $option_value,
10440 $store,
10441 $network_level_or_blog_id
10442 );
10443 }
10444
10445 /**
10446 * @author Vova Feldman (@svovaf)
10447 *
10448 * @since 1.2.2.7
10449 *
10450 * @param string $module_type
10451 * @param string $option_name
10452 * @param mixed $option_value
10453 * @param bool $store
10454 * @param null|int $network_level_or_blog_id Since 2.0.0
10455 */
10456 private static function set_account_option_by_module(
10457 $module_type,
10458 $option_name,
10459 $option_value,
10460 $store,
10461 $network_level_or_blog_id = null
10462 ) {
10463 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10464 $option_name = $module_type . '_' . $option_name;
10465 }
10466
10467 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10468 }
10469
10470 /**
10471 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10472 *
10473 * @author Leo Fajardo (@leorw)
10474 * @since 2.3.1
10475 *
10476 * @param string $option_name
10477 * @param mixed $default
10478 * @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).
10479 *
10480 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10481 */
10482 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10483 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10484
10485 $class_name = '';
10486
10487 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10488 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10489 }
10490
10491 switch ( $option_name ) {
10492 case 'plugins':
10493 case 'themes':
10494 case 'addons':
10495 $class_name = FS_Plugin::get_class_name();
10496 break;
10497 case 'users':
10498 $class_name = FS_User::get_class_name();
10499 break;
10500 case 'sites':
10501 $class_name = FS_Site::get_class_name();
10502 break;
10503 case 'licenses':
10504 case 'all_licenses':
10505 $class_name = FS_Plugin_License::get_class_name();
10506 break;
10507 case 'plans':
10508 $class_name = FS_Plugin_Plan::get_class_name();
10509 break;
10510 case 'updates':
10511 $class_name = FS_Plugin_Tag::get_class_name();
10512 break;
10513 }
10514
10515 if ( empty( $class_name ) ) {
10516 return $option;
10517 }
10518
10519 return fs_get_entities( $option, $class_name );
10520 }
10521
10522 /**
10523 * @author Vova Feldman (@svovaf)
10524 * @since 1.0.6
10525 *
10526 * @param number|null $module_id
10527 *
10528 * @return FS_Plugin_License[]
10529 */
10530 private static function get_all_licenses( $module_id = null ) {
10531 $licenses = self::get_account_option( 'all_licenses' );
10532
10533 if ( ! is_array( $licenses ) ) {
10534 $licenses = array();
10535 }
10536
10537 if ( is_null( $module_id ) ) {
10538 return $licenses;
10539 }
10540
10541 $licenses = isset( $licenses[ $module_id ] ) ?
10542 $licenses[ $module_id ] :
10543 array();
10544
10545 return $licenses;
10546 }
10547
10548 /**
10549 * @author Leo Fajardo (@leorw)
10550 * @since 2.0.0
10551 *
10552 * @param number $module_id
10553 * @param number|null $user_id
10554 *
10555 * @return array
10556 */
10557 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10558 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10559
10560 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10561 $all_modules_user_id_license_ids_map = array();
10562 }
10563
10564 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10565 $all_modules_user_id_license_ids_map[ $module_id ] :
10566 array();
10567
10568 if ( FS_User::is_valid_id( $user_id ) ) {
10569 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10570 $user_id_license_ids_map[ $user_id ] :
10571 array();
10572 }
10573
10574 return $user_id_license_ids_map;
10575 }
10576
10577 /**
10578 * @author Leo Fajardo (@leorw)
10579 * @since 2.0.0
10580 *
10581 * @param array $new_user_id_license_ids_map
10582 * @param number $module_id
10583 * @param number|null $user_id
10584 */
10585 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10586 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10587 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10588 $all_modules_user_id_license_ids_map = array();
10589 }
10590
10591 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10592 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10593 }
10594
10595 if ( FS_User::is_valid_id( $user_id ) ) {
10596 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10597 } else {
10598 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10599 }
10600
10601 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10602 }
10603
10604 /**
10605 * Get a collection of the user's linked license IDs.
10606 *
10607 * @author Vova Feldman (@svovaf)
10608 * @since 2.0.0
10609 *
10610 * @param number $user_id
10611 *
10612 * @return number[]
10613 */
10614 private function get_user_linked_license_ids( $user_id ) {
10615 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10616 }
10617
10618 /**
10619 * Override the user's linked license IDs with a new IDs collection.
10620 *
10621 * @author Vova Feldman (@svovaf)
10622 * @since 2.0.0
10623 *
10624 * @param number $user_id
10625 * @param number[] $license_ids
10626 */
10627 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10628 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10629 }
10630
10631 /**
10632 * Link a specified license ID to a given user.
10633 *
10634 * @author Vova Feldman (@svovaf)
10635 * @since 2.0.0
10636 *
10637 * @param number $license_id
10638 * @param number $user_id
10639 */
10640 private function link_license_2_user( $license_id, $user_id ) {
10641 $license_ids = $this->get_user_linked_license_ids( $user_id );
10642
10643 if ( in_array( $license_id, $license_ids ) ) {
10644 // License already linked.
10645 return;
10646 }
10647
10648 $license_ids[] = $license_id;
10649
10650 $this->set_user_linked_license_ids( $user_id, $license_ids );
10651 }
10652
10653 /**
10654 * @param string|bool $module_type
10655 *
10656 * @return FS_Plugin_Plan[]
10657 */
10658 private static function get_all_plans( $module_type = false ) {
10659 $plans = self::get_account_option( 'plans', $module_type );
10660
10661 if ( ! is_array( $plans ) ) {
10662 $plans = array();
10663 }
10664
10665 return $plans;
10666 }
10667
10668 /**
10669 * @author Vova Feldman (@svovaf)
10670 * @since 1.0.4
10671 *
10672 * @return FS_Plugin_Tag[]
10673 */
10674 private static function get_all_updates() {
10675 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10676
10677 if ( ! is_array( $updates ) ) {
10678 $updates = array();
10679 }
10680
10681 return $updates;
10682 }
10683
10684 /**
10685 * @author Vova Feldman (@svovaf)
10686 * @since 1.0.6
10687 *
10688 * @return array<number,FS_Plugin[]>|false
10689 */
10690 public static function get_all_addons() {
10691 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10692
10693 if ( ! is_array( $addons ) ) {
10694 $addons = array();
10695 }
10696
10697 return $addons;
10698 }
10699
10700 /**
10701 * @author Vova Feldman (@svovaf)
10702 * @since 1.0.6
10703 *
10704 * @return number[]|false
10705 */
10706 public static function get_all_account_addons() {
10707 $addons = self::$_accounts->get_option( 'account_addons', array() );
10708
10709 if ( ! is_array( $addons ) ) {
10710 $addons = array();
10711 }
10712
10713 return $addons;
10714 }
10715
10716 /**
10717 * Check if user has connected his account (opted-in).
10718 *
10719 * Note:
10720 * If the user opted-in and opted-out on a later stage,
10721 * this will still return true. If you want to check if the
10722 * user is currently opted-in, use:
10723 * `$fs->is_registered() && $fs->is_tracking_allowed()`
10724 *
10725 * @author Vova Feldman (@svovaf)
10726 * @since 1.0.1
10727 *
10728 * @param bool $ignore_anonymous_state Since 2.5.1
10729 *
10730 * @return bool
10731 */
10732 function is_registered( $ignore_anonymous_state = false ) {
10733 return (
10734 is_object( $this->_user ) &&
10735 (
10736 $this->is_premium() ||
10737 $ignore_anonymous_state ||
10738 ! $this->is_anonymous()
10739 )
10740 );
10741 }
10742
10743 /**
10744 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
10745 *
10746 * @author Leo Fajardo (@leorw)
10747 * @since 1.2.1.5
10748 *
10749 * @return bool
10750 */
10751 function is_tracking_allowed( $blog_id = null, $install = null ) {
10752 if ( is_null( $install ) ) {
10753 $install = is_null( $blog_id ) ?
10754 $this->_site :
10755 $this->get_install_by_blog_id( $blog_id );
10756 }
10757
10758 return (
10759 is_object( $install ) &&
10760 FS_Permission_Manager::instance( $this )->is_homepage_url_tracking_allowed( $blog_id )
10761 );
10762 }
10763
10764 /**
10765 * Returns TRUE if the user never opted-in or manually opted-out.
10766 *
10767 * @author Vova Feldman (@svovaf)
10768 * @since 1.2.1.5
10769 *
10770 * @param int|null $blog_id
10771 *
10772 * @return bool
10773 */
10774 function is_tracking_prohibited( $blog_id = null ) {
10775 return (
10776 ! $this->is_registered( true ) ||
10777 ! $this->is_tracking_allowed( $blog_id )
10778 );
10779 }
10780
10781 /**
10782 * @author Leo Fajardo (@leorw)
10783 * @since 2.4.0
10784 *
10785 * @return bool
10786 */
10787 function is_bundle_license_auto_activation_enabled() {
10788 return $this->is_addon() ?
10789 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
10790 $this->_is_bundle_license_auto_activation_enabled;
10791 }
10792
10793 /**
10794 * @author Vova Feldman (@svovaf)
10795 * @since 1.0.4
10796 *
10797 * @return FS_Plugin
10798 */
10799 function get_plugin() {
10800 return $this->_plugin;
10801 }
10802
10803 /**
10804 * @author Vova Feldman (@svovaf)
10805 * @since 1.0.3
10806 *
10807 * @return FS_User
10808 */
10809 function get_user() {
10810 return $this->_user;
10811 }
10812
10813 /**
10814 * @author Vova Feldman (@svovaf)
10815 * @since 1.0.3
10816 *
10817 * @return FS_Site
10818 */
10819 function get_site() {
10820 return $this->_site;
10821 }
10822
10823 /**
10824 * @author Daniele Alessandra (@danielealessandra)
10825 * @return FS_Storage
10826 * @since 2.6.2
10827 *
10828 */
10829 public function get_storage() {
10830 return $this->_storage;
10831 }
10832
10833 /**
10834 * @author Leo Fajardo (@leorw)
10835 * @since 2.5.0
10836 */
10837 function store_site( $site ) {
10838 $this->_site = $site;
10839 $this->_store_site( true );
10840 }
10841
10842 /**
10843 * 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).
10844 *
10845 * @author Leo Fajardo (@leorw)
10846 * @since 2.5.0
10847 */
10848 function delete_current_install( $back_up ) {
10849 // Back up and delete the unique ID.
10850 if ( $back_up ) {
10851 self::$_accounts->set_option( 'prev_unique_id', $this->get_anonymous_id() );
10852 }
10853
10854 self::$_accounts->set_option( 'unique_id', null );
10855
10856 if ( $back_up ) {
10857 // 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).
10858 $this->back_up_site();
10859 }
10860
10861 $this->_delete_site();
10862 $this->_site = null;
10863 }
10864
10865 /**
10866 * @author Leo Fajardo (@leorw)
10867 * @since 2.5.0
10868 */
10869 function restore_backup_site() {
10870 self::$_accounts->set_option(
10871 'unique_id',
10872 self::$_accounts->get_option( 'prev_unique_id' )
10873 );
10874
10875 $sites = self::get_all_sites( $this->_module_type, null, true );
10876 $this->store_site( clone $sites[ $this->_slug ] );
10877 }
10878
10879 /**
10880 * Get plugin add-ons.
10881 *
10882 * @author Vova Feldman (@svovaf)
10883 * @since 1.0.6
10884 *
10885 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
10886 *
10887 * @param bool $flush
10888 *
10889 * @return FS_Plugin[]|false
10890 */
10891 function get_addons( $flush = false ) {
10892 $this->_logger->entrance();
10893
10894 if ( ! $this->_has_addons ) {
10895 return false;
10896 }
10897
10898 $addons = $this->sync_addons( $flush );
10899
10900 return ( ! is_array( $addons ) || empty( $addons ) ) ?
10901 false :
10902 $addons;
10903 }
10904
10905 /**
10906 * @author Vova Feldman (@svovaf)
10907 * @since 1.0.6
10908 *
10909 * @return number[]|false
10910 */
10911 function get_account_addons() {
10912 $this->_logger->entrance();
10913
10914 $addons = self::get_all_account_addons();
10915
10916 if ( ! is_array( $addons ) ||
10917 ! isset( $addons[ $this->_plugin->id ] ) ||
10918 ! is_array( $addons[ $this->_plugin->id ] ) ||
10919 0 === count( $addons[ $this->_plugin->id ] )
10920 ) {
10921 return false;
10922 }
10923
10924 return $addons[ $this->_plugin->id ];
10925 }
10926
10927 /**
10928 * Check if user has any
10929 *
10930 * @author Vova Feldman (@svovaf)
10931 * @since 1.1.6
10932 *
10933 * @return bool
10934 */
10935 function has_account_addons() {
10936 $addons = $this->get_account_addons();
10937
10938 return is_array( $addons ) && ( 0 < count( $addons ) );
10939 }
10940
10941
10942 /**
10943 * Get add-on by ID (from local data).
10944 *
10945 * @author Vova Feldman (@svovaf)
10946 * @since 1.0.6
10947 *
10948 * @param number $id
10949 *
10950 * @return FS_Plugin|false
10951 */
10952 function get_addon( $id ) {
10953 $this->_logger->entrance();
10954
10955 $addons = $this->get_addons();
10956
10957 if ( is_array( $addons ) ) {
10958 foreach ( $addons as $addon ) {
10959 if ( $id == $addon->id ) {
10960 return $addon;
10961 }
10962 }
10963 }
10964
10965 return false;
10966 }
10967
10968 /**
10969 * Get add-on by slug (from local data).
10970 *
10971 * @author Vova Feldman (@svovaf)
10972 * @since 1.0.6
10973 *
10974 * @param string $slug
10975 *
10976 * @param bool $flush
10977 *
10978 * @return FS_Plugin|false
10979 */
10980 function get_addon_by_slug( $slug, $flush = false ) {
10981 $this->_logger->entrance();
10982
10983 $addons = $this->get_addons( $flush );
10984
10985 if ( is_array( $addons ) ) {
10986 foreach ( $addons as $addon ) {
10987 if ( $slug === $addon->slug ) {
10988 return $addon;
10989 }
10990 }
10991 }
10992
10993 return false;
10994 }
10995
10996 /**
10997 * @var array<number,object[]> {
10998 * @key number Add-on ID.
10999 * @val object[] The add-on's plans and prices object.
11000 * }
11001 */
11002 private $plans_and_pricing_by_addon_id;
11003
11004 /**
11005 * @author Leo Fajardo (@leorw)
11006 * @since 2.3.0
11007 *
11008 * @return array<number,object[]> {
11009 * @key number Add-on ID.
11010 * @val object[] The add-on's plans and prices object.
11011 * }
11012 */
11013 function _get_addons_plans_and_pricing_map_by_id() {
11014 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
11015 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
11016
11017 $plans_and_pricing_by_addon_id = array();
11018 if ( $this->is_api_result_object( $result, 'addons' ) ) {
11019 foreach ( $result->addons as $addon ) {
11020 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
11021 }
11022 }
11023
11024 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
11025 }
11026
11027 return $this->plans_and_pricing_by_addon_id;
11028 }
11029
11030 /**
11031 * @author Leo Fajardo (@leorw)
11032 * @since 2.3.0
11033 *
11034 * @param number $addon_id
11035 * @param bool $is_installed
11036 *
11037 * @return array
11038 */
11039 function _get_addon_info( $addon_id, $is_installed ) {
11040 $addon = $this->get_addon( $addon_id );
11041
11042 if ( ! is_object( $addon ) ) {
11043 // Unexpected call.
11044 return array();
11045 }
11046
11047 $slug = $addon->slug;
11048
11049 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
11050
11051 if ( ! fs_is_network_admin() ) {
11052 // Get blog-level activated installations.
11053 $sites = self::maybe_get_entities_account_option( 'sites', array() );
11054 } else {
11055 $sites = null;
11056
11057 if ( $this->is_addon_activated( $addon_id ) &&
11058 $this->get_addon_instance( $addon_id )->is_network_active()
11059 ) {
11060 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
11061 // Get network-level activated installations.
11062 $sites = self::maybe_get_entities_account_option(
11063 'sites',
11064 array(),
11065 $addon_storage->network_install_blog_id
11066 );
11067 }
11068 }
11069 }
11070
11071 $addon_info = array(
11072 'is_connected' => false,
11073 'slug' => $slug,
11074 'title' => $addon->title,
11075 'is_whitelabeled' => $addon_storage->is_whitelabeled
11076 );
11077
11078 if ( ! $is_installed ) {
11079 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
11080
11081 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
11082 $has_paid_plan = false;
11083 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
11084
11085 if ( is_array( $plans ) && count( $plans ) > 0 ) {
11086 foreach ( $plans as $plan ) {
11087 if ( isset( $plan->pricing ) &&
11088 is_array( $plan->pricing ) &&
11089 count( $plan->pricing ) > 0
11090 ) {
11091 $has_paid_plan = true;
11092 break;
11093 }
11094 }
11095 }
11096
11097 $addon_info['has_paid_plan'] = $has_paid_plan;
11098 }
11099 }
11100
11101 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
11102 return $addon_info;
11103 }
11104
11105 $site = $sites[ $slug ];
11106
11107 $addon_info['is_connected'] = (
11108 ( $addon->parent_plugin_id == $this->get_id() ) &&
11109 is_object( $site ) &&
11110 FS_Site::is_valid_id( $site->id ) &&
11111 FS_User::is_valid_id( $site->user_id ) &&
11112 FS_Plugin_Plan::is_valid_id( $site->plan_id )
11113 );
11114
11115 if ( $addon_info['is_connected'] && $is_installed ) {
11116 return $addon_info;
11117 }
11118
11119 $addon_info['site'] = $site;
11120
11121 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
11122 if ( isset( $plugins_data[ $slug ] ) ) {
11123 $plugin_data = $plugins_data[ $slug ];
11124
11125 $addon_info['version'] = $plugin_data->version;
11126 }
11127
11128 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
11129 if ( isset( $all_plans[ $slug ] ) ) {
11130 $plans = $all_plans[ $slug ];
11131
11132 foreach ( $plans as $plan ) {
11133 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
11134 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
11135 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
11136 break;
11137 }
11138 }
11139 }
11140
11141 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
11142 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
11143 foreach ( $licenses[ $addon_id ] as $license ) {
11144 if ( $license->id == $site->license_id ) {
11145 $addon_info['license'] = $license;
11146 break;
11147 }
11148 }
11149 }
11150
11151 if ( isset( $addon_info['license'] ) ) {
11152 if ( isset( $addon_storage->subscriptions ) &&
11153 ! empty( $addon_storage->subscriptions )
11154 ) {
11155 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11156
11157 foreach ( $addon_subscriptions as $subscription ) {
11158 if ( $subscription->license_id == $site->license_id ) {
11159 $addon_info['subscription'] = $subscription;
11160 break;
11161 }
11162 }
11163 }
11164 }
11165
11166 return $addon_info;
11167 }
11168
11169 /**
11170 * @author Vova Feldman (@svovaf)
11171 * @since 2.0.0
11172 *
11173 * @param number $user_id
11174 *
11175 * @return FS_User
11176 */
11177 static function _get_user_by_id( $user_id ) {
11178 self::$_static_logger->entrance( "user_id = {$user_id}" );
11179
11180 $users = self::get_all_users();
11181
11182 if ( is_array( $users ) ) {
11183 if ( isset( $users[ $user_id ] ) &&
11184 $users[ $user_id ] instanceof FS_User &&
11185 $user_id == $users[ $user_id ]->id
11186 ) {
11187 return $users[ $user_id ];
11188 }
11189
11190 // If user wasn't found by the key, iterate over all the users collection.
11191 foreach ( $users as $user ) {
11192 /**
11193 * @var FS_User $user
11194 */
11195 if ( $user_id == $user->id ) {
11196 return $user;
11197 }
11198 }
11199 }
11200
11201 return null;
11202 }
11203
11204 /**
11205 * Checks if a Freemius user_id is associated with a super-admin.
11206 *
11207 * @author Vova Feldman (@svovaf)
11208 * @since 2.0.0
11209 *
11210 * @param number $user_id
11211 *
11212 * @return bool
11213 */
11214 private static function is_super_admin( $user_id ) {
11215 $is_super_admin = false;
11216
11217 $user = self::_get_user_by_id( $user_id );
11218
11219 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11220 self::require_pluggable_essentials();
11221
11222 $wp_user = get_user_by( 'email', $user->email );
11223
11224 if ( $wp_user instanceof WP_User ) {
11225 $super_admins = get_super_admins();
11226 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11227 }
11228 }
11229
11230 return $is_super_admin;
11231 }
11232
11233 #----------------------------------------------------------------------------------
11234 #region Plans & Licensing
11235 #----------------------------------------------------------------------------------
11236
11237 /**
11238 * Check if running premium plugin code.
11239 *
11240 * @author Vova Feldman (@svovaf)
11241 * @since 1.0.5
11242 *
11243 * @return bool
11244 */
11245 function is_premium() {
11246 /**
11247 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11248 * `register_constructor_hooks` method.
11249 *
11250 * @author Leo Fajardo (@leorw)
11251 * @since 2.2.3
11252 */
11253 return is_object( $this->_plugin ) ?
11254 $this->_plugin->is_premium :
11255 false;
11256 }
11257
11258 /**
11259 * Get site's plan ID.
11260 *
11261 * @author Vova Feldman (@svovaf)
11262 * @since 1.0.2
11263 *
11264 * @return number
11265 */
11266 function get_plan_id() {
11267 return $this->_site->plan_id;
11268 }
11269
11270 /**
11271 * Get site's plan title.
11272 *
11273 * @author Vova Feldman (@svovaf)
11274 * @since 1.0.2
11275 *
11276 * @return string
11277 */
11278 function get_plan_title() {
11279 $plan = $this->get_plan();
11280
11281 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11282 }
11283
11284 /**
11285 * Get site's plan name.
11286 *
11287 * @author Vova Feldman (@svovaf)
11288 * @since 2.0.0
11289 *
11290 * @return string
11291 */
11292 function get_plan_name() {
11293 $plan = $this->get_plan();
11294
11295 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11296 }
11297
11298 /**
11299 * @author Vova Feldman (@svovaf)
11300 * @since 1.0.9
11301 *
11302 * @return FS_Plugin_Plan|false
11303 */
11304 function get_plan() {
11305 if ( ! is_object( $this->_site ) ) {
11306 return false;
11307 }
11308
11309 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11310 $this->_get_plan_by_id( $this->_site->plan_id ) :
11311 false;
11312 }
11313
11314 /**
11315 * @author Vova Feldman (@svovaf)
11316 * @since 1.0.3
11317 *
11318 * @return bool
11319 */
11320 function is_trial() {
11321 $this->_logger->entrance();
11322
11323 if ( ! $this->is_registered( true ) || ! is_object( $this->_site ) ) {
11324 return false;
11325 }
11326
11327 return $this->_site->is_trial();
11328 }
11329
11330 /**
11331 * Check if currently in a trial with payment method (credit card or paypal).
11332 *
11333 * @author Vova Feldman (@svovaf)
11334 * @since 1.1.7
11335 *
11336 * @return bool
11337 */
11338 function is_paid_trial() {
11339 $this->_logger->entrance();
11340
11341 if ( ! $this->is_trial() ) {
11342 return false;
11343 }
11344
11345 if ( ! $this->has_active_valid_license() ) {
11346 return false;
11347 }
11348
11349 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11350 return false;
11351 }
11352
11353 /**
11354 * @var FS_Subscription $subscription
11355 */
11356 $subscription = $this->_get_subscription( $this->_license->id );
11357
11358 return ( is_object( $subscription ) && $subscription->is_active() );
11359 }
11360
11361 /**
11362 * Check if trial already utilized.
11363 *
11364 * @since 1.0.9
11365 *
11366 * @return bool
11367 */
11368 function is_trial_utilized() {
11369 $this->_logger->entrance();
11370
11371 if ( ! $this->is_registered() ) {
11372 return false;
11373 }
11374
11375 return $this->_site->is_trial_utilized();
11376 }
11377
11378 /**
11379 * Get trial plan information (if in trial).
11380 *
11381 * @author Vova Feldman (@svovaf)
11382 * @since 1.0.9
11383 *
11384 * @return bool|FS_Plugin_Plan
11385 */
11386 function get_trial_plan() {
11387 $this->_logger->entrance();
11388
11389 if ( ! $this->is_trial() ) {
11390 return false;
11391 }
11392
11393 // Try to load plan from local cache.
11394 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11395
11396 if ( ! is_object( $trial_plan ) ) {
11397 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11398
11399 /**
11400 * If managed to fetch the plan, add it to the plans collection.
11401 */
11402 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11403 if ( ! is_array( $this->_plans ) ) {
11404 $this->_plans = array();
11405 }
11406
11407 $this->_plans[] = $trial_plan;
11408 $this->_store_plans();
11409 }
11410 }
11411
11412 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11413 return $trial_plan;
11414 }
11415
11416 /**
11417 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11418 */
11419 $trial_plan = new FS_Plugin_Plan();
11420 $trial_plan->id = $this->_site->trial_plan_id;
11421 $trial_plan->name = 'pro';
11422 $trial_plan->title = 'Pro';
11423
11424 return $trial_plan;
11425 }
11426
11427 /**
11428 * Check if the user has an activate, non-expired license on current plugin's install.
11429 *
11430 * @since 1.0.9
11431 *
11432 * @return bool
11433 */
11434 function is_paying() {
11435 $this->_logger->entrance();
11436
11437 if ( ! $this->is_registered( true ) ) {
11438 return false;
11439 }
11440
11441 if ( ! $this->has_paid_plan() ) {
11442 return false;
11443 }
11444
11445 return (
11446 ! $this->is_trial() &&
11447 'free' !== $this->get_plan_name() &&
11448 $this->has_active_valid_license()
11449 );
11450 }
11451
11452 /**
11453 * @author Vova Feldman (@svovaf)
11454 * @since 1.0.4
11455 *
11456 * @return bool
11457 */
11458 function is_free_plan() {
11459 if ( ! $this->is_registered() ) {
11460 return true;
11461 }
11462
11463 if ( ! $this->has_paid_plan() ) {
11464 return true;
11465 }
11466
11467 return (
11468 'free' === $this->get_plan_name() ||
11469 ! $this->has_features_enabled_license()
11470 );
11471 }
11472
11473 /**
11474 * @author Vova Feldman (@svovaf)
11475 * @since 1.0.5
11476 *
11477 * @return bool
11478 */
11479 function _has_premium_license() {
11480 $this->_logger->entrance();
11481
11482 $premium_license = $this->_get_available_premium_license();
11483
11484 return ( false !== $premium_license );
11485 }
11486
11487 /**
11488 * Check if user has any licenses associated with the plugin (including expired or blocking).
11489 *
11490 * @author Vova Feldman (@svovaf)
11491 * @since 1.1.7.3
11492 *
11493 * @param bool $including_foreign
11494 *
11495 * @return bool
11496 */
11497 function has_any_license( $including_foreign = true ) {
11498 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11499 return false;
11500 }
11501
11502 if ( $including_foreign ) {
11503 return true;
11504 }
11505
11506 foreach ( $this->_licenses as $license ) {
11507 if ( $this->_user->id == $license->user_id ) {
11508 return true;
11509 }
11510 }
11511
11512 return false;
11513 }
11514
11515 /**
11516 * @author Vova Feldman (@svovaf)
11517 * @since 1.0.5
11518 *
11519 * @param bool|null $is_localhost
11520 *
11521 * @return FS_Plugin_License|false
11522 */
11523 function _get_available_premium_license( $is_localhost = null ) {
11524 $this->_logger->entrance();
11525
11526 $licenses = $this->get_available_premium_licenses( $is_localhost );
11527 if ( ! empty( $licenses ) ) {
11528 return $licenses[0];
11529 }
11530
11531 return false;
11532 }
11533
11534 /**
11535 * @author Vova Feldman (@svovaf)
11536 * @since 1.0.5
11537 *
11538 * @param bool|null $is_localhost
11539 *
11540 * @return FS_Plugin_License[]
11541 */
11542 function get_available_premium_licenses( $is_localhost = null ) {
11543 $this->_logger->entrance();
11544
11545 $licenses = array();
11546 if ( ! $this->has_paid_plan() ) {
11547 return $licenses;
11548 }
11549
11550 if ( is_array( $this->_licenses ) ) {
11551 foreach ( $this->_licenses as $license ) {
11552 if ( ! $license->can_activate( $is_localhost ) ) {
11553 continue;
11554 }
11555
11556 $licenses[] = $license;
11557 }
11558 }
11559
11560 return $licenses;
11561 }
11562
11563 /**
11564 * Sync local plugin plans with remote server.
11565 *
11566 * 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).
11567 *
11568 * @author Vova Feldman (@svovaf)
11569 * @since 1.0.5
11570 *
11571 * @return FS_Plugin_Plan[]|object
11572 */
11573 function _sync_plans() {
11574 $plans = $this->_fetch_plugin_plans();
11575
11576 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11577 $plans_map = array();
11578 foreach ( $plans as $plan ) {
11579 $plans_map[ $plan->id ] = true;
11580 }
11581
11582 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11583
11584 foreach ( $plans_ids_to_keep as $plan_id ) {
11585 if ( isset( $plans_map[ $plan_id ] ) ) {
11586 continue;
11587 }
11588
11589 $missing_plan = self::_get_plan_by_id( $plan_id, false );
11590
11591 if ( is_object( $missing_plan ) ) {
11592 $plans[] = $missing_plan;
11593 }
11594 }
11595
11596 $this->_plans = $plans;
11597 $this->_store_plans();
11598 }
11599
11600 $this->do_action( 'after_plans_sync', $plans );
11601
11602 return $this->_plans;
11603 }
11604
11605 /**
11606 * Check if specified plan exists locally. If not, fetch it and store it.
11607 *
11608 * @author Vova Feldman (@svovaf)
11609 * @since 2.0.0
11610 *
11611 * @param number $plan_id
11612 *
11613 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11614 */
11615 private function sync_plan_if_not_exist( $plan_id ) {
11616 $plan = self::_get_plan_by_id( $plan_id );
11617
11618 if ( is_object( $plan ) ) {
11619 // Plan already exists.
11620 return $plan;
11621 }
11622
11623 $plan = $this->fetch_plan_by_id( $plan_id );
11624
11625 if ( $plan instanceof FS_Plugin_Plan ) {
11626 $this->_plans[] = $plan;
11627 $this->_store_plans();
11628
11629 return $plan;
11630 }
11631
11632 return $plan;
11633 }
11634
11635 /**
11636 * Check if specified license exists locally. If not, fetch it and store it.
11637 *
11638 * @author Vova Feldman (@svovaf)
11639 * @since 2.0.0
11640 *
11641 * @param number $license_id
11642 * @param string $license_key
11643 *
11644 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11645 */
11646 private function sync_license_if_not_exist( $license_id, $license_key ) {
11647 $license = $this->_get_license_by_id( $license_id );
11648
11649 if ( is_object( $license ) ) {
11650 // License already exists.
11651 return $license;
11652 }
11653
11654 $license = $this->fetch_license_by_key( $license_id, $license_key );
11655
11656 if ( $license instanceof FS_Plugin_License ) {
11657 $this->_licenses[] = $license;
11658
11659 $this->set_license( $license );
11660
11661 $this->_store_licenses();
11662
11663 return $license;
11664 }
11665
11666 return $license;
11667 }
11668
11669 /**
11670 * Get a collection of unique plan IDs that are associated with any installs in the network.
11671 *
11672 * @author Leo Fajardo (@leorw)
11673 * @since 2.0.0
11674 *
11675 * @return number[]
11676 */
11677 private function get_plans_ids_associated_with_installs() {
11678 if ( ! is_multisite() ) {
11679 if ( ! is_object( $this->_site ) ||
11680 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11681 ) {
11682 return array();
11683 }
11684
11685 return array( $this->_site->plan_id );
11686 }
11687
11688 $plan_ids = array();
11689 $sites = self::get_sites();
11690 foreach ( $sites as $site ) {
11691 $blog_id = self::get_site_blog_id( $site );
11692 $install = $this->get_install_by_blog_id( $blog_id );
11693
11694 if ( ! is_object( $install ) ||
11695 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11696 ) {
11697 continue;
11698 }
11699
11700 $plan_ids[ $install->plan_id ] = true;
11701 }
11702
11703 return array_keys( $plan_ids );
11704 }
11705
11706 /**
11707 * Get a collection of unique license IDs that are associated with any installs in the network.
11708 *
11709 * @author Leo Fajardo (@leorw)
11710 * @since 2.0.0
11711 *
11712 * @return number[]
11713 */
11714 private function get_license_ids_associated_with_installs() {
11715 if ( ! $this->_is_network_active ) {
11716 if ( ! is_object( $this->_site ) ||
11717 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11718 ) {
11719 return array();
11720 }
11721
11722 return array( $this->_site->license_id );
11723 }
11724
11725 $license_ids = array();
11726 $sites = self::get_sites();
11727 foreach ( $sites as $site ) {
11728 $blog_id = self::get_site_blog_id( $site );
11729 $install = $this->get_install_by_blog_id( $blog_id );
11730
11731 if ( ! is_object( $install ) ||
11732 ! FS_Plugin_License::is_valid_id( $install->license_id )
11733 ) {
11734 continue;
11735 }
11736
11737 $license_ids[ $install->license_id ] = true;
11738 }
11739
11740 return array_keys( $license_ids );
11741 }
11742
11743 /**
11744 * @author Vova Feldman (@svovaf)
11745 * @since 1.0.5
11746 *
11747 * @param number $id
11748 *
11749 * @return FS_Plugin_Plan|false
11750 */
11751 function _get_plan_by_id( $id, $allow_sync = true ) {
11752 $this->_logger->entrance();
11753
11754 if ( $allow_sync && ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) ) {
11755 $this->_sync_plans();
11756 }
11757
11758 foreach ( $this->_plans as $plan ) {
11759 if ( $id == $plan->id ) {
11760 return $plan;
11761 }
11762 }
11763
11764 return false;
11765 }
11766
11767 /**
11768 * @author Vova Feldman (@svovaf)
11769 * @since 1.1.8.1
11770 *
11771 * @param string $name
11772 *
11773 * @return FS_Plugin_Plan|false
11774 */
11775 private function get_plan_by_name( $name ) {
11776 $this->_logger->entrance();
11777
11778 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11779 $this->_sync_plans();
11780 }
11781
11782 foreach ( $this->_plans as $plan ) {
11783 if ( $name == $plan->name ) {
11784 return $plan;
11785 }
11786 }
11787
11788 return false;
11789 }
11790
11791 /**
11792 * Sync local licenses with remote server.
11793 *
11794 * @author Vova Feldman (@svovaf)
11795 * @since 1.0.6
11796 *
11797 * @param number|bool $site_license_id
11798 * @param number|null $blog_id
11799 *
11800 * @return FS_Plugin_License[]|object
11801 */
11802 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
11803 $this->_logger->entrance();
11804
11805 $is_network_admin = fs_is_network_admin();
11806
11807 if ( $is_network_admin && is_null( $blog_id ) ) {
11808 $all_licenses = self::get_all_licenses( $this->_module_id );
11809 } else {
11810 $all_licenses = $this->get_user_licenses( $this->_user->id );
11811 }
11812
11813 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
11814
11815 $all_licenses_map = array();
11816 foreach ( $all_licenses as $license ) {
11817 $all_licenses_map[ $license->id ] = true;
11818 }
11819
11820 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
11821
11822 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
11823 $licenses_map = array();
11824 foreach ( $licenses as $license ) {
11825 $licenses_map[ $license->id ] = true;
11826 }
11827
11828 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
11829 // foreach ( $license_ids_to_keep as $license_id ) {
11830 // if ( isset( $licenses_map[ $license_id ] ) ) {
11831 // continue;
11832 // }
11833 //
11834 // $missing_license = self::_get_license_by_id( $license_id, false );
11835 // if ( is_object( $missing_license ) ) {
11836 // $licenses[] = $missing_license;
11837 // $licenses_map[ $missing_license->id ] = true;
11838 // }
11839 // }
11840
11841 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
11842
11843 foreach ( $user_license_ids as $key => $license_id ) {
11844 if ( ! isset( $licenses_map[ $license_id ] ) ) {
11845 // Remove access to licenses that no longer exist.
11846 unset( $user_license_ids[ $key ] );
11847 }
11848 }
11849
11850 if ( ! empty( $user_license_ids ) ) {
11851 foreach ( $licenses_map as $license_id => $value ) {
11852 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
11853 // Associate new licenses with the user who triggered the license syncing.
11854 $user_license_ids[] = $license_id;
11855 }
11856 }
11857
11858 $user_license_ids = array_unique( $user_license_ids );
11859 } else {
11860 $user_license_ids = array_keys( $licenses_map );
11861 }
11862
11863 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
11864 $user_licenses = array();
11865 foreach ( $licenses as $license ) {
11866 if ( ! in_array( $license->id, $user_license_ids ) ) {
11867 continue;
11868 }
11869
11870 $user_licenses[] = $license;
11871 }
11872
11873 $this->_licenses = $user_licenses;
11874 } else {
11875 $this->_licenses = $licenses;
11876 }
11877
11878 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
11879
11880 $this->_store_licenses( true, $this->_module_id, $licenses );
11881 }
11882
11883 // Update current license.
11884 if ( is_object( $this->_license ) ) {
11885 $license = $this->_get_license_by_id( $this->_license->id );
11886
11887 if ( is_object( $license ) ) {
11888 /**
11889 * `$license` can be `false` in case a user change action has just been completed and this method
11890 * has synced the `$this->_licenses` collection for the new user. In this case, the
11891 * `$this->_licenses` collection may have only the newly activated license that is associated with
11892 * the new user. `set_license` will eventually be called in the same request by the logic that
11893 * follows outside this method which will detect that the install's license has been updated, and
11894 * then `_update_site_license` will be called which in turn will call `set_license`.
11895 *
11896 * @author Leo Fajardo (@leorw)
11897 * @since 2.3.2
11898 */
11899 $this->set_license( $license );
11900 }
11901 }
11902
11903 return $this->_licenses;
11904 }
11905
11906 /**
11907 * @author Vova Feldman (@svovaf)
11908 * @since 1.0.5
11909 *
11910 * @param number $id
11911 * @param bool $sync_licenses
11912 *
11913 * @return FS_Plugin_License|false
11914 */
11915 function _get_license_by_id( $id, $sync_licenses = true ) {
11916 $this->_logger->entrance();
11917
11918 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
11919 return false;
11920 }
11921
11922 /**
11923 * When running from the network level admin and opted-in from the network,
11924 * check if the license exists in the network user licenses collection.
11925 *
11926 * @author Vova Feldman (@svovaf)
11927 * @since 2.0.0
11928 */
11929 if ( fs_is_network_admin() &&
11930 $this->is_network_registered() &&
11931 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
11932 ) {
11933 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
11934
11935 foreach ( $licenses as $license ) {
11936 if ( $id == $license->id ) {
11937 return $license;
11938 }
11939 }
11940 }
11941
11942 if ( ! $this->has_any_license() && $sync_licenses ) {
11943 $this->_sync_licenses( $id );
11944 }
11945
11946 if ( is_array( $this->_licenses ) ) {
11947 foreach ( $this->_licenses as $license ) {
11948 if ( $id == $license->id ) {
11949 return $license;
11950 }
11951 }
11952 }
11953
11954 return false;
11955 }
11956
11957 /**
11958 * 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.
11959 *
11960 * @author Vova Feldman (@svovaf)
11961 * @since 2.0.0
11962 *
11963 * @param number $id
11964 *
11965 * @return FS_Plugin_License
11966 */
11967 private function get_license_by_id( $id ) {
11968 $licenses = self::get_all_licenses( $this->_module_id );
11969
11970 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
11971 foreach ( $licenses as $license ) {
11972 if ( $id == $license->id ) {
11973 return $license;
11974 }
11975 }
11976 }
11977
11978 return null;
11979 }
11980
11981 /**
11982 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
11983 *
11984 * @author Vova Feldman (@svovaf)
11985 * @since 2.0.0
11986 *
11987 * @return \FS_Plugin_License|mixed
11988 */
11989 private function sync_site_license() {
11990 $api = $this->get_api_user_scope();
11991
11992 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
11993
11994 if ( ! $this->is_api_result_entity( $result ) ) {
11995 return $result;
11996 }
11997
11998 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
11999 $this->_store_licenses();
12000
12001 return $license;
12002 }
12003
12004 /**
12005 * Get all user's available licenses for the current module.
12006 *
12007 * @author Vova Feldman (@svovaf)
12008 * @since 2.0.0
12009 *
12010 * @param number $user_id
12011 *
12012 * @return FS_Plugin_License[]
12013 */
12014 private function get_user_licenses( $user_id ) {
12015 $all_licenses = self::get_all_licenses( $this->_module_id );
12016 if ( empty( $all_licenses ) ) {
12017 return array();
12018 }
12019
12020 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
12021 if ( empty( $user_license_ids ) ) {
12022 return array();
12023 }
12024
12025 $licenses = array();
12026 foreach ( $all_licenses as $license ) {
12027 if ( in_array( $license->id, $user_license_ids ) ) {
12028 $licenses[] = $license;
12029 }
12030 }
12031
12032 return $licenses;
12033 }
12034
12035 /**
12036 * Checks if the context license is network activated except on the given blog ID.
12037 *
12038 * @author Vova Feldman (@svovaf)
12039 * @since 2.0.0
12040 *
12041 * @param int $except_blog_id
12042 *
12043 * @return bool
12044 */
12045 private function is_license_network_active( $except_blog_id = 0 ) {
12046 $this->_logger->entrance();
12047
12048 if ( ! is_object( $this->_license ) ) {
12049 return false;
12050 }
12051
12052 $sites = self::get_sites();
12053
12054 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
12055 // There are more sites than the number of activations, so license cannot be network activated.
12056 return false;
12057 }
12058
12059 foreach ( $sites as $site ) {
12060 $blog_id = self::get_site_blog_id( $site );
12061
12062 if ( $except_blog_id == $blog_id ) {
12063 // Skip excluded blog.
12064 continue;
12065 }
12066
12067 $install = $this->get_install_by_blog_id( $blog_id );
12068
12069 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
12070 return false;
12071 }
12072 }
12073
12074 return true;
12075 }
12076
12077 /**
12078 * 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.
12079 *
12080 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
12081 *
12082 * @author Vova Feldman (@svovaf)
12083 * @since 2.0.0
12084 *
12085 * @param \FS_User $user
12086 * @param \FS_Plugin_License $license
12087 *
12088 * @return bool
12089 */
12090 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
12091 $this->_logger->entrance();
12092
12093 $result = $this->can_activate_license_on_network( $license );
12094
12095 if ( false === $result ) {
12096 return false;
12097 }
12098
12099 $installs_without_license = $result['installs'];
12100 if ( ! empty( $installs_without_license ) ) {
12101 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
12102 }
12103
12104 $disconnected_site_ids = $result['sites'];
12105 if ( ! empty( $disconnected_site_ids ) ) {
12106 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
12107 }
12108
12109 $this->link_license_2_user( $license->id, $user->id );
12110
12111 // Sync license after activations.
12112 $license->activated += $result['production_count'];
12113 $license->activated_local += $result['localhost_count'];
12114
12115 // $this->_store_licenses()
12116
12117 return true;
12118 }
12119
12120 /**
12121 * Checks if the given license can be activated on the whole network.
12122 *
12123 * @author Vova Feldman (@svovaf)
12124 * @since 2.0.0
12125 *
12126 * @param \FS_Plugin_License $license
12127 *
12128 * @return false|array {
12129 * @type array[int]FS_Site $installs Blog ID to install map.
12130 * @type int[] $sites Non-connected blog IDs.
12131 * @type int $production_count Production sites count.
12132 * @type int $localhost_count Production sites count.
12133 * }
12134 */
12135 private function can_activate_license_on_network( FS_Plugin_License $license ) {
12136 $sites = self::get_sites();
12137
12138 $production_count = 0;
12139 $localhost_count = 0;
12140
12141 $installs_without_license = array();
12142 $disconnected_site_ids = array();
12143
12144 foreach ( $sites as $site ) {
12145 $blog_id = self::get_site_blog_id( $site );
12146 $install = $this->get_install_by_blog_id( $blog_id );
12147
12148 if ( is_object( $install ) ) {
12149 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
12150 // License already activated on the install.
12151 continue;
12152 }
12153
12154 $url = $install->url;
12155
12156 $installs_without_license[ $blog_id ] = $install;
12157 } else {
12158 $url = is_object( $site ) ?
12159 $site->siteurl :
12160 self::get_unfiltered_site_url( $blog_id );
12161
12162 $disconnected_site_ids[] = $blog_id;
12163 }
12164
12165 if ( FS_Site::is_localhost_by_address( $url ) ) {
12166 $localhost_count ++;
12167 } else {
12168 $production_count ++;
12169 }
12170 }
12171
12172 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12173 return false;
12174 }
12175
12176 return array(
12177 'installs' => $installs_without_license,
12178 'sites' => $disconnected_site_ids,
12179 'production_count' => $production_count,
12180 'localhost_count' => $localhost_count,
12181 );
12182 }
12183
12184 /**
12185 * Activate a given license on a collection of installs.
12186 *
12187 * @author Vova Feldman (@svovaf)
12188 * @since 2.0.0
12189 *
12190 * @param \FS_User $user
12191 * @param string $license_key
12192 * @param array $blog_2_install_map {
12193 * @key int Blog ID.
12194 * @value FS_Site Blog's associated install.
12195 * }
12196 *
12197 * @return mixed|true
12198 */
12199 private function activate_license_on_many_installs(
12200 FS_User $user,
12201 $license_key,
12202 array $blog_2_install_map
12203 ) {
12204 $params = array(
12205 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12206 );
12207
12208 $install_2_blog_map = array();
12209 foreach ( $blog_2_install_map as $blog_id => $install ) {
12210 $params[] = array( 'id' => $install->id, 'url' => $install->url );
12211
12212 $install_2_blog_map[ $install->id ] = $blog_id;
12213 }
12214
12215 $result = $this->get_api_user_scope_by_user( $user )->call(
12216 "plugins/{$this->_plugin->id}/installs.json",
12217 'PUT',
12218 $params
12219 );
12220
12221 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12222 return $result;
12223 }
12224
12225 foreach ( $result->installs as $r_install ) {
12226 $install = new FS_Site( $r_install );
12227 $install->is_disconnected = false;
12228
12229 // Update install.
12230 $this->_store_site(
12231 true,
12232 $install_2_blog_map[ $r_install->id ],
12233 $install
12234 );
12235 }
12236
12237 return true;
12238 }
12239
12240 /**
12241 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12242 *
12243 * @author Vova Feldman (@svovaf)
12244 * @since 2.3.1
12245 *
12246 * @param \FS_User $user
12247 * @param string $license_key
12248 *
12249 * @return true|mixed True if successful, otherwise, the API result.
12250 */
12251 private function activate_license_on_site( FS_User $user, $license_key ) {
12252 return $this->activate_license_on_many_sites( $user, $license_key );
12253 }
12254
12255 /**
12256 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12257 *
12258 * @author Vova Feldman (@svovaf)
12259 * @since 2.0.0
12260 *
12261 * @param \FS_User $user
12262 * @param string $license_key
12263 * @param int[] $site_ids
12264 *
12265 * @return true|mixed True if successful, otherwise, the API result.
12266 */
12267 private function activate_license_on_many_sites(
12268 FS_User $user,
12269 $license_key,
12270 array $site_ids = array()
12271 ) {
12272 $sites = array();
12273 foreach ( $site_ids as $site_id ) {
12274 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12275 }
12276
12277 // Install the plugin.
12278 $result = $this->create_installs_with_user(
12279 $user,
12280 $license_key,
12281 false,
12282 $sites,
12283 false,
12284 true
12285 );
12286
12287 if ( ! $this->is_api_result_entity( $result ) &&
12288 ! $this->is_api_result_object( $result, 'installs' )
12289 ) {
12290 return $result;
12291 }
12292
12293 $installs = array();
12294
12295 if ( $this->is_api_result_entity( $result ) ) {
12296 $install = new FS_Site( $result );
12297
12298 $this->_user = $user;
12299
12300 $this->_store_site( true, null, $install );
12301
12302 $this->_site = $install;
12303
12304 $this->reset_anonymous_mode();
12305 } else {
12306 foreach ( $result->installs as $install ) {
12307 $installs[] = new FS_Site( $install );
12308 }
12309
12310 // Map site addresses to their blog IDs.
12311 $address_to_blog_map = $this->get_address_to_blog_map();
12312
12313 $first_blog_id = null;
12314
12315 foreach ( $installs as $install ) {
12316 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12317 $blog_id = $address_to_blog_map[ $address ];
12318
12319 $this->_store_site( true, $blog_id, $install );
12320
12321 $this->reset_anonymous_mode( $blog_id );
12322
12323 if ( is_null( $first_blog_id ) ) {
12324 $first_blog_id = $blog_id;
12325 }
12326 }
12327
12328 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12329 $this->_storage->network_install_blog_id = $first_blog_id;
12330 }
12331 }
12332
12333 return true;
12334 }
12335
12336 /**
12337 * Sync site's license with user licenses.
12338 *
12339 * @author Vova Feldman (@svovaf)
12340 * @since 1.0.6
12341 *
12342 * @param FS_Plugin_License|null $new_license
12343 *
12344 * @return FS_Plugin_License|null
12345 */
12346 function _update_site_license( $new_license ) {
12347 $this->_logger->entrance();
12348
12349 /**
12350 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12351 * accordingly so that it will also handle the case when an ownership change is done via license
12352 * activation.
12353 *
12354 * @author Leo Fajardo (@leorw)
12355 * @since 2.3.2
12356 */
12357 $this->set_license( $new_license );
12358
12359 if ( ! is_object( $new_license ) ) {
12360 $this->_site->license_id = null;
12361 $this->_sync_site_subscription( null );
12362
12363 return $this->_license;
12364 }
12365
12366 $this->_site->license_id = $this->_license->id;
12367
12368 if ( ! is_array( $this->_licenses ) ) {
12369 $this->_licenses = array();
12370 }
12371
12372 $is_license_found = false;
12373 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12374 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12375 $this->_licenses[ $i ] = $new_license;
12376
12377 $is_license_found = true;
12378 break;
12379 }
12380 }
12381
12382 // If new license just append.
12383 if ( ! $is_license_found ) {
12384 $this->_licenses[] = $new_license;
12385 }
12386
12387 $this->_sync_site_subscription( $new_license );
12388
12389 return $this->_license;
12390 }
12391
12392 /**
12393 * @author Vova Feldman (@svovaf)
12394 * @since 2.3.1
12395 *
12396 * @param \FS_Plugin_License $license
12397 */
12398 private function set_license( $license = null ) {
12399 $this->_license = $license;
12400
12401 $this->maybe_update_whitelabel_flag( $license );
12402 }
12403
12404 /**
12405 * @author Leo Fajardo (@leorw)
12406 * @since 2.3.1
12407 *
12408 * @param FS_Plugin_License $license
12409 */
12410 private function maybe_update_whitelabel_flag( $license ) {
12411 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12412 $this->_storage->is_whitelabeled :
12413 false;
12414
12415 if ( is_object( $license ) ) {
12416 $license_user = self::_get_user_by_id( $license->user_id );
12417
12418 if ( ! is_object( $license_user ) ) {
12419 // If foreign license, do not update the `is_whitelabeled` flag.
12420 return;
12421 }
12422
12423 if ( $this->is_addon() ) {
12424 /**
12425 * Store the last license data to the parent's storage since it's needed only when showing the
12426 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12427 * iterate over the add-ons just to get the last license data.
12428 */
12429 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12430 } else {
12431 $this->store_last_activated_license_data( $license );
12432 }
12433
12434 if ( $license->is_whitelabeled ) {
12435 // Activated a developer license, data should be hidden.
12436 $is_whitelabeled = true;
12437 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12438 // The account owner activated a regular license key, no need to hide the data.
12439 $is_whitelabeled = false;
12440 }
12441 }
12442
12443 $this->_storage->is_whitelabeled = $is_whitelabeled;
12444
12445 // Reset the whitelabeled status after update.
12446 $this->is_whitelabeled = null;
12447 if ( $this->is_addon() ) {
12448 $parent_fs = $this->get_parent_instance();
12449
12450 if ( is_object( $parent_fs ) ) {
12451 $parent_fs->is_whitelabeled = null;
12452 }
12453 }
12454 }
12455
12456 /**
12457 * @author Leo Fajardo (@leorw)
12458 * @since 2.3.1
12459 *
12460 * @param FS_Plugin_License $license
12461 * @param FS_User $license_user
12462 */
12463 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12464 if ( ! is_object( $license_user ) ) {
12465 $this->_storage->last_license_key = md5( $license->secret_key );
12466 $this->_storage->last_license_user_id = null;
12467 } else {
12468 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12469 $this->_storage->last_license_user_id = $license_user->id;
12470 }
12471 }
12472
12473 /**
12474 * @author Leo Fajardo (@leorw)
12475 * @since 2.3.1
12476 *
12477 * @param bool $ignore_data_debug_mode
12478 *
12479 * @return bool
12480 */
12481 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12482 if ( true !== $this->_storage->is_whitelabeled ) {
12483 return false;
12484 } else if ( $ignore_data_debug_mode ) {
12485 return true;
12486 }
12487
12488 $fs = $this->is_addon() ?
12489 $this->get_parent_instance() :
12490 $this;
12491
12492 return ! $fs->is_data_debug_mode();
12493 }
12494
12495 /**
12496 * @author Leo Fajardo (@leorw)
12497 * @since 2.3.1
12498 *
12499 * @return number
12500 */
12501 function get_last_license_user_id() {
12502 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12503 $this->_storage->last_license_user_id :
12504 null;
12505 }
12506
12507 /**
12508 * @author Leo Fajardo (@leorw)
12509 * @since 2.3.1
12510 *
12511 * @param int $blog_id
12512 * @param bool $ignore_data_debug_mode
12513 *
12514 * @return bool
12515 */
12516 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12517 if ( ! is_null( $blog_id ) ) {
12518 $this->switch_to_blog( $blog_id );
12519 }
12520
12521 if ( ! is_null( $this->is_whitelabeled ) ) {
12522 $is_whitelabeled = $this->is_whitelabeled;
12523 } else {
12524 $is_whitelabeled = false;
12525
12526 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12527
12528 if ( ! $this->has_addons() ) {
12529 $is_whitelabeled = $is_whitelabeled_flag;
12530 } else if ( $is_whitelabeled_flag ) {
12531 $is_whitelabeled = true;
12532 } else {
12533 if ( $this->is_registered() || $this->is_premium() ) {
12534 $addon_ids = $this->get_updated_account_addons();
12535 } else {
12536 $addons = self::get_all_addons();
12537
12538 $plugin_addons = isset( $addons[ $this->_plugin->id ] ) ?
12539 $addons[ $this->_plugin->id ] :
12540 array();
12541
12542 $addon_ids = array();
12543 foreach ( $plugin_addons as $addon ) {
12544 $addon_ids[] = $addon->id;
12545 }
12546 }
12547
12548 $installed_addons = $this->get_installed_addons();
12549 foreach ( $installed_addons as $fs_addon ) {
12550 $addon_ids[] = $fs_addon->get_id();
12551 }
12552
12553 if ( ! empty( $addon_ids ) ) {
12554 $addon_ids = array_unique( $addon_ids );
12555
12556 $is_network_level = (
12557 fs_is_network_admin() &&
12558 $this->is_network_active()
12559 );
12560
12561 foreach ( $addon_ids as $addon_id ) {
12562 $addon = $this->get_addon( $addon_id );
12563
12564 if ( ! is_object( $addon ) ) {
12565 continue;
12566 }
12567
12568 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12569 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12570 self::get_addon_instance( $addon_id ) :
12571 null;
12572
12573 $was_addon_network_activated = false;
12574
12575 if ( is_object( $fs_addon ) ) {
12576 $was_addon_network_activated = $fs_addon->is_network_active();
12577 } else if ( $is_network_level ) {
12578 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12579 }
12580
12581 $network_delegated_connection = (
12582 $was_addon_network_activated &&
12583 $addon_storage->get( 'is_delegated_connection', false, true )
12584 );
12585
12586 if (
12587 $is_network_level &&
12588 ( ! $was_addon_network_activated || $network_delegated_connection )
12589 ) {
12590 $sites = self::get_sites();
12591
12592 /**
12593 * If in network admin area and the add-on was not network-activated or network-activated
12594 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12595 */
12596 foreach ( $sites as $site ) {
12597 $site_info = $this->get_site_info( $site );
12598
12599 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12600 $is_whitelabeled = true;
12601 break;
12602 }
12603 }
12604
12605 if ( $is_whitelabeled ) {
12606 break;
12607 }
12608 } else {
12609 /**
12610 * This will be executed when any of the following is met:
12611 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12612 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12613 * 3. Add-on was not network-activated and in site admin area.
12614 */
12615 if ( true === $addon_storage->is_whitelabeled ) {
12616 $is_whitelabeled = true;
12617 break;
12618 }
12619 }
12620 }
12621 }
12622 }
12623
12624 $this->is_whitelabeled = $is_whitelabeled;
12625
12626 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12627 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12628 }
12629
12630 if ( ! is_null( $blog_id ) ) {
12631 $this->restore_current_blog();
12632 }
12633 }
12634
12635 return (
12636 $is_whitelabeled &&
12637 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12638 );
12639 }
12640
12641 /**
12642 * Sync site's subscription.
12643 *
12644 * @author Vova Feldman (@svovaf)
12645 * @since 1.0.9
12646 *
12647 * @param FS_Plugin_License|null $license
12648 *
12649 * @return bool|\FS_Subscription
12650 */
12651 private function _sync_site_subscription( $license ) {
12652 if ( ! is_object( $license ) ) {
12653 $this->delete_unused_subscriptions();
12654
12655 return false;
12656 }
12657
12658 // Load subscription details if not lifetime.
12659 $subscription = $license->is_lifetime() ?
12660 false :
12661 $this->_fetch_site_license_subscription();
12662
12663 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12664 $this->store_subscription( $subscription );
12665 } else {
12666 $this->delete_unused_subscriptions();
12667 }
12668
12669 return $subscription;
12670 }
12671
12672 /**
12673 * @author Vova Feldman (@svovaf)
12674 * @since 1.0.6
12675 *
12676 * @return bool|\FS_Plugin_License
12677 */
12678 function _get_license() {
12679 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12680 return $this->_license;
12681 }
12682
12683 return $this->_get_available_premium_license();
12684 }
12685
12686 /**
12687 * @param number $license_id
12688 *
12689 * @return null|\FS_Subscription
12690 */
12691 function _get_subscription( $license_id ) {
12692 if ( ! isset( $this->_storage->subscriptions ) ||
12693 empty( $this->_storage->subscriptions )
12694 ) {
12695 return null;
12696 }
12697
12698 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12699 if ( $subscription->license_id == $license_id ) {
12700 return $subscription;
12701 }
12702 }
12703
12704 return null;
12705 }
12706
12707 /**
12708 * @author Leo Fajardo (@leorw)
12709 * @since 2.0.0
12710 *
12711 * @param FS_Subscription $subscription
12712 */
12713 function store_subscription( FS_Subscription $subscription ) {
12714 if ( ! isset( $this->_storage->subscriptions ) ) {
12715 $this->_storage->subscriptions = array();
12716 }
12717
12718 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12719 $this->_storage->subscriptions = array( $subscription );
12720
12721 return;
12722 }
12723
12724 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
12725
12726 $updated_subscription = false;
12727 foreach ( $subscriptions as $key => $existing_subscription ) {
12728 if ( $existing_subscription->id == $subscription->id ) {
12729 $subscriptions[ $key ] = $subscription;
12730 $updated_subscription = true;
12731 break;
12732 }
12733 }
12734
12735 if ( ! $updated_subscription ) {
12736 $subscriptions[] = $subscription;
12737 }
12738
12739 $this->_storage->subscriptions = $subscriptions;
12740 }
12741
12742 /**
12743 * @author Leo Fajardo (@leorw)
12744 * @since 2.0.0
12745 */
12746 function delete_unused_subscriptions() {
12747 if ( ! isset( $this->_storage->subscriptions ) ||
12748 empty( $this->_storage->subscriptions ) ||
12749 // Clean up only if there are already at least 3 subscriptions.
12750 ( count( $this->_storage->subscriptions ) < 3 )
12751 ) {
12752 return;
12753 }
12754
12755 if ( ! is_multisite() ) {
12756 // If not multisite, there should only be 1 subscription, so just clear the array.
12757 $this->_storage->subscriptions = array();
12758
12759 return;
12760 }
12761
12762 $subscriptions_to_keep_by_license_id_map = array();
12763 $sites = self::get_sites();
12764 foreach ( $sites as $site ) {
12765 $blog_id = self::get_site_blog_id( $site );
12766 $install = $this->get_install_by_blog_id( $blog_id );
12767
12768 if ( ! is_object( $install ) ||
12769 ! FS_Plugin_License::is_valid_id( $install->license_id )
12770 ) {
12771 continue;
12772 }
12773
12774 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
12775 }
12776
12777 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
12778 $this->_storage->subscriptions = array();
12779
12780 return;
12781 }
12782
12783 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
12784 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
12785 unset( $this->_storage->subscriptions[ $key ] );
12786 }
12787 }
12788 }
12789
12790 /**
12791 * @author Vova Feldman (@svovaf)
12792 * @since 1.0.2
12793 *
12794 * @param string $plan Plan name
12795 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12796 *
12797 * @return bool
12798 */
12799 function is_plan( $plan, $exact = false ) {
12800 $this->_logger->entrance();
12801
12802 if ( ! $this->is_registered() ) {
12803 return false;
12804 }
12805
12806 $plan = strtolower( $plan );
12807
12808 $current_plan_name = $this->get_plan_name();
12809
12810 if ( $current_plan_name === $plan ) {
12811 // Exact plan.
12812 return true;
12813 } else if ( $exact ) {
12814 // Required exact, but plans are different.
12815 return false;
12816 }
12817
12818 $current_plan_order = - 1;
12819 $required_plan_order = PHP_INT_MAX;
12820 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12821 if ( $plan === $this->_plans[ $i ]->name ) {
12822 $required_plan_order = $i;
12823 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
12824 $current_plan_order = $i;
12825 }
12826 }
12827
12828 return ( $current_plan_order > $required_plan_order );
12829 }
12830
12831 /**
12832 * Check if module has only one plan.
12833 *
12834 * @author Vova Feldman (@svovaf)
12835 * @since 1.2.1.7
12836 *
12837 * @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.
12838 *
12839 * @return bool
12840 */
12841 function is_single_plan( $double_check = false ) {
12842 $this->_logger->entrance();
12843
12844 if ( ! $this->is_registered() ||
12845 ! is_array( $this->_plans ) ||
12846 0 === count( $this->_plans )
12847 ) {
12848 return true;
12849 }
12850
12851 $has_free_plan = $this->has_free_plan();
12852
12853 if ( ! $has_free_plan && $double_check ) {
12854 foreach ( $this->_plans as $plan ) {
12855 if ( $plan->is_free() ) {
12856 $has_free_plan = true;
12857 break;
12858 }
12859 }
12860 }
12861
12862 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
12863 }
12864
12865 /**
12866 * Check if plan based on trial. If not in trial mode, should return false.
12867 *
12868 * @since 1.0.9
12869 *
12870 * @param string $plan Plan name
12871 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12872 *
12873 * @return bool
12874 */
12875 function is_trial_plan( $plan, $exact = false ) {
12876 $this->_logger->entrance();
12877
12878 if ( ! $this->is_registered() ) {
12879 return false;
12880 }
12881
12882 if ( ! $this->is_trial() ) {
12883 return false;
12884 }
12885
12886 $trial_plan = $this->get_trial_plan();
12887
12888 if ( $trial_plan->name === $plan ) {
12889 // Exact plan.
12890 return true;
12891 } else if ( $exact ) {
12892 // Required exact, but plans are different.
12893 return false;
12894 }
12895
12896 $current_plan_order = - 1;
12897 $required_plan_order = - 1;
12898 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12899 if ( $plan === $this->_plans[ $i ]->name ) {
12900 $required_plan_order = $i;
12901 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
12902 $current_plan_order = $i;
12903 }
12904 }
12905
12906 return ( $current_plan_order > $required_plan_order );
12907 }
12908
12909 /**
12910 * Check if plugin has any paid plans.
12911 *
12912 * @author Vova Feldman (@svovaf)
12913 * @since 1.0.7
12914 *
12915 * @return bool
12916 */
12917 function has_paid_plan() {
12918 return $this->_has_paid_plans ||
12919 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
12920 }
12921
12922 /**
12923 * Check if plugin has any plan with a trail.
12924 *
12925 * @author Vova Feldman (@svovaf)
12926 * @since 1.0.9
12927 *
12928 * @return bool
12929 */
12930 function has_trial_plan() {
12931 /**
12932 * @author Vova Feldman(@svovaf)
12933 * @since 1.2.1.5
12934 *
12935 * Allow setting a trial from the SDK without calling the API.
12936 * But, if the user did opt-in, continue using the real data from the API.
12937 */
12938 if ( $this->_trial_days >= 0 ) {
12939 return true;
12940 }
12941
12942 return $this->_storage->get( 'has_trial_plan', false );
12943 }
12944
12945 /**
12946 * Check if plugin has any free plan, or is it premium only.
12947 *
12948 * Note: If no plans configured, assume plugin is free.
12949 *
12950 * @author Vova Feldman (@svovaf)
12951 * @since 1.0.7
12952 *
12953 * @return bool
12954 */
12955 function has_free_plan() {
12956 return ! $this->is_only_premium();
12957 }
12958
12959 /**
12960 * Displays a license activation dialog box when the user clicks on the "Activate License"
12961 * or "Change License" link on the plugins
12962 * page.
12963 *
12964 * @author Leo Fajardo (@leorw)
12965 * @since 1.1.9
12966 */
12967 function _add_license_activation_dialog_box() {
12968 $vars = array(
12969 'id' => $this->_module_id,
12970 );
12971
12972 fs_require_template( 'forms/license-activation.php', $vars );
12973 fs_require_template( 'forms/resend-key.php', $vars );
12974 }
12975
12976 /**
12977 * Displays an email address update dialog box when the user clicks on the email address "Edit" button on the "Account" page.
12978 *
12979 * @author Leo Fajardo (@leorw)
12980 * @since 2.5.0
12981 */
12982 function _add_email_address_update_dialog_box() {
12983 $vars = array( 'id' => $this->_module_id );
12984
12985 fs_require_template( 'forms/email-address-update.php', $vars );
12986 }
12987
12988 /**
12989 * @author Leo Fajardo (@leorw)
12990 * @since 2.5.0
12991 */
12992 function _add_email_address_update_option() {
12993 if ( ! $this->should_handle_user_change() ) {
12994 return;
12995 }
12996
12997 // Add email address update AJAX handler.
12998 $this->add_ajax_action( 'update_email_address', array( &$this, '_email_address_update_ajax_handler' ) );
12999 }
13000
13001 /**
13002 * @author Leo Fajardo (@leorw)
13003 * @since 2.5.0
13004 */
13005 function _email_address_update_ajax_handler() {
13006 $this->check_ajax_referer( 'update_email_address' );
13007
13008 $new_email_address = fs_request_get( 'email_address' );
13009 $transfer_type = fs_request_get( 'transfer_type' );
13010
13011 $result = $this->update_email( $new_email_address );
13012
13013 if ( ! FS_Api::is_api_error( $result ) ) {
13014 self::shoot_ajax_success();
13015 }
13016
13017 $error = '';
13018
13019 if ( FS_Api::is_api_error_object( $result ) ) {
13020 switch ( $result->error->code ) {
13021 case 'user_exist':
13022 case 'account_verification_required':
13023 $error = array(
13024 'code' => 'change_ownership',
13025 'url' => $this->get_account_url( 'change_owner', array(
13026 'state' => 'init',
13027 'candidate_email' => $new_email_address,
13028 'transfer_type' => $transfer_type,
13029 ) ),
13030 );
13031
13032 break;
13033 }
13034 }
13035
13036 if ( empty( $error ) ) {
13037 $error = is_object( $result ) ?
13038 var_export( $result->error, true ) :
13039 $result;
13040 }
13041
13042 self::shoot_ajax_failure( $error );
13043 }
13044
13045 /**
13046 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
13047 *
13048 * @author Leo Fajardo (@leorw)
13049 * @since 2.3.2
13050 *
13051 * @return number[]
13052 */
13053 function get_installs_ids_with_foreign_licenses() {
13054 $installs = array();
13055
13056 if (
13057 is_object( $this->_license ) &&
13058 $this->_site->user_id != $this->_license->user_id
13059 ) {
13060 $installs[] = $this->_site->id;
13061 }
13062
13063 /**
13064 * Also try to get foreign licenses for the context product's add-ons.
13065 */
13066 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
13067
13068 foreach ( $installs_by_slug_map as $slug => $install_info ) {
13069 if ( $slug == $this->get_slug() ) {
13070 continue;
13071 }
13072
13073 $install = $install_info['install'];
13074 $license = $install_info['license'];
13075
13076 if (
13077 is_object( $license ) &&
13078 $install->user_id != $license->user_id
13079 ) {
13080 $installs[] = $install->id;
13081 }
13082 }
13083
13084 return $installs;
13085 }
13086
13087 /**
13088 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
13089 *
13090 * @author Leo Fajardo (@leorw)
13091 * @since 2.3.2
13092 *
13093 * @param number[] $install_ids
13094 */
13095 function _add_user_change_dialog_box( $install_ids ) {
13096 $vars = array(
13097 'id' => $this->_module_id,
13098 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
13099 );
13100
13101 fs_require_template( 'forms/user-change.php', $vars );
13102 }
13103
13104 /**
13105 * @author Leo Fajardo (@leorw)
13106 * @since 2.3.1
13107 */
13108 function _add_data_debug_mode_dialog_box() {
13109 $vars = array(
13110 'id' => $this->_module_id,
13111 );
13112
13113 fs_require_template( 'forms/data-debug-mode.php', $vars );
13114 }
13115
13116 /**
13117 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
13118 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
13119 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
13120 * is only activated on a single production site.
13121 *
13122 * @author Leo Fajardo (@leorw)
13123 * @since 2.2.1
13124 *
13125 * @param bool $is_license_deactivation
13126 *
13127 * @return array
13128 */
13129 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
13130 if ( fs_is_network_admin() ) {
13131 // Subscription cancellation dialog box is currently not supported for multisite networks.
13132 return array();
13133 }
13134
13135 if ( $this->is_whitelabeled() ) {
13136 return array();
13137 }
13138
13139 $license = $this->_get_license();
13140
13141 /**
13142 * 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.
13143 *
13144 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
13145 * @since 2.2.1
13146 */
13147 if ( ! is_object( $license ) ||
13148 $license->is_lifetime() ||
13149 ( ! $license->is_single_site() && $license->activated > 1 )
13150 ) {
13151 return array();
13152 }
13153
13154 /**
13155 * @var FS_Subscription $subscription
13156 */
13157 $subscription = $this->_get_subscription( $license->id );
13158 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
13159 return array();
13160 }
13161
13162 return array(
13163 'id' => $this->_module_id,
13164 'license' => $license,
13165 'has_trial' => $this->is_paid_trial(),
13166 'is_license_deactivation' => $is_license_deactivation,
13167 );
13168 }
13169
13170 /**
13171 * @author Leo Fajardo (@leorw)
13172 * @since 2.0.2
13173 */
13174 function _add_premium_version_upgrade_selection_dialog_box() {
13175 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
13176 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
13177 return;
13178 }
13179
13180 $vars = array(
13181 'id' => $this->_module_id,
13182 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
13183 $modules_update->response[ $this->_plugin_basename ]->new_version :
13184 $modules_update->response[ $this->_plugin_basename ]['new_version']
13185 );
13186
13187 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13188 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13189 }
13190
13191 /**
13192 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13193 * page.
13194 *
13195 * @author Leo Fajardo (@leorw)
13196 * @since 1.2.1.5
13197 */
13198 function _add_optout_dialog() {
13199 if ( $this->is_theme() ) {
13200 $vars = null;
13201 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13202 }
13203
13204 $vars = array( 'id' => $this->_module_id );
13205 fs_require_template( 'forms/optout.php', $vars );
13206 }
13207
13208 /**
13209 * Prepare page to include all required UI and logic for the license activation dialog.
13210 *
13211 * @author Vova Feldman (@svovaf)
13212 * @since 1.2.0
13213 */
13214 function _add_license_activation() {
13215 if ( $this->is_migration() ) {
13216 return;
13217 }
13218
13219 if ( ! $this->is_user_admin() ) {
13220 // Only admins can activate a license.
13221 return;
13222 }
13223
13224 if ( ! $this->has_paid_plan() ) {
13225 // Module doesn't have any paid plans.
13226 return;
13227 }
13228
13229 if (
13230 $this->has_premium_version() &&
13231 ! $this->is_premium() &&
13232 /**
13233 * Also handle the case when an upgrade was made using the free version.
13234 *
13235 * @author Leo Fajardo (@leorw)
13236 * @since 2.3.2
13237 */
13238 ! is_object( $this->_get_license() )
13239 ) {
13240 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13241 return;
13242 }
13243
13244 // Add license activation link and AJAX request handler.
13245 if ( self::is_plugins_page() ) {
13246 $is_network_admin = fs_is_network_admin();
13247
13248 if (
13249 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13250 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13251 ) {
13252 if (
13253 $this->is_premium() ||
13254 ( $this->has_paid_plan() && ! $this->has_premium_version() )
13255 ) {
13256 /**
13257 * @since 1.2.0 Add license action link only on plugins page.
13258 */
13259 $this->_add_license_action_link();
13260 }
13261 }
13262 }
13263
13264 // Add license activation AJAX callback.
13265 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13266
13267 // Add resend license AJAX callback.
13268 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13269 }
13270
13271 /**
13272 * Prepares page to include all required UI and logic for the "Change User" dialog.
13273 *
13274 * @author Leo Fajardo (@leorw)
13275 * @since 2.3.2
13276 */
13277 function _add_user_change_option() {
13278 if ( ! $this->should_handle_user_change() ) {
13279 return;
13280 }
13281
13282 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13283
13284 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13285 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13286 return;
13287 }
13288
13289 // Add user change AJAX handler.
13290 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13291 }
13292
13293 /**
13294 * @author Leo Fajardo (@leorw)
13295 * @since 2.3.2
13296 */
13297 function should_handle_user_change() {
13298 if ( ! $this->is_user_admin() ) {
13299 // Only admins can change user.
13300 return false;
13301 }
13302
13303 if ( $this->is_addon() ) {
13304 return false;
13305 }
13306
13307 if ( ! $this->is_registered() ) {
13308 return false;
13309 }
13310
13311 if (
13312 $this->is_network_active() &&
13313 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13314 ) {
13315 // Handle only on site-level "Account" section for now.
13316 return false;
13317 }
13318
13319 return true;
13320 }
13321
13322 /**
13323 * @author Leo Fajardo (@leorw)
13324 * @since 2.0.2
13325 */
13326 function _add_premium_version_upgrade_selection() {
13327 if ( ! $this->is_user_admin() ) {
13328 return;
13329 }
13330
13331 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13332 // This is relevant only to the free versions and premium versions without an active license.
13333 return;
13334 }
13335
13336 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13337 $this->_add_premium_version_upgrade_selection_action();
13338 }
13339 }
13340
13341 /**
13342 * @author Edgar Melkonyan
13343 * @since 2.4.1
13344 *
13345 * @throws Freemius_Exception
13346 */
13347 function _toggle_whitelabel_mode_ajax_handler() {
13348 $this->_logger->entrance();
13349
13350 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13351
13352 if ( ! $this->is_user_admin() ) {
13353 // Only for admins.
13354 self::shoot_ajax_failure();
13355 }
13356
13357 $license = $this->get_api_user_scope()->call(
13358 "/licenses/{$this->_site->license_id}.json",
13359 'put',
13360 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13361 );
13362
13363 if ( ! $this->is_api_result_entity( $license ) ) {
13364 self::shoot_ajax_failure(
13365 FS_Api::is_api_error_object( $license ) ?
13366 $license->error->message :
13367 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13368 );
13369 }
13370
13371 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13372 $this->_store_licenses();
13373
13374 $this->_sync_license();
13375
13376 if ( ! $license->is_whitelabeled ) {
13377 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13378 } else {
13379 $this->_admin_notices->add_sticky(
13380 sprintf(
13381 $this->get_text_inline(
13382 '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.',
13383 'license_whitelabeled'
13384 ),
13385 "<strong>{$this->get_plugin_title()}</strong>",
13386 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13387 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13388 ),
13389 'license_whitelabeled'
13390 );
13391 }
13392
13393 self::shoot_ajax_response( array( 'success' => true ) );
13394 }
13395
13396 /**
13397 * @author Leo Fajardo (@leorw)
13398 * @since 2.3.0
13399 */
13400 function _add_beta_mode_update_handler() {
13401 if ( ! $this->is_user_admin() ) {
13402 return;
13403 }
13404
13405 if ( ! $this->is_premium() ) {
13406 return;
13407 }
13408
13409 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13410 }
13411
13412 /**
13413 * @author Leo Fajardo (@leorw)
13414 * @since 2.3.0
13415 */
13416 function _set_beta_mode_ajax_handler() {
13417 $this->_logger->entrance();
13418
13419 $this->check_ajax_referer( 'set_beta_mode' );
13420
13421 if ( ! $this->is_user_admin() ) {
13422 // Only for admins.
13423 self::shoot_ajax_failure();
13424 }
13425
13426 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13427
13428 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13429 self::shoot_ajax_failure();
13430 }
13431
13432 $site = $this->api_site_call(
13433 '',
13434 'put',
13435 array(
13436 'is_beta' => ( 'true' == $is_beta ),
13437 'fields' => 'is_beta'
13438 )
13439 );
13440
13441 if ( ! $this->is_api_result_entity( $site ) ) {
13442 self::shoot_ajax_failure(
13443 FS_Api::is_api_error_object( $site ) ?
13444 $site->error->message :
13445 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13446 );
13447 }
13448
13449 $this->_site->is_beta = $site->is_beta;
13450 $this->_store_site();
13451
13452 self::shoot_ajax_response( array( 'success' => true ) );
13453 }
13454
13455 /**
13456 * License activation WP AJAX handler.
13457 *
13458 * @author Leo Fajardo (@leorw)
13459 * @since 1.1.9
13460 *
13461 * @uses Freemius::activate_license()
13462 */
13463 function _activate_license_ajax_action() {
13464 $this->_logger->entrance();
13465
13466 $this->check_ajax_referer( 'activate_license' );
13467
13468 $license_key = trim( fs_request_get_raw( 'license_key' ) );
13469
13470 if ( empty( $license_key ) ) {
13471 $license_id = trim( fs_request_get_raw( 'license_id' ) );
13472
13473 if ( FS_Plugin_License::is_valid_id( $license_id ) ) {
13474 $license = $this->_get_license_by_id( $license_id, false );
13475
13476 if ( is_object( $license ) ) {
13477 $license_key = $license->secret_key;
13478 }
13479 }
13480 }
13481
13482 if ( empty( $license_key ) ) {
13483 exit;
13484 }
13485
13486 $sites = fs_is_network_admin() ?
13487 fs_request_get( 'sites', array(), 'post' ) :
13488 array();
13489
13490 $result = $this->activate_license(
13491 $license_key,
13492 $sites,
13493 fs_request_get_bool( 'is_marketing_allowed', null ),
13494 fs_request_get( 'blog_id', null ),
13495 fs_request_get( 'module_id', null, 'post' ),
13496 fs_request_get( 'user_id', null ),
13497 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
13498 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
13499 fs_request_get( 'license_owner_id', null )
13500 );
13501
13502 if (
13503 $result['success'] &&
13504 $this->is_bundle_license_auto_activation_enabled()
13505 ) {
13506 $license = new FS_Plugin_License();
13507 $license->secret_key = $license_key;
13508
13509 $this->maybe_activate_bundle_license( $license, $sites );
13510 }
13511
13512 echo json_encode( $result );
13513
13514 exit;
13515 }
13516
13517 /**
13518 * User change WP AJAX handler.
13519 *
13520 * @author Leo Fajardo (@leorw)
13521 * @since 2.3.2
13522 */
13523 function _user_change_ajax_action() {
13524 $this->_logger->entrance();
13525
13526 $this->check_ajax_referer( 'change_user' );
13527
13528 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13529 $new_user_id = fs_request_get( 'user_id' );
13530
13531 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13532 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13533 }
13534
13535 $params = array();
13536
13537 if ( ! empty( $new_email_address ) ) {
13538 $params['user_email'] = $new_email_address;
13539 } else {
13540 $params['user_id'] = $new_user_id;
13541 }
13542
13543 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13544 $install_ids = array();
13545
13546 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13547 $install_ids[ $slug ] = $install_info['install']->id;
13548 }
13549
13550 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13551
13552 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13553
13554 if ( FS_Api::is_api_error( $install ) ) {
13555 $error = '';
13556
13557 if ( is_object( $install ) ) {
13558 switch ( $install->error->code ) {
13559 case 'user_exist':
13560 $error = (
13561 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13562 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
13563 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>' ) .
13564 sprintf(
13565 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13566 $this->get_account_url( 'change_owner', array(
13567 'state' => 'init',
13568 'candidate_email' => $new_email_address
13569 ) ),
13570 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
13571 )
13572 );
13573 break;
13574 }
13575 }
13576
13577 if ( empty( $error ) ) {
13578 $error = FS_Api::is_api_error_object( $install ) ?
13579 $install->error->message :
13580 var_export( $install->error, true );
13581 }
13582
13583 self::shoot_ajax_failure( $error );
13584 } else {
13585 if (
13586 // If successful ownership change.
13587 $this->get_user()->id != $install->user_id ||
13588 ! empty( $new_email_address )
13589 ) {
13590 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
13591 }
13592 }
13593
13594 self::shoot_ajax_success();
13595 }
13596
13597 /**
13598 * @author Leo Fajardo (@leorw)
13599 * @since 2.3.2.14
13600 */
13601 function starting_migration() {
13602 if ( ! empty( $this->_storage->license_migration ) ) {
13603 // Do not overwrite the data if already set.
13604 return;
13605 }
13606
13607 $this->_storage->license_migration = array(
13608 'is_migrating' => true,
13609 'start_timestamp' => time()
13610 );
13611 }
13612
13613 /**
13614 * @author Leo Fajardo (@leorw)
13615 * @since 2.3.2.14
13616 */
13617 function is_migration() {
13618 if ( $this->is_addon() ) {
13619 return $this->get_parent_instance()->is_migration();
13620 }
13621
13622 if ( empty( $this->_storage->license_migration ) ) {
13623 return false;
13624 }
13625
13626 if ( ! $this->_storage->license_migration['is_migrating'] ) {
13627 return false;
13628 }
13629
13630 return (
13631 // Return `true` if the migration is within 5 minutes from the starting time.
13632 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
13633 );
13634 }
13635
13636 /**
13637 *
13638 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
13639 *
13640 * @author Vova Feldman (@svovaf)
13641 * @since 2.3.0
13642 *
13643 * @param string $license_key
13644 * @param null|bool $is_marketing_allowed
13645 * @param null|number $plugin_id
13646 * @param array $sites
13647 * @param int $blog_id
13648 * @param null|number $license_owner_id
13649 *
13650 * @return array {
13651 * @var bool $success
13652 * @var string $error
13653 * @var string $next_page
13654 * }
13655 *
13656 * @uses Freemius::activate_license()
13657 */
13658 function activate_migrated_license(
13659 $license_key,
13660 $is_marketing_allowed = null,
13661 $plugin_id = null,
13662 $sites = array(),
13663 $blog_id = null,
13664 $license_owner_id = null
13665 ) {
13666 $this->_logger->entrance();
13667
13668 $result = $this->activate_license(
13669 $license_key,
13670 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
13671 $this->get_sites_for_network_level_optin() :
13672 $sites,
13673 $is_marketing_allowed,
13674 $blog_id,
13675 $plugin_id,
13676 null,
13677 null,
13678 null,
13679 $license_owner_id
13680 );
13681
13682 // No need to show the sticky after license activation notice after migrating a license.
13683 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13684
13685 return $result;
13686 }
13687
13688 /**
13689 * @author Leo Fajardo (@leorw)
13690 * @since 2.3.1
13691 *
13692 * @return string
13693 */
13694 function get_pricing_js_path() {
13695 if ( ! isset( $this->_pricing_js_path ) ) {
13696 $default_path = WP_FS__DIR_JS . '/pricing/freemius-pricing.js';
13697
13698 $pricing_js_path = $this->apply_filters( 'freemius_pricing_js_path', $default_path );
13699
13700 // Backward compatibility for people who placed the freemius-pricing inside `includes` directory. Let it take more preference than the default path.
13701 if ( empty( $pricing_js_path ) ) {
13702 global $fs_active_plugins;
13703
13704 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
13705 if ( $data->plugin_path == $this->get_plugin_basename() ) {
13706 $plugin_or_theme_root_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
13707
13708 $pricing_js_path = $plugin_or_theme_root_dir
13709 . '/'
13710 // The basename will be `plugins`, `themes`, or the basename of a custom plugins or themes directory.
13711 . str_replace( '../' . basename( $plugin_or_theme_root_dir ) . '/', '', $sdk_path )
13712 . '/includes/freemius-pricing/freemius-pricing.js';
13713
13714 break;
13715 }
13716 }
13717 }
13718
13719 // If it is still empty, load the default pricing JS.
13720 if ( ! file_exists( $pricing_js_path ) ) {
13721 $pricing_js_path = $default_path;
13722 }
13723
13724 $this->_pricing_js_path = $pricing_js_path;
13725 }
13726
13727 return $this->_pricing_js_path;
13728 }
13729
13730 /**
13731 * @author Leo Fajardo (@leorw)
13732 * @since 2.3.1
13733 *
13734 * @deprecated Since v2.9.0 we have removed the iFrame based pricing. This will always return `false`.
13735 *
13736 * @return bool
13737 */
13738 function should_use_external_pricing() {
13739 return false;
13740 }
13741
13742 /**
13743 * The implementation of this method was previously in `_activate_license_ajax_action()`.
13744 *
13745 * @author Vova Feldman (@svovaf)
13746 * @since 2.2.4
13747 * @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).
13748 * @param string $license_key
13749 * @param array $sites
13750 * @param null|bool $is_marketing_allowed
13751 * @param null|int $blog_id
13752 * @param null|number $plugin_id
13753 * @param null|number $user_id
13754 * @param bool|null $is_extensions_tracking_allowed
13755 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint.
13756 * @param null|number $license_owner_id
13757 *
13758 *
13759 * @return array {
13760 * @var bool $success
13761 * @var string $error
13762 * @var string $next_page
13763 * }
13764 */
13765 private function activate_license(
13766 $license_key,
13767 $sites = array(),
13768 $is_marketing_allowed = null,
13769 $blog_id = null,
13770 $plugin_id = null,
13771 $user_id = null,
13772 $is_extensions_tracking_allowed = null,
13773 $is_diagnostic_tracking_allowed = null,
13774 $license_owner_id = null
13775 ) {
13776 $this->_logger->entrance();
13777
13778 $license_key = trim( $license_key );
13779
13780 $is_network_activation_or_migration = (
13781 fs_is_network_admin() ||
13782 ( ! empty( $sites ) && $this->is_migration() )
13783 );
13784
13785 if ( ! $is_network_activation_or_migration ) {
13786 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
13787 $sites = array();
13788 }
13789
13790 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
13791 $this :
13792 $this->get_addon_instance( $plugin_id );
13793
13794 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
13795 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
13796 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
13797 ) );
13798
13799 $error = false;
13800 $next_page = false;
13801
13802 $has_valid_blog_id = is_numeric( $blog_id );
13803
13804 $user = null;
13805
13806 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
13807 /**
13808 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
13809 *
13810 * @author Vova Feldman (@svovaf)
13811 */
13812 $user = $fs->get_parent_instance()->get_current_or_network_user();
13813 } else if ( $fs->is_registered() ) {
13814 $user = $fs->get_current_or_network_user();
13815 }
13816
13817 if ( $has_valid_blog_id ) {
13818 /**
13819 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
13820 *
13821 * @author Leo Fajardo (@leorw)
13822 */
13823 $fs->switch_to_blog( $blog_id );
13824 }
13825
13826 if ( is_object( $user ) ) {
13827 $result = true;
13828
13829 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
13830 // If no specific blog ID was provided, activate the license for all sites in the network.
13831 $blog_2_install_map = array();
13832 $site_ids = array();
13833
13834 foreach ( $sites as $site ) {
13835 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
13836 continue;
13837 }
13838
13839 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
13840
13841 if ( is_object( $install ) ) {
13842 $blog_2_install_map[ $site['blog_id'] ] = $install;
13843 } else {
13844 $site_ids[] = $site['blog_id'];
13845 }
13846 }
13847
13848 if ( ! empty( $blog_2_install_map ) ) {
13849 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
13850 }
13851
13852 if ( true === $result && ! empty( $site_ids ) ) {
13853 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
13854 }
13855 } else {
13856 if ( $fs->is_registered() ) {
13857 $params = array(
13858 'license_key' => $fs->apply_filters( 'license_key', $license_key )
13859 );
13860
13861 $install_ids = array();
13862
13863 $change_owner = FS_User::is_valid_id( $user_id );
13864
13865 if ( $change_owner ) {
13866 $params['user_id'] = $user_id;
13867
13868 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
13869
13870 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13871 $install_ids[ $slug ] = $install_info['install']->id;
13872 }
13873
13874 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13875 }
13876
13877 $api = $fs->get_api_site_scope();
13878
13879 $result = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
13880
13881 if ( ! FS_Api::is_api_error( $result ) ) {
13882 $install = $result;
13883
13884 $fs->reconnect_locally( $has_valid_blog_id );
13885
13886 if (
13887 $change_owner &&
13888 // If successful ownership change.
13889 $fs->get_user()->id != $install->user_id
13890 ) {
13891 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
13892 }
13893 }
13894 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
13895 $result = $fs->activate_license_on_site( $user, $license_key );
13896 }
13897 }
13898
13899 $is_connected = null;
13900
13901 if ( true !== $result && ! FS_Api::is_api_result_entity( $result ) ) {
13902 if ( FS_Api::is_blocked( $result ) ) {
13903 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
13904
13905 $is_connected = false;
13906 }
13907
13908 $error = FS_Api::is_api_error_object( $result ) ?
13909 $result->error->message :
13910 var_export( $result, true );
13911 } else {
13912 $is_connected = true;
13913
13914 $fs->network_upgrade_mode_completed();
13915
13916 $fs->_user = $user;
13917
13918 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
13919 $fs->_site = $fs->get_network_install();
13920 }
13921
13922 $fs->_sync_license( true, $has_valid_blog_id );
13923
13924 $this->maybe_sync_install_user();
13925
13926 $next_page = $fs->is_addon() ?
13927 $fs->get_parent_instance()->get_account_url() :
13928 $fs->get_after_activation_url( 'after_connect_url' );
13929 }
13930
13931 $fs->update_connectivity_info( $is_connected );
13932 } else {
13933 $next_page = $fs->opt_in(
13934 false,
13935 false,
13936 false,
13937 $license_key,
13938 false,
13939 false,
13940 false,
13941 $is_marketing_allowed,
13942 $sites,
13943 true,
13944 $license_owner_id
13945 );
13946
13947 if ( isset( $next_page->error ) ) {
13948 $error = $next_page->error;
13949 } else {
13950 if ( $is_network_activation_or_migration ) {
13951 /**
13952 * Get the list of sites that were just opted-in (and license activated).
13953 * This is an optimization for the next part below saving some DB queries.
13954 */
13955 $connected_sites = array();
13956 foreach ( $sites as $site ) {
13957 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
13958 $connected_sites[ $site['blog_id'] ] = true;
13959 }
13960 }
13961
13962 $all_sites = self::get_sites();
13963 $pending_blog_ids = array();
13964
13965 /**
13966 * 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.
13967 *
13968 * @author Vova Feldman (@svovaf)
13969 */
13970 foreach ( $all_sites as $site ) {
13971 $blog_id = self::get_site_blog_id( $site );
13972
13973 if ( isset( $connected_sites[ $blog_id ] ) ) {
13974 // Site was just connected.
13975 continue;
13976 }
13977
13978 if ( $fs->is_installed_on_site( $blog_id ) ) {
13979 // Site was already connected before.
13980 continue;
13981 }
13982
13983 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
13984 // Site's connection was delegated.
13985 continue;
13986 }
13987
13988 if ( $fs->is_anonymous_site( $blog_id ) ) {
13989 // Site connection was already skipped.
13990 continue;
13991 }
13992
13993 $pending_blog_ids[] = $blog_id;
13994 }
13995
13996 if ( ! empty( $pending_blog_ids ) ) {
13997 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
13998 $fs->skip_connection( $pending_blog_ids );
13999 } else {
14000 $fs->delegate_connection( $pending_blog_ids );
14001 }
14002 }
14003 }
14004 }
14005 }
14006
14007 if ( false === $error && true === $fs->_storage->require_license_activation ) {
14008 $fs->_storage->require_license_activation = false;
14009 }
14010
14011 $result = array(
14012 'success' => ( false === $error )
14013 );
14014
14015 if ( false !== $error ) {
14016 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
14017 } else {
14018 if ( $fs->is_addon() || $fs->has_addons() ) {
14019 /**
14020 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
14021 * an updated valid user licenses collection will be fetched from the server which is used to also
14022 * update the account add-ons (add-ons the user has licenses for).
14023 *
14024 * @author Leo Fajardo (@leorw)
14025 * @since 2.2.4
14026 */
14027 $fs->purge_valid_user_licenses_cache();
14028 }
14029
14030 $result['next_page'] = $next_page;
14031 }
14032
14033 if ( $result['success'] ) {
14034 $this->do_action( 'after_license_activation' );
14035 }
14036
14037 return $result;
14038 }
14039
14040 /**
14041 * @author Leo Fajardo (@leorw)
14042 * @since 2.3.2
14043 *
14044 * @return array {
14045 * @key string Product slug.
14046 * @value array {
14047 * @property FS_Site $site
14048 * @property FS_Plugin_License $license
14049 * }
14050 * }
14051 */
14052 private function get_parent_and_addons_installs_info() {
14053 $fs = $this->is_addon() ?
14054 $this->get_parent_instance() :
14055 $this;
14056
14057 $installed_addons_ids = array();
14058
14059 $installed_addons_instances = $fs->get_installed_addons();
14060 foreach ( $installed_addons_instances as $instance ) {
14061 $installed_addons_ids[] = $instance->get_id();
14062 }
14063
14064 $addons_ids = array_unique( array_merge(
14065 $installed_addons_ids,
14066 $fs->get_updated_account_addons()
14067 ) );
14068
14069 // Add parent product info.
14070 $installs_info_by_slug_map = array(
14071 $fs->get_slug() => array(
14072 'install' => $fs->get_site(),
14073 'license' => $fs->_get_license()
14074 )
14075 );
14076
14077 foreach ( $addons_ids as $addon_id ) {
14078 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
14079
14080 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
14081
14082 if ( ! isset( $addon_info['is_connected'] ) || ! $addon_info['is_connected'] ) {
14083 // Add-on is not associated with an install entity.
14084 continue;
14085 }
14086
14087 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
14088 'install' => $addon_info['site'],
14089 'license' => isset( $addon_info['license'] ) ?
14090 $addon_info['license'] :
14091 null
14092 );
14093 }
14094
14095 return $installs_info_by_slug_map;
14096 }
14097
14098 /**
14099 * @author Leo Fajardo (@leorw)
14100 * @since 1.2.3.1
14101 */
14102 function _network_activate_ajax_action() {
14103 $this->_logger->entrance();
14104
14105 $this->check_ajax_referer( 'network_activate' );
14106
14107 $plugin_id = fs_request_get( 'module_id', '', 'post' );
14108 $fs = ( $plugin_id == $this->_module_id ) ?
14109 $this :
14110 $this->get_addon_instance( $plugin_id );
14111
14112 $error = false;
14113
14114 $sites = fs_request_get( 'sites', array(), 'post' );
14115 if ( is_array( $sites ) && ! empty( $sites ) ) {
14116 $sites_by_action = array(
14117 'allow' => array(),
14118 'delegate' => array(),
14119 'skip' => array()
14120 );
14121
14122 foreach ( $sites as $site ) {
14123 $sites_by_action[ $site['action'] ][] = $site;
14124 }
14125
14126 $total_sites = count( $sites );
14127 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
14128
14129 $next_page = '';
14130
14131 $has_any_install = fs_request_get_bool( 'has_any_install' );
14132
14133 if ( $total_sites === $total_sites_to_delegate &&
14134 ! $this->is_network_upgrade_mode() &&
14135 ! $has_any_install
14136 ) {
14137 $this->delegate_connection();
14138 } else {
14139 if ( ! empty( $sites_by_action['delegate'] ) ) {
14140 $this->delegate_connection( self::get_sites_blog_ids( $sites_by_action['delegate'] ) );
14141 }
14142
14143 if ( ! empty( $sites_by_action['skip'] ) ) {
14144 $this->skip_connection( self::get_sites_blog_ids( $sites_by_action['skip'] ) );
14145 }
14146
14147 if ( empty( $sites_by_action['allow'] ) ) {
14148 if ( $has_any_install ) {
14149 $first_install = $fs->find_first_install();
14150
14151 if ( ! is_null( $first_install ) ) {
14152 $fs->_site = $first_install['install'];
14153 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
14154
14155 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
14156 $fs->_storage->network_user_id = $fs->_user->id;
14157 }
14158 }
14159 } else {
14160 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
14161 $next_page = $fs->opt_in(
14162 false,
14163 false,
14164 false,
14165 false,
14166 false,
14167 false,
14168 false,
14169 fs_request_get_bool( 'is_marketing_allowed', null ),
14170 $sites_by_action['allow']
14171 );
14172 } else {
14173 $next_page = $fs->install_with_user(
14174 $this->get_network_user(),
14175 false,
14176 false,
14177 false,
14178 true,
14179 $sites_by_action['allow']
14180 );
14181 }
14182
14183 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
14184 $error = $next_page->error;
14185 }
14186 }
14187 }
14188
14189 if ( empty( $next_page ) ) {
14190 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
14191 }
14192 } else {
14193 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
14194 }
14195
14196 $result = array(
14197 'success' => ( false === $error )
14198 );
14199
14200 if ( false !== $error ) {
14201 $result['error'] = $error;
14202 } else {
14203 $result['next_page'] = $next_page;
14204 }
14205
14206 echo json_encode( $result );
14207
14208 exit;
14209 }
14210
14211 /**
14212 * Billing update AJAX callback.
14213 *
14214 * @author Vova Feldman (@svovaf)
14215 * @since 1.2.1.5
14216 */
14217 function _update_billing_ajax_action() {
14218 $this->_logger->entrance();
14219
14220 $this->check_ajax_referer( 'update_billing' );
14221
14222 if ( ! $this->is_user_admin() ) {
14223 // Only for admins.
14224 self::shoot_ajax_failure();
14225 }
14226
14227 $billing = fs_request_get( 'billing' );
14228
14229 $api = $this->get_api_user_scope();
14230 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
14231 'plugin_id' => $this->get_parent_id(),
14232 ) ) );
14233
14234 if ( ! $this->is_api_result_entity( $result ) ) {
14235 self::shoot_ajax_failure();
14236 }
14237
14238 // Purge cached billing.
14239 $this->get_api_user_scope()->purge_cache( 'billing.json' );
14240
14241 self::shoot_ajax_success();
14242 }
14243
14244 /**
14245 * Trial start for anonymous users (AJAX callback).
14246 *
14247 * @author Vova Feldman (@svovaf)
14248 * @since 1.2.1.5
14249 */
14250 function _start_trial_ajax_action() {
14251 $this->_logger->entrance();
14252
14253 $this->check_ajax_referer( 'start_trial' );
14254
14255 if ( ! $this->is_user_admin() ) {
14256 // Only for admins.
14257 self::shoot_ajax_failure();
14258 }
14259
14260 $trial_data = fs_request_get( 'trial' );
14261
14262 $next_page = $this->opt_in(
14263 false,
14264 false,
14265 false,
14266 false,
14267 false,
14268 $trial_data['plan_id']
14269 );
14270
14271 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14272 self::shoot_ajax_failure(
14273 isset( $next_page->error ) ?
14274 $next_page->error->message :
14275 var_export( $next_page, true )
14276 );
14277 }
14278
14279 $this->shoot_ajax_success( array(
14280 'next_page' => $next_page,
14281 ) );
14282 }
14283
14284 /**
14285 * @author Leo Fajardo (@leorw)
14286 * @since 1.2.0
14287 */
14288 function _resend_license_key_ajax_action() {
14289 $this->_logger->entrance();
14290
14291 $this->check_ajax_referer( 'resend_license_key' );
14292
14293 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14294
14295 if ( empty( $email_address ) ) {
14296 exit;
14297 }
14298
14299 $error = false;
14300
14301 $api = $this->get_api_plugin_scope();
14302 $result = $api->call( '/licenses/resend.json', 'post',
14303 array(
14304 'email' => $email_address,
14305 'url' => home_url(),
14306 )
14307 );
14308
14309 if ( is_object( $result ) && isset( $result->error ) ) {
14310 $error = $result->error;
14311
14312 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14313 $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' );
14314 } else if ( 'no_license' === $error->code ) {
14315 $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' );
14316 } else {
14317 $error = $error->message;
14318 }
14319 }
14320
14321 $licenses = array(
14322 'success' => ( false === $error )
14323 );
14324
14325 if ( false !== $error ) {
14326 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14327 }
14328
14329 echo json_encode( $licenses );
14330
14331 exit;
14332 }
14333
14334 /**
14335 * @author Vova Feldman (@svovaf)
14336 * @since 1.2.1.8
14337 *
14338 * @var string
14339 */
14340 private static $_pagenow;
14341
14342 /**
14343 * Get current page or the referer if executing a WP AJAX request.
14344 *
14345 * @author Vova Feldman (@svovaf)
14346 * @since 1.2.1.8
14347 *
14348 * @return string
14349 */
14350 static function get_current_page() {
14351 if ( ! isset( self::$_pagenow ) ) {
14352 global $pagenow;
14353 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14354 /**
14355 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14356 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14357 *
14358 * @author Leo Fajardo (@leorw)
14359 * @since 2.2.3
14360 */
14361 if ( is_network_admin() ) {
14362 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14363 } else if ( is_user_admin() ) {
14364 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14365 } else {
14366 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14367 }
14368
14369 $pagenow = $self_matches[1];
14370 $pagenow = trim( $pagenow, '/' );
14371 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14372 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14373 $pagenow = 'index.php';
14374 } else {
14375 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14376 $pagenow = strtolower( $self_matches[1] );
14377 if ( '.php' !== substr($pagenow, -4, 4) )
14378 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14379 }
14380 }
14381
14382 self::$_pagenow = $pagenow;
14383
14384 if ( self::is_ajax() &&
14385 'admin-ajax.php' === $pagenow
14386 ) {
14387 $referer = fs_get_raw_referer();
14388
14389 if ( is_string( $referer ) ) {
14390 $parts = explode( '?', $referer );
14391
14392 self::$_pagenow = basename( $parts[0] );
14393 }
14394 }
14395 }
14396
14397 return self::$_pagenow;
14398 }
14399
14400 /**
14401 * Helper method to check if user in the plugins page.
14402 *
14403 * @author Vova Feldman (@svovaf)
14404 * @since 1.2.1.5
14405 *
14406 * @return bool
14407 */
14408 static function is_plugins_page() {
14409 return ( 'plugins.php' === self::get_current_page() );
14410 }
14411
14412 /**
14413 * @author Leo Fajardo (@leorw)
14414 * @since 2.2.3
14415 *
14416 * @return bool
14417 */
14418 static function is_plugin_install_page() {
14419 return ( 'plugin-install.php' === self::get_current_page() );
14420 }
14421
14422 /**
14423 * @author Leo Fajardo (@leorw)
14424 * @since 2.0.2
14425 *
14426 * @return bool
14427 */
14428 static function is_updates_page() {
14429 return ( 'update-core.php' === self::get_current_page() );
14430 }
14431
14432 /**
14433 * Helper method to check if user in the themes page.
14434 *
14435 * @author Vova Feldman (@svovaf)
14436 * @since 1.2.2.6
14437 *
14438 * @return bool
14439 */
14440 static function is_themes_page() {
14441 return ( 'themes.php' === self::get_current_page() );
14442 }
14443
14444 #----------------------------------------------------------------------------------
14445 #region Affiliation
14446 #----------------------------------------------------------------------------------
14447
14448 /**
14449 * @author Leo Fajardo (@leorw)
14450 * @since 1.2.3
14451 *
14452 * @return bool
14453 */
14454 function has_affiliate_program() {
14455 if ( ! is_object( $this->_plugin ) ) {
14456 return false;
14457 }
14458
14459 return $this->_plugin->has_affiliate_program();
14460 }
14461
14462 /**
14463 * Get Plugin ID under which we will track affiliate application.
14464 *
14465 * This could either be the Bundle ID or the main plugin ID.
14466 *
14467 * @return number Bundle ID if developer has provided one, else the main plugin ID.
14468 */
14469 private function get_plugin_id_for_affiliate_terms() {
14470 return $this->has_bundle_context() ?
14471 $this->get_bundle_id() :
14472 $this->_plugin->id;
14473 }
14474
14475 /**
14476 * @author Leo Fajardo (@leorw)
14477 * @since 1.2.4
14478 */
14479 private function fetch_affiliate_terms() {
14480 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14481 /**
14482 * In case we have a bundle set in SDK configuration, we would like to use that for affiliates, not the main plugin.
14483 */
14484 $plugins_api = $this->has_bundle_context() ?
14485 $this->get_api_bundle_scope() :
14486 $this->get_api_plugin_scope();
14487
14488 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14489
14490 /**
14491 * 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.
14492 */
14493 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14494 return;
14495 }
14496
14497 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14498 }
14499 }
14500
14501 /**
14502 * @author Leo Fajardo (@leorw)
14503 * @since 1.2.4
14504 */
14505 private function fetch_affiliate_and_custom_terms() {
14506 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14507 $application_data = $this->_storage->affiliate_application_data;
14508 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14509
14510 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14511
14512 $users_api = $this->get_api_user_scope();
14513 $result = $users_api->get( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14514 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14515 if ( ! empty( $result->affiliates ) ) {
14516 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14517
14518 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14519 $application_data['status'] = $affiliate->status;
14520 $this->_storage->affiliate_application_data = $application_data;
14521 }
14522
14523 if ( $affiliate->is_using_custom_terms ) {
14524 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14525 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14526 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14527 }
14528 }
14529
14530 $this->affiliate = $affiliate;
14531 }
14532 }
14533 }
14534 }
14535
14536 /**
14537 * @author Leo Fajardo (@leorw)
14538 * @since 1.2.3
14539 */
14540 private function fetch_affiliate_and_terms() {
14541 $this->_logger->entrance();
14542
14543 $this->fetch_affiliate_terms();
14544 $this->fetch_affiliate_and_custom_terms();
14545 }
14546
14547 /**
14548 * @author Leo Fajardo (@leorw)
14549 * @since 1.2.3
14550 *
14551 * @return FS_Affiliate
14552 */
14553 function get_affiliate() {
14554 return $this->affiliate;
14555 }
14556
14557
14558 /**
14559 * @author Leo Fajardo (@leorw)
14560 * @since 1.2.3
14561 *
14562 * @return FS_AffiliateTerms
14563 */
14564 function get_affiliate_terms() {
14565 return is_object( $this->custom_affiliate_terms ) ?
14566 $this->custom_affiliate_terms :
14567 $this->plugin_affiliate_terms;
14568 }
14569
14570 /**
14571 * @author Leo Fajardo (@leorw)
14572 * @since 1.2.3
14573 */
14574 function _submit_affiliate_application() {
14575 $this->_logger->entrance();
14576
14577 $this->check_ajax_referer( 'submit_affiliate_application' );
14578
14579 if ( ! $this->is_user_admin() ) {
14580 // Only for admins.
14581 self::shoot_ajax_failure();
14582 }
14583
14584 $affiliate = fs_request_get( 'affiliate' );
14585
14586 if ( empty( $affiliate['promotion_methods'] ) ) {
14587 unset( $affiliate['promotion_methods'] );
14588 }
14589
14590 if ( ! empty( $affiliate['additional_domains'] ) ) {
14591 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
14592 }
14593
14594 if ( ! $this->is_registered() ) {
14595 $email_address = isset( $affiliate['email'] ) ? $affiliate['email'] : '';
14596
14597 if ( ! is_email( $email_address ) ) {
14598 self::shoot_ajax_failure('Invalid email address.');
14599 }
14600
14601 // Opt in but don't track usage.
14602 $next_page = $this->opt_in(
14603 $email_address,
14604 false,
14605 false,
14606 false,
14607 false,
14608 false,
14609 true
14610 );
14611
14612 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14613 self::shoot_ajax_failure(
14614 isset( $next_page->error ) ?
14615 $next_page->error->message :
14616 var_export( $next_page, true )
14617 );
14618 } else if ( $this->is_pending_activation() ) {
14619 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' ) );
14620 }
14621 }
14622
14623 $this->fetch_affiliate_terms();
14624
14625 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14626
14627 $api = $this->get_api_user_scope();
14628 $result = $api->call(
14629 ( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
14630 'post',
14631 $affiliate
14632 );
14633
14634 if ( $this->is_api_error( $result ) ) {
14635 self::shoot_ajax_failure(
14636 isset( $result->error ) ?
14637 $result->error->message :
14638 var_export( $result, true )
14639 );
14640 } else {
14641 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
14642 $this->_admin_notices->remove_sticky( 'affiliate_program' );
14643 }
14644
14645 $affiliate_application_data = array(
14646 'status' => 'pending',
14647 'stats_description' => $affiliate['stats_description'],
14648 'promotion_method_description' => $affiliate['promotion_method_description'],
14649 );
14650
14651 if ( ! empty( $affiliate['promotion_methods'] ) ) {
14652 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
14653 }
14654
14655 if ( ! empty( $affiliate['domain'] ) ) {
14656 $affiliate_application_data['domain'] = $affiliate['domain'];
14657 }
14658
14659 if ( ! empty( $affiliate['additional_domains'] ) ) {
14660 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
14661 }
14662
14663 $this->_storage->affiliate_application_data = $affiliate_application_data;
14664 }
14665
14666 // Purge cached affiliate.
14667 $api->purge_cache( 'affiliate.json' );
14668
14669 self::shoot_ajax_success( $result );
14670 }
14671
14672 /**
14673 * @author Leo Fajardo (@leorw)
14674 * @since 1.2.3
14675 *
14676 * @return array|null
14677 */
14678 function get_affiliate_application_data() {
14679 if ( empty( $this->_storage->affiliate_application_data ) ) {
14680 return null;
14681 }
14682
14683 return $this->_storage->affiliate_application_data;
14684 }
14685
14686 #endregion Affiliation ------------------------------------------------------------
14687
14688 #----------------------------------------------------------------------------------
14689 #region URL Generators
14690 #----------------------------------------------------------------------------------
14691
14692 /**
14693 * Alias to pricing_url().
14694 *
14695 * @author Vova Feldman (@svovaf)
14696 * @since 1.0.2
14697 *
14698 * @uses pricing_url()
14699 *
14700 * @param string $period Billing cycle
14701 * @param bool $is_trial
14702 *
14703 * @return string
14704 */
14705 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14706 return $this->pricing_url( $period, $is_trial );
14707 }
14708
14709 /**
14710 * @author Vova Feldman (@svovaf)
14711 * @since 1.0.9
14712 *
14713 * @uses get_upgrade_url()
14714 *
14715 * @return string
14716 */
14717 function get_trial_url() {
14718 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
14719 }
14720
14721 /**
14722 * @author Leo Fajardo (@leorw)
14723 * @since 2.1.4
14724 *
14725 * @param string $new_version
14726 *
14727 * @return string
14728 */
14729 function version_upgrade_checkout_link( $new_version ) {
14730 if ( ! is_object( $this->_license ) ) {
14731 $url = $this->pricing_url();
14732
14733 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
14734 } else {
14735 $subscription = $this->_get_subscription( $this->_license->id );
14736
14737 $url = $this->checkout_url(
14738 is_object( $subscription ) ?
14739 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
14740 WP_FS__PERIOD_LIFETIME,
14741 false,
14742 array( 'licenses' => $this->_license->quota )
14743 );
14744
14745 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
14746 }
14747
14748 return sprintf(
14749 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
14750 sprintf(
14751 '<a href="%s">%s</a>',
14752 $this->apply_filters( 'update_notice_checkout_url', $url ),
14753 $purchase_license_text
14754 ),
14755 $new_version
14756 );
14757 }
14758
14759 /**
14760 * Plugin's pricing URL.
14761 *
14762 * @author Vova Feldman (@svovaf)
14763 * @since 1.0.4
14764 *
14765 * @param string $billing_cycle Billing cycle
14766 *
14767 * @param bool $is_trial
14768 *
14769 * @return string
14770 */
14771 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14772 $this->_logger->entrance();
14773
14774 $params = array(
14775 'billing_cycle' => $billing_cycle
14776 );
14777
14778 if ( $is_trial ) {
14779 $params['trial'] = 'true';
14780 }
14781
14782 $url = $this->is_addon() ?
14783 $this->_parent->addon_url( $this->_slug ) :
14784 $this->_get_admin_page_url( 'pricing', $params );
14785
14786 return $this->get_pricing_url_with_filter( $url );
14787 }
14788
14789 /**
14790 * Retrieves the filtered pricing URL.
14791 *
14792 * @author Leo Fajardo (@leorw)
14793 * @since 2.7.4
14794 *
14795 * @param string $url
14796 *
14797 * @return string
14798 */
14799 private function get_pricing_url_with_filter( $url ) {
14800 return $this->apply_filters( 'pricing_url', $url );
14801 }
14802
14803 /**
14804 * Checkout page URL.
14805 *
14806 * @author Vova Feldman (@svovaf)
14807 * @since 1.0.6
14808 *
14809 * @param string $billing_cycle Billing cycle
14810 * @param bool $is_trial
14811 * @param array $extra (optional) Extra parameters, override other query params.
14812 * @param bool|null $network
14813 *
14814 * @return string
14815 */
14816 function checkout_url(
14817 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14818 $is_trial = false,
14819 $extra = array(),
14820 $network = null
14821 ) {
14822 $this->_logger->entrance();
14823
14824 $params = array(
14825 'checkout' => 'true',
14826 'billing_cycle' => $billing_cycle,
14827 );
14828
14829 if ( $is_trial ) {
14830 $params['trial'] = 'true';
14831 }
14832
14833 /**
14834 * Params in extra override other params.
14835 */
14836 $params = array_merge( $params, $extra );
14837
14838 return $this->apply_filters( 'checkout_url', $this->_get_admin_page_url( 'pricing', $params, $network ) );
14839 }
14840
14841 /**
14842 * Add-on checkout URL.
14843 *
14844 * @author Vova Feldman (@svovaf)
14845 * @since 1.1.7
14846 *
14847 * @param number $addon_id
14848 * @param number $pricing_id
14849 * @param string $billing_cycle
14850 * @param bool $is_trial
14851 * @param bool|null $network
14852 *
14853 * @return string
14854 */
14855 function addon_checkout_url(
14856 $addon_id,
14857 $pricing_id,
14858 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14859 $is_trial = false,
14860 $network = null
14861 ) {
14862 return $this->checkout_url( $billing_cycle, $is_trial, array(
14863 'plugin_id' => $addon_id,
14864 'pricing_id' => $pricing_id,
14865 ), $network );
14866 }
14867
14868 #endregion
14869
14870 #endregion ------------------------------------------------------------------
14871
14872 /**
14873 * Check if plugin has any add-ons.
14874 *
14875 * @author Vova Feldman (@svovaf)
14876 * @since 1.0.5
14877 *
14878 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
14879 *
14880 * @return bool
14881 */
14882 function has_addons() {
14883 $this->_logger->entrance();
14884
14885 return $this->_has_addons;
14886 }
14887
14888 /**
14889 * Check if plugin can work in anonymous mode.
14890 *
14891 * @author Vova Feldman (@svovaf)
14892 * @since 1.0.9
14893 *
14894 * @return bool
14895 *
14896 * @deprecated Please use is_enable_anonymous() instead.
14897 */
14898 function enable_anonymous() {
14899 return $this->_enable_anonymous;
14900 }
14901
14902 /**
14903 * Check if plugin can work in anonymous mode.
14904 *
14905 * @author Vova Feldman (@svovaf)
14906 * @since 1.1.9
14907 *
14908 * @return bool
14909 */
14910 function is_enable_anonymous() {
14911 return $this->_enable_anonymous;
14912 }
14913
14914 /**
14915 * Check if plugin is premium only (no free plans).
14916 *
14917 * @author Vova Feldman (@svovaf)
14918 * @since 1.1.9
14919 *
14920 * @return bool
14921 */
14922 function is_only_premium() {
14923 return $this->_is_premium_only;
14924 }
14925
14926 /**
14927 * Checks if the plugin's type is "plugin". The other type is "theme".
14928 *
14929 * @author Leo Fajardo (@leorw)
14930 * @since 1.2.2
14931 *
14932 * @return bool
14933 */
14934 function is_plugin() {
14935 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
14936 }
14937
14938 /**
14939 * @author Leo Fajardo (@leorw)
14940 * @since 1.2.2
14941 *
14942 * @return string
14943 */
14944 function get_module_type() {
14945 if ( ! isset( $this->_module_type ) ) {
14946 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
14947 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
14948 }
14949
14950 return $this->_module_type;
14951 }
14952
14953 /**
14954 * @author Leo Fajardo (@leorw)
14955 * @since 1.2.2
14956 *
14957 * @return string
14958 */
14959 function get_plugin_main_file_path() {
14960 return $this->_plugin_main_file_path;
14961 }
14962
14963 /**
14964 * Check if module has a premium code version.
14965 *
14966 * Serviceware module might be freemium without any
14967 * premium code version, where the paid features
14968 * are all part of the service.
14969 *
14970 * @author Vova Feldman (@svovaf)
14971 * @since 1.2.1.6
14972 *
14973 * @return bool
14974 */
14975 function has_premium_version() {
14976 return $this->_has_premium_version;
14977 }
14978
14979 /**
14980 * Check if feature supported with current site's plan.
14981 *
14982 * @author Vova Feldman (@svovaf)
14983 * @since 1.0.1
14984 *
14985 * @todo IMPLEMENT
14986 *
14987 * @param number $feature_id
14988 *
14989 * @throws Exception
14990 */
14991 function is_feature_supported( $feature_id ) {
14992 throw new Exception( 'not implemented' );
14993 }
14994
14995 /**
14996 * @author Vova Feldman (@svovaf)
14997 * @since 1.0.1
14998 *
14999 * @return bool Is running in SSL/HTTPS
15000 */
15001 function is_ssl() {
15002 return WP_FS__IS_HTTPS;
15003 }
15004
15005 /**
15006 * @author Vova Feldman (@svovaf)
15007 * @since 1.0.9
15008 *
15009 * @return bool Is running in AJAX call.
15010 *
15011 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
15012 */
15013 static function is_ajax() {
15014 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
15015 }
15016
15017 /**
15018 * Check if it's an AJAX call targeted for the current module.
15019 *
15020 * @author Vova Feldman (@svovaf)
15021 * @since 1.2.0
15022 *
15023 * @param array|string $actions Collection of AJAX actions.
15024 *
15025 * @return bool
15026 */
15027 function is_ajax_action( $actions ) {
15028 // Verify it's an ajax call.
15029 if ( ! self::is_ajax() ) {
15030 return false;
15031 }
15032
15033 // Verify the call is relevant for the plugin.
15034 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
15035 return false;
15036 }
15037
15038 // Verify it's one of the specified actions.
15039 if ( is_string( $actions ) ) {
15040 $actions = explode( ',', $actions );
15041 }
15042
15043 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15044 $ajax_action = fs_request_get( 'action' );
15045
15046 foreach ( $actions as $action ) {
15047 if ( $ajax_action === $this->get_action_tag( $action ) ) {
15048 return true;
15049 }
15050 }
15051 }
15052
15053 return false;
15054 }
15055
15056 /**
15057 * Check if it's an AJAX call targeted for current request.
15058 *
15059 * @author Vova Feldman (@svovaf)
15060 * @since 1.2.0
15061 *
15062 * @param array|string $actions Collection of AJAX actions.
15063 * @param number|null $module_id
15064 *
15065 * @return bool
15066 */
15067 static function is_ajax_action_static( $actions, $module_id = null ) {
15068 // Verify it's an ajax call.
15069 if ( ! self::is_ajax() ) {
15070 return false;
15071 }
15072
15073
15074 if ( ! empty( $module_id ) ) {
15075 // Verify the call is relevant for the plugin.
15076 if ( $module_id != fs_request_get( 'module_id' ) ) {
15077 return false;
15078 }
15079 }
15080
15081 // Verify it's one of the specified actions.
15082 if ( is_string( $actions ) ) {
15083 $actions = explode( ',', $actions );
15084 }
15085
15086 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15087 $ajax_action = fs_request_get( 'action' );
15088
15089 foreach ( $actions as $action ) {
15090 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
15091 return true;
15092 }
15093 }
15094 }
15095
15096 return false;
15097 }
15098
15099 /**
15100 * @author Vova Feldman (@svovaf)
15101 * @since 1.1.7
15102 *
15103 * @return bool
15104 */
15105 static function is_cron() {
15106 return ( defined( 'DOING_CRON' ) && DOING_CRON );
15107 }
15108
15109 /**
15110 * @author Leo Fajardo (@leorw)
15111 * @since 2.5.0
15112 *
15113 * @return bool
15114 */
15115 static function is_admin_post() {
15116 return ( 'admin-post.php' === self::get_current_page() );
15117 }
15118
15119 /**
15120 * Check if a real user is visiting the admin dashboard.
15121 *
15122 * @author Vova Feldman (@svovaf)
15123 * @since 1.1.7
15124 *
15125 * @return bool
15126 */
15127 function is_user_in_admin() {
15128 return (
15129 is_admin() &&
15130 ! self::is_ajax() &&
15131 ! self::is_cron() &&
15132 ! self::is_admin_post()
15133 );
15134 }
15135
15136 /**
15137 * Check if a real user is in the customizer view.
15138 *
15139 * @author Vova Feldman (@svovaf)
15140 * @since 1.2.2.7
15141 *
15142 * @return bool
15143 */
15144 static function is_customizer() {
15145 return is_customize_preview();
15146 }
15147
15148 /**
15149 * Check if running in HTTPS and if site's plan matching the specified plan.
15150 *
15151 * @param string $plan
15152 * @param bool $exact
15153 *
15154 * @return bool
15155 */
15156 function is_ssl_and_plan( $plan, $exact = false ) {
15157 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
15158 }
15159
15160 /**
15161 * Construct plugin's settings page URL.
15162 *
15163 * @author Vova Feldman (@svovaf)
15164 * @since 1.0.4
15165 *
15166 * @param string $page
15167 * @param array $params
15168 * @param bool|null $network
15169 *
15170 * @return string
15171 */
15172 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
15173 if ( is_null( $network ) ) {
15174 $network = (
15175 $this->_is_network_active &&
15176 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
15177 );
15178 }
15179
15180 if ( 0 < count( $params ) ) {
15181 foreach ( $params as $k => $v ) {
15182 $params[ $k ] = urlencode( $v );
15183 }
15184 }
15185
15186 $page_param = $this->_menu->get_slug( $page );
15187
15188 if ( empty( $page ) &&
15189 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
15190 $this->show_opt_in_on_themes_page()
15191 ) {
15192 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
15193
15194 return add_query_arg(
15195 $params,
15196 $this->admin_url( 'themes.php', 'admin', $network )
15197 );
15198 }
15199
15200 if ( ! $this->has_settings_menu() ) {
15201 if ( ! empty( $page ) ) {
15202 // Module doesn't have a setting page, but since the request is for
15203 // a specific Freemius page, use the admin.php path.
15204 return add_query_arg( array_merge( $params, array(
15205 'page' => $page_param,
15206 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15207 } else {
15208 if ( $this->is_activation_mode() ) {
15209 /**
15210 * @author Vova Feldman
15211 * @since 1.2.1.6
15212 *
15213 * If plugin doesn't have a settings page, create one for the opt-in screen.
15214 */
15215 return add_query_arg( array_merge( $params, array(
15216 'page' => $this->_slug,
15217 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15218 } else {
15219 // Plugin without a settings page.
15220 return add_query_arg(
15221 $params,
15222 $this->admin_url( 'plugins.php', 'admin', $network )
15223 );
15224 }
15225 }
15226 }
15227
15228 // Module has a submenu settings page.
15229 if ( ! $this->_menu->is_top_level() ) {
15230 $parent_slug = $this->_menu->get_parent_slug();
15231 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
15232 $parent_slug :
15233 'admin.php';
15234
15235 return add_query_arg( array_merge( $params, array(
15236 'page' => $page_param,
15237 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
15238 }
15239
15240 // Module has a top level CPT settings page.
15241 if ( $this->_menu->is_cpt() ) {
15242 if ( empty( $page ) && $this->is_activation_mode() ) {
15243 return add_query_arg( array_merge( $params, array(
15244 'page' => $page_param
15245 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15246 } else {
15247 if ( ! empty( $page ) ) {
15248 $params['page'] = $page_param;
15249 }
15250
15251 return add_query_arg(
15252 $params,
15253 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
15254 );
15255 }
15256 }
15257
15258 // Module has a custom top level settings page.
15259 return add_query_arg( array_merge( $params, array(
15260 'page' => $page_param,
15261 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15262 }
15263
15264 #--------------------------------------------------------------------------------
15265 #region Multisite
15266 #--------------------------------------------------------------------------------
15267
15268 /**
15269 * @author Leo Fajardo (@leorw)
15270 * @since 2.0.0
15271 *
15272 * @return bool
15273 */
15274 function is_network_active() {
15275 return $this->_is_network_active;
15276 }
15277
15278 /**
15279 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
15280 *
15281 * @author Leo Fajardo (@leorw)
15282 * @since 2.0.0
15283 *
15284 * @param bool|int[] $all_or_blog_ids
15285 */
15286 private function delegate_connection( $all_or_blog_ids = true ) {
15287 $this->_logger->entrance();
15288
15289 $this->_admin_notices->remove_sticky( 'connect_account' );
15290
15291 if ( true === $all_or_blog_ids ) {
15292 // All sites delegation.
15293 $this->_storage->store( 'is_delegated_connection', true, true );
15294 } else {
15295 // Specified sites delegation.
15296 foreach ( $all_or_blog_ids as $blog_id ) {
15297 $this->delegate_site_connection( $blog_id );
15298 }
15299 }
15300
15301 $this->network_upgrade_mode_completed();
15302 }
15303
15304 /**
15305 * Delegate specific network site conncetion to the site admin.
15306 *
15307 * @author Vova Feldman (@svovaf)
15308 * @since 2.0.0
15309 *
15310 * @param int $blog_id
15311 */
15312 private function delegate_site_connection( $blog_id ) {
15313 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
15314 }
15315
15316 /**
15317 * Check if super-admin delegated the connection of ALL sites to the site admins.
15318 *
15319 * @author Vova Feldman (@svovaf)
15320 * @since 2.0.0
15321 *
15322 * @return bool
15323 */
15324 function is_network_delegated_connection() {
15325 if ( ! $this->_is_network_active ) {
15326 return false;
15327 }
15328
15329 return $this->_storage->get( 'is_delegated_connection', false, true );
15330 }
15331
15332 /**
15333 * @author Leo Fajardo (@leorw)
15334 * @since 2.0.0
15335 *
15336 * @param int $blog_id
15337 *
15338 * @return bool
15339 */
15340 function is_site_delegated_connection( $blog_id = 0 ) {
15341 if ( ! $this->_is_network_active ) {
15342 return false;
15343 }
15344
15345 if ( 0 == $blog_id ) {
15346 $blog_id = get_current_blog_id();
15347 }
15348
15349 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15350 }
15351
15352 /**
15353 * Check if delegated the connection. When running within the network admin,
15354 * and haven't specified the blog ID, checks if network level delegated. If running
15355 * within a site admin or specified a blog ID, check if delegated the connection for
15356 * the current context site.
15357 *
15358 * If executed outside the the admin, check if delegated the connection
15359 * for the current context site OR the whole network.
15360 *
15361 * @author Vova Feldman (@svovaf)
15362 * @since 2.0.0
15363 *
15364 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15365 *
15366 * @return bool
15367 */
15368 function is_delegated_connection( $blog_id = 0 ) {
15369 if ( ! $this->_is_network_active ) {
15370 return false;
15371 }
15372
15373 if ( fs_is_network_admin() && 0 == $blog_id ) {
15374 return $this->is_network_delegated_connection();
15375 }
15376
15377 return (
15378 $this->is_network_delegated_connection() ||
15379 $this->is_site_delegated_connection( $blog_id )
15380 );
15381 }
15382
15383 /**
15384 * Check if the current module is active for the site.
15385 *
15386 * @author Vova Feldman (@svovaf)
15387 * @since 2.0.0
15388 *
15389 * @param int $blog_id
15390 *
15391 * @return bool
15392 */
15393 function is_active_for_site( $blog_id ) {
15394 if ( ! is_multisite() ) {
15395 // Not a multisite and this code is executed, means that the plugin is active.
15396 return true;
15397 }
15398
15399 if ( $this->is_theme() ) {
15400 // All themes are site level activated.
15401 return true;
15402 }
15403
15404 if ( $this->_is_network_active ) {
15405 // Plugin was network activated so it's active.
15406 return true;
15407 }
15408
15409 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15410 }
15411
15412 /**
15413 * @todo Implement pagination when accessing the subsites collection.
15414 *
15415 * @author Leo Fajardo (@leorw)
15416 * @since 2.0.0
15417 *
15418 * @param int $limit Default to 1,000
15419 * @param int $offset Default to 0
15420 *
15421 * @return array Active & public sites collection.
15422 */
15423 static function get_sites( $limit = 1000, $offset = 0 ) {
15424 if ( ! is_multisite() ) {
15425 return array();
15426 }
15427
15428 /**
15429 * For consistency with get_blog_list() which only return active public sites.
15430 *
15431 * @author Vova Feldman (@svovaf)
15432 */
15433 $args = array(
15434 /**
15435 * Commented out in order to handle the migration of site options whether the site is public or not.
15436 *
15437 * @author Leo Fajardo (@leorw)
15438 * @since 2.2.1
15439 */
15440 // 'public' => 1,
15441 'archived' => 0,
15442 'mature' => 0,
15443 'spam' => 0,
15444 'deleted' => 0,
15445 'number' => $limit,
15446 'offset' => $offset,
15447 );
15448
15449 return get_sites( $args );
15450 }
15451
15452 /**
15453 * Checks if a given blog is active.
15454 *
15455 * @author Vova Feldman (@svovaf)
15456 * @since 2.0.0
15457 *
15458 * @param $blog_id
15459 *
15460 * @return bool
15461 */
15462 private static function is_site_active( $blog_id ) {
15463 global $wpdb;
15464
15465 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15466
15467 if ( ! is_object( $blog_info ) ) {
15468 return false;
15469 }
15470
15471 return (
15472 true == $blog_info->public &&
15473 false == $blog_info->archived &&
15474 false == $blog_info->mature &&
15475 false == $blog_info->spam &&
15476 false == $blog_info->deleted
15477 );
15478 }
15479
15480 /**
15481 * Get a mapping between the site addresses to their blog IDs.
15482 *
15483 * @author Vova Feldman (@svovaf)
15484 * @since 2.0.0
15485 *
15486 * @return array {
15487 * @key string Site address without protocol with a trailing slash.
15488 * @value int Site's blog ID.
15489 * }
15490 */
15491 private function get_address_to_blog_map() {
15492 $sites = self::get_sites();
15493
15494 // Map site addresses to their blog IDs.
15495 $address_to_blog_map = array();
15496 foreach ( $sites as $site ) {
15497 $blog_id = self::get_site_blog_id( $site );
15498 $address = self::get_unfiltered_site_url( $blog_id, true, true );
15499 $address_to_blog_map[ $address ] = $blog_id;
15500 }
15501
15502 return $address_to_blog_map;
15503 }
15504
15505 /**
15506 * Get a mapping between the site addresses to their blog IDs.
15507 *
15508 * @author Vova Feldman (@svovaf)
15509 * @since 2.0.0
15510 *
15511 * @return array {
15512 * @key int Site's blog ID.
15513 * @value FS_Site Associated install.
15514 * }
15515 */
15516 function get_blog_install_map() {
15517 $sites = self::get_sites();
15518
15519 // Map site blog ID to its install.
15520 $install_map = array();
15521
15522 foreach ( $sites as $site ) {
15523 $blog_id = self::get_site_blog_id( $site );
15524 $install = $this->get_install_by_blog_id( $blog_id );
15525
15526 if ( is_object( $install ) ) {
15527 $install_map[ $blog_id ] = $install;
15528 }
15529 }
15530
15531 return $install_map;
15532 }
15533
15534 /**
15535 * @author Vova Feldman (@svovaf)
15536 * @since 2.5.1
15537 *
15538 * @param bool|null $is_delegated When `true`, returns only connection delegated blog IDs. When `false`, only non-delegated blog IDs.
15539 *
15540 * @return int[]
15541 */
15542 private function get_blog_ids( $is_delegated = null ) {
15543 $blog_ids = array();
15544
15545 $sites = self::get_sites();
15546 foreach ( $sites as $site ) {
15547 $blog_id = self::get_site_blog_id( $site );
15548
15549 if (
15550 is_null( $is_delegated ) ||
15551 $is_delegated === $this->is_site_delegated_connection( $blog_id )
15552 ) {
15553 $blog_ids[] = $blog_id;
15554 }
15555 }
15556
15557 return $blog_ids;
15558 }
15559
15560 /**
15561 * @author Vova Feldman (@svovaf)
15562 * @since 2.5.1
15563 *
15564 * @return int[]
15565 */
15566 private function get_non_delegated_blog_ids() {
15567 return $this->get_blog_ids( false );
15568 }
15569
15570 /**
15571 * Gets a map of module IDs that the given user has opted-in to.
15572 *
15573 * @author Leo Fajardo (@leorw)
15574 * @since 2.1.0
15575 *
15576 * @param number $fs_user_id
15577 *
15578 * @return array {
15579 * @key number $plugin_id
15580 * @value bool Always true.
15581 * }
15582 */
15583 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15584 self::$_static_logger->entrance();
15585
15586 if ( ! is_multisite() ) {
15587 $installs = array_merge(
15588 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15589 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15590 );
15591 } else {
15592 $sites = self::get_sites();
15593
15594 $installs = array();
15595 foreach ( $sites as $site ) {
15596 $blog_id = self::get_site_blog_id( $site );
15597
15598 $installs = array_merge(
15599 $installs,
15600 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15601 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15602 );
15603 }
15604 }
15605
15606 $module_ids_map = array();
15607 foreach ( $installs as $install ) {
15608 if ( is_object( $install ) &&
15609 FS_Site::is_valid_id( $install->id ) &&
15610 FS_User::is_valid_id( $install->user_id ) &&
15611 ( $install->user_id == $fs_user_id )
15612 ) {
15613 $module_ids_map[ $install->plugin_id ] = true;
15614 }
15615 }
15616
15617 return $module_ids_map;
15618 }
15619
15620 /**
15621 * @author Leo Fajardo (@leorw)
15622 *
15623 * @return null|array {
15624 * 'install' => FS_Site Module's install,
15625 * 'blog_id' => string The associated blog ID.
15626 * }
15627 */
15628 function find_first_install() {
15629 $sites = self::get_sites();
15630
15631 foreach ( $sites as $site ) {
15632 $blog_id = self::get_site_blog_id( $site );
15633 $install = $this->get_install_by_blog_id( $blog_id );
15634
15635 if ( is_object( $install ) ) {
15636 return array(
15637 'install' => $install,
15638 'blog_id' => $blog_id
15639 );
15640 }
15641 }
15642
15643 return null;
15644 }
15645
15646 /**
15647 * Switches the Freemius site level context to a specified blog.
15648 *
15649 * @author Vova Feldman (@svovaf)
15650 * @since 2.0.0
15651 *
15652 * @param int $blog_id
15653 * @param FS_Site $install
15654 * @param bool $flush
15655 *
15656 * @return bool Since 2.3.1 returns if a switch was made.
15657 */
15658 function switch_to_blog( $blog_id, $install = null, $flush = false ) {
15659 if ( ! is_numeric( $blog_id ) ) {
15660 return false;
15661 }
15662
15663 if ( ! $flush && $blog_id == $this->_context_is_network_or_blog_id ) {
15664 return false;
15665 }
15666
15667 switch_to_blog( $blog_id );
15668 $this->_context_is_network_or_blog_id = $blog_id;
15669
15670 self::$_accounts->set_site_blog_context( $blog_id );
15671 $this->_storage->set_site_blog_context( $blog_id );
15672 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
15673
15674 $this->_site = is_object( $install ) ?
15675 $install :
15676 $this->get_install_by_blog_id( $blog_id );
15677
15678 $this->_user = false;
15679 $this->_licenses = false;
15680 $this->_license = null;
15681 $this->is_whitelabeled = null;
15682
15683 if ( is_object( $this->_site ) ) {
15684 // Try to fetch user from install.
15685 $this->_user = self::_get_user_by_id( $this->_site->user_id );
15686
15687 if ( ! is_object( $this->_user ) &&
15688 FS_User::is_valid_id( $this->_storage->prev_user_id )
15689 ) {
15690 // Try to fetch previously saved user.
15691 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
15692
15693 if ( ! is_object( $this->_user ) ) {
15694 // Fallback to network's user.
15695 $this->_user = $this->get_network_user();
15696 }
15697 }
15698
15699 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
15700
15701 if ( ! empty( $all_plugin_licenses ) ) {
15702 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
15703 $this->_license = null;
15704 } else {
15705 $license_found = false;
15706 foreach ( $all_plugin_licenses as $license ) {
15707 if ( $license->id == $this->_site->license_id ) {
15708 // License found.
15709 $this->_license = $license;
15710 $license_found = true;
15711 break;
15712 }
15713 }
15714
15715 if ( $license_found ) {
15716 $this->link_license_2_user( $this->_license->id, $this->_user->id );
15717 }
15718 }
15719
15720 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15721 }
15722 }
15723
15724 unset( $this->_site_api );
15725 unset( $this->_user_api );
15726
15727 return true;
15728 }
15729
15730 /**
15731 * Restore the blog context to the blog that originally loaded the module.
15732 *
15733 * @author Vova Feldman (@svovaf)
15734 * @since 2.0.0
15735 */
15736 function restore_current_blog() {
15737 $this->switch_to_blog( $this->_blog_id );
15738 }
15739
15740 /**
15741 * @author Vova Feldman (@svovaf)
15742 * @since 2.0.0
15743 *
15744 * @param array|WP_Site $site
15745 *
15746 * @return int
15747 */
15748 static function get_site_blog_id( &$site ) {
15749 return ( $site instanceof WP_Site ) ?
15750 $site->blog_id :
15751 ( is_object( $site ) && isset( $site->userblog_id ) ?
15752 $site->userblog_id :
15753 $site['blog_id'] );
15754 }
15755
15756 /**
15757 * @author Vova Feldman (@svovaf)
15758 * @since 2.5.1
15759 *
15760 * @param WP_Site[]|array[] $sites
15761 *
15762 * @return int[]
15763 */
15764 static function get_sites_blog_ids( $sites ) {
15765 $blog_ids = array();
15766 foreach ( $sites as $site ) {
15767 $blog_ids[] = self::get_site_blog_id( $site );
15768 }
15769
15770 return $blog_ids;
15771 }
15772
15773 /**
15774 * @author Leo Fajardo (@leorw)
15775 * @since 2.0.0
15776 *
15777 * @param array|WP_Site|null $site
15778 * @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.
15779 *
15780 * @return array
15781 */
15782 function get_site_info( $site = null, $load_registration = false ) {
15783 $this->_logger->entrance();
15784
15785 $fs_hook_snapshot = new FS_Hook_Snapshot();
15786 // Remove all filters from `switch_blog`.
15787 $fs_hook_snapshot->remove( 'switch_blog' );
15788
15789 $switched = false;
15790
15791 $registration_date = null;
15792
15793 if ( is_null( $site ) ) {
15794 $url = self::get_unfiltered_site_url();
15795 $name = get_bloginfo( 'name' );
15796 $blog_id = null;
15797 } else {
15798 $blog_id = self::get_site_blog_id( $site );
15799
15800 if ( get_current_blog_id() != $blog_id ) {
15801 switch_to_blog( $blog_id );
15802 $switched = true;
15803 }
15804
15805 if ( $site instanceof WP_Site ) {
15806 $url = $site->siteurl;
15807 $name = $site->blogname;
15808 $registration_date = $site->registered;
15809 } else {
15810 $url = self::get_unfiltered_site_url( $blog_id );
15811 $name = get_bloginfo( 'name' );
15812 }
15813 }
15814
15815 if ( empty( $registration_date ) && $load_registration ) {
15816 $blog_details = get_blog_details( $blog_id, false );
15817
15818 if ( is_object( $blog_details ) && isset( $blog_details->registered ) ) {
15819 $registration_date = $blog_details->registered;
15820 }
15821 }
15822
15823 $info = array(
15824 'uid' => $this->get_anonymous_id( $blog_id ),
15825 'url' => $url,
15826 );
15827
15828 // Add these diagnostic information only if user allowed to track.
15829 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
15830 $info = array_merge( $info, array(
15831 'title' => $name,
15832 'language' => self::get_sanitized_language(),
15833 ) );
15834 }
15835
15836 if ( is_numeric( $blog_id ) ) {
15837 $info['blog_id'] = $blog_id;
15838 }
15839
15840 if ( ! empty( $registration_date ) ) {
15841 $info[ 'registration_date' ] = $registration_date;
15842 }
15843
15844 if ( $switched ) {
15845 restore_current_blog();
15846 }
15847
15848 // Add the filters back to `switch_blog`.
15849 $fs_hook_snapshot->restore( 'switch_blog' );
15850
15851 return $info;
15852 }
15853
15854 /**
15855 * Load the module's install based on the blog ID.
15856 *
15857 * @author Vova Feldman (@svovaf)
15858 * @since 2.0.0
15859 *
15860 * @param int|null $blog_id
15861 *
15862 * @return FS_Site
15863 */
15864 function get_install_by_blog_id( $blog_id = null ) {
15865 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15866 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15867
15868 if ( is_object( $install ) &&
15869 is_numeric( $install->id ) &&
15870 is_numeric( $install->user_id ) &&
15871 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15872 ) {
15873 // Load site.
15874 $install = clone $install;
15875 }
15876
15877 return $install;
15878 }
15879
15880 /**
15881 * Check if module is installed on a specified site.
15882 *
15883 * @author Vova Feldman (@svovaf)
15884 * @since 2.0.0
15885 *
15886 * @param int|null $blog_id
15887 *
15888 * @return bool
15889 */
15890 function is_installed_on_site( $blog_id = null ) {
15891 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15892 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15893
15894 return (
15895 is_object( $install ) &&
15896 is_numeric( $install->id ) &&
15897 is_numeric( $install->user_id ) &&
15898 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15899 );
15900 }
15901
15902 /**
15903 * Check if super-admin connected at least one site via the network opt-in.
15904 *
15905 * @author Vova Feldman (@svovaf)
15906 * @since 2.0.0
15907 *
15908 * @return bool
15909 */
15910 function is_network_registered() {
15911 if ( ! $this->_is_network_active ) {
15912 return false;
15913 }
15914
15915 return FS_User::is_valid_id( $this->_storage->network_user_id );
15916 }
15917
15918 /**
15919 * Returns the main user associated with the network.
15920 *
15921 * @author Vova Feldman (@svovaf)
15922 * @since 2.0.0
15923 *
15924 * @return FS_User
15925 */
15926 function get_network_user() {
15927 if ( ! $this->_is_network_active ) {
15928 return null;
15929 }
15930
15931 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
15932 self::_get_user_by_id( $this->_storage->network_user_id ) :
15933 null;
15934 }
15935
15936 /**
15937 * Returns the current context user or the network's main user.
15938 *
15939 * @author Vova Feldman (@svovaf)
15940 * @since 2.0.0
15941 *
15942 * @return FS_User
15943 */
15944 function get_current_or_network_user() {
15945 return ( $this->_user instanceof FS_User ) ?
15946 $this->_user :
15947 $this->get_network_user();
15948 }
15949
15950 /**
15951 * Returns the main install associated with the network.
15952 *
15953 * @author Vova Feldman (@svovaf)
15954 * @since 2.0.0
15955 *
15956 * @return FS_Site
15957 */
15958 function get_network_install() {
15959 if ( ! $this->_is_network_active ) {
15960 return null;
15961 }
15962
15963 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15964 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
15965 null;
15966 }
15967
15968 /**
15969 * Returns the blog ID that is associated with the main install.
15970 *
15971 * @author Leo Fajardo (@leorw)
15972 * @since 2.0.0
15973 *
15974 * @return int|null
15975 */
15976 function get_network_install_blog_id() {
15977 if ( ! $this->_is_network_active ) {
15978 return null;
15979 }
15980
15981 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15982 $this->_storage->network_install_blog_id :
15983 null;
15984 }
15985
15986 /**
15987 * Returns the current context install or the network's main install.
15988 *
15989 * @author Vova Feldman (@svovaf)
15990 * @since 2.0.0
15991 *
15992 * @return FS_Site
15993 */
15994 function get_current_or_network_install() {
15995 return ( $this->_site instanceof FS_Site ) ?
15996 $this->_site :
15997 $this->get_network_install();
15998 }
15999
16000 /**
16001 * Check if executing a site level action from the network level admin.
16002 *
16003 * @author Vova Feldman (@svovaf)
16004 * @since 2.0.0
16005 *
16006 * @return false|int If yes, return the requested blog ID.
16007 */
16008 private function is_network_level_site_specific_action() {
16009 if ( ! $this->_is_network_active ) {
16010 return false;
16011 }
16012
16013 if ( ! fs_is_network_admin() ) {
16014 return false;
16015 }
16016
16017 $blog_id = fs_request_get( 'blog_id', '' );
16018
16019 return is_numeric( $blog_id ) ? $blog_id : false;
16020 }
16021
16022 /**
16023 * Check if executing an action from the network level admin.
16024 *
16025 * @author Vova Feldman (@svovaf)
16026 * @since 2.0.0
16027 *
16028 * @return bool
16029 */
16030 private function is_network_level_action() {
16031 return ( $this->_is_network_active && fs_is_network_admin() );
16032 }
16033
16034 /**
16035 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
16036 * 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.
16037 *
16038 * @author Vova Feldman (@svovaf)
16039 * @since 2.0.0
16040 *
16041 * @param int $context_blog_id
16042 */
16043 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
16044 $this->_logger->entrance();
16045
16046 if ( $this->_is_network_active ) {
16047 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
16048 $installs_map = $this->get_blog_install_map();
16049
16050 foreach ( $installs_map as $blog_id => $install ) {
16051 /**
16052 * @var FS_Site $install
16053 */
16054 if ( $context_blog_id == $blog_id ) {
16055 continue;
16056 }
16057
16058 if ( $install->user_id != $this->_storage->network_user_id ) {
16059 continue;
16060 }
16061
16062 // Switch reference to a blog that is opted-in and belong to the same super-admin.
16063 $this->_storage->network_install_blog_id = $blog_id;
16064 break;
16065 }
16066 }
16067 }
16068
16069 if ( ! $this->is_registered() ) {
16070 return;
16071 }
16072
16073 if ( $this->is_sync_cron_scheduled() &&
16074 $context_blog_id == $this->get_sync_cron_blog_id()
16075 ) {
16076 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
16077 }
16078
16079 if ( $this->is_install_sync_scheduled() &&
16080 $context_blog_id == $this->get_install_sync_cron_blog_id()
16081 ) {
16082 $this->maybe_schedule_install_sync_cron( $context_blog_id );
16083 }
16084 }
16085
16086 /**
16087 * Executed after site deactivation, archive, or flag as spam.
16088 *
16089 * @author Vova Feldman (@svovaf)
16090 * @since 2.0.0
16091 *
16092 * @param int $context_blog_id
16093 */
16094 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
16095 $this->_logger->entrance();
16096
16097 $install = $this->get_install_by_blog_id( $context_blog_id );
16098
16099 if ( ! is_object( $install ) ) {
16100 // Site not connected.
16101 return;
16102 }
16103
16104 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16105
16106 if ( ! $this->is_registered() ) {
16107 return;
16108 }
16109
16110 $current_blog_id = get_current_blog_id();
16111
16112 $this->switch_to_blog( $context_blog_id );
16113
16114 // Send deactivation event.
16115 $this->sync_install( array(
16116 'is_active' => false,
16117 ) );
16118
16119 $this->switch_to_blog( $current_blog_id );
16120 }
16121
16122 /**
16123 * Executed after site deletion.
16124 *
16125 * @author Vova Feldman (@svovaf)
16126 * @since 2.0.0
16127 *
16128 * @param int $context_blog_id
16129 * @param bool $drop True if site's database tables should be dropped. Default is false.
16130 */
16131 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
16132 $this->_logger->entrance();
16133
16134 $install = $this->get_install_by_blog_id( $context_blog_id );
16135
16136 if ( ! is_object( $install ) ) {
16137 // Site not connected.
16138 return;
16139 }
16140
16141 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16142
16143 if ( ! $this->is_registered() ) {
16144 return;
16145 }
16146
16147 $current_blog_id = get_current_blog_id();
16148
16149 $this->switch_to_blog( $context_blog_id );
16150
16151 if ( $drop ) {
16152 // Delete install if dropping site DB.
16153 $this->delete_account_event();
16154 } else {
16155 // Send deactivation event.
16156 $this->sync_install( array(
16157 'is_active' => false,
16158 ) );
16159 }
16160
16161 $this->switch_to_blog( $current_blog_id );
16162 }
16163
16164 /**
16165 * Executed after site deletion, called from wp_delete_site
16166 *
16167 * @author Dario Curvino (@dudo)
16168 * @since 2.5.0
16169 *
16170 * @param WP_Site $old_site
16171 */
16172 public function _after_wpsite_deleted_callback( WP_Site $old_site ) {
16173 $this->_logger->entrance();
16174
16175 $this->_after_site_deleted_callback( $old_site->blog_id, true );
16176 }
16177
16178 /**
16179 * Executed after site re-activation.
16180 *
16181 * @author Vova Feldman (@svovaf)
16182 * @since 2.0.0
16183 *
16184 * @param int $context_blog_id
16185 */
16186 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
16187 $this->_logger->entrance();
16188
16189 $install = $this->get_install_by_blog_id( $context_blog_id );
16190
16191 if ( ! is_object( $install ) ) {
16192 // Site not connected.
16193 return;
16194 }
16195
16196 if ( ! self::is_site_active( $context_blog_id ) ) {
16197 // Site not yet active (can be in spam mode, archived, deleted...).
16198 return;
16199 }
16200
16201 $current_blog_id = get_current_blog_id();
16202
16203 $this->switch_to_blog( $context_blog_id );
16204
16205 // Send re-activation event.
16206 $this->sync_install( array(
16207 'is_active' => true,
16208 ) );
16209
16210 $this->switch_to_blog( $current_blog_id );
16211 }
16212
16213 #endregion Multisite
16214
16215 /**
16216 * @author Leo Fajardo (@leorw)
16217 *
16218 * @param string $path
16219 * @param string $scheme
16220 * @param bool $network
16221 *
16222 * @return string
16223 */
16224 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
16225 return ( $this->_is_network_active && $network ) ?
16226 network_admin_url( $path, $scheme ) :
16227 admin_url( $path, $scheme );
16228 }
16229
16230 /**
16231 * Check if currently in a specified admin page.
16232 *
16233 * @author Vova Feldman (@svovaf)
16234 * @since 1.2.2.7
16235 *
16236 * @param string $page
16237 *
16238 * @return bool
16239 */
16240 function is_admin_page( $page ) {
16241 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
16242 }
16243
16244 /**
16245 * Check if currently in the product's main admin page.
16246 *
16247 * @author Vova Feldman (@svovaf)
16248 * @since 2.3.1
16249 *
16250 * @return bool
16251 */
16252 function is_main_admin_page() {
16253 return $this->is_admin_page( '' );
16254 }
16255
16256 /**
16257 * Get module's main admin setting page URL.
16258 *
16259 * @author Vova Feldman (@svovaf)
16260 * @since 1.2.2.7
16261 *
16262 * @return string
16263 */
16264 function main_menu_url() {
16265 return $this->_menu->main_menu_url();
16266 }
16267
16268 /**
16269 * Check if currently on the theme's setting page or
16270 * on any of the Freemius added pages (via tabs).
16271 *
16272 * @author Vova Feldman (@svovaf)
16273 * @since 1.2.2.7
16274 *
16275 * @return bool
16276 *
16277 * @deprecated Please use is_product_settings_page() instead;
16278 */
16279 function is_theme_settings_page() {
16280 return $this->is_product_settings_page();
16281 }
16282
16283 /**
16284 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
16285 *
16286 * @author Vova Feldman (@svovaf)
16287 * @since 1.2.2.7
16288 *
16289 * @return bool
16290 */
16291 function is_product_settings_page() {
16292 $page = fs_request_get( 'page', '', 'get' );
16293 $menu_slug = $this->_menu->get_slug();
16294
16295 if ( $page === $menu_slug ) {
16296 return true;
16297 }
16298
16299 return fs_starts_with(
16300 // e.g., {$menu_slug}-account, {$menu_slug}-affiliation, etc.
16301 $page,
16302 ( $menu_slug . '-' )
16303 );
16304 }
16305
16306 /**
16307 * Plugin's account page + sync license URL.
16308 *
16309 * @author Vova Feldman (@svovaf)
16310 * @since 1.1.9.1
16311 *
16312 * @param bool|number $plugin_id
16313 * @param bool $add_action_nonce
16314 * @param array $params
16315 *
16316 * @return string
16317 */
16318 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
16319 if ( is_numeric( $plugin_id ) ) {
16320 $params['plugin_id'] = $plugin_id;
16321 }
16322
16323 return $this->get_account_url(
16324 $this->get_unique_affix() . '_sync_license',
16325 $params,
16326 $add_action_nonce
16327 );
16328 }
16329
16330 /**
16331 * Plugin's account URL.
16332 *
16333 * @author Vova Feldman (@svovaf)
16334 * @since 1.0.4
16335 *
16336 * @param bool|string $action
16337 * @param array $params
16338 *
16339 * @param bool $add_action_nonce
16340 *
16341 * @return string
16342 */
16343 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
16344 if ( is_string( $action ) ) {
16345 $params['fs_action'] = $action;
16346 }
16347
16348 self::require_pluggable_essentials();
16349
16350 return ( $add_action_nonce && is_string( $action ) ) ?
16351 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
16352 $this->_get_admin_page_url( 'account', $params );
16353 }
16354
16355 /**
16356 * @author Vova Feldman (@svovaf)
16357 * @since 1.2.0
16358 *
16359 * @param string $tab
16360 * @param bool $action
16361 * @param array $params
16362 * @param bool $add_action_nonce
16363 *
16364 * @return string
16365 *
16366 * @uses get_account_url()
16367 */
16368 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
16369 $params['tab'] = $tab;
16370
16371 return $this->get_account_url( $action, $params, $add_action_nonce );
16372 }
16373
16374 /**
16375 * Plugin's account URL.
16376 *
16377 * @author Vova Feldman (@svovaf)
16378 * @since 1.0.4
16379 *
16380 * @param bool|string $topic
16381 * @param bool|string $message
16382 * @param bool|string $summary Since 2.5.1.
16383 *
16384 * @return string
16385 */
16386 function contact_url( $topic = false, $message = false, $summary = false ) {
16387 $params = array();
16388 if ( is_string( $topic ) ) {
16389 $params['topic'] = $topic;
16390 }
16391 if ( is_string( $message ) ) {
16392 $params['message'] = $message;
16393 }
16394
16395 if ( is_string( $summary ) ) {
16396 $params['summary'] = $summary;
16397 }
16398
16399 if ( $this->is_addon() ) {
16400 $params['addon_id'] = $this->get_id();
16401
16402 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
16403 } else {
16404 return $this->_get_admin_page_url( 'contact', $params );
16405 }
16406 }
16407
16408 /**
16409 * Add-on direct info URL.
16410 *
16411 * @author Vova Feldman (@svovaf)
16412 * @since 1.1.0
16413 *
16414 * @param string $slug
16415 *
16416 * @return string
16417 */
16418 function addon_url( $slug ) {
16419 return $this->_get_admin_page_url( 'addons', array(
16420 'slug' => $slug
16421 ) );
16422 }
16423
16424 /**
16425 * Add-ons URL.
16426 *
16427 * @author Vova Feldman (@svovaf)
16428 * @since 2.4.5
16429 *
16430 * @return string
16431 */
16432 function get_addons_url() {
16433 return $this->_get_admin_page_url( 'addons' );
16434 }
16435
16436 /* Logger
16437 ------------------------------------------------------------------------------------------------------------------*/
16438 /**
16439 * @param string $id
16440 * @param bool $prefix_slug
16441 *
16442 * @return FS_Logger
16443 */
16444 function get_logger( $id = '', $prefix_slug = true ) {
16445 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16446 }
16447
16448 /**
16449 * Note: This method is used externally so don't delete it.
16450 *
16451 * @param $id
16452 * @param bool $load_options
16453 * @param bool $prefix_slug
16454 *
16455 * @return FS_Option_Manager
16456 */
16457 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16458 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16459 }
16460
16461 /* Security
16462 ------------------------------------------------------------------------------------------------------------------*/
16463 private static function _encrypt( $str ) {
16464 if ( is_null( $str ) ) {
16465 return null;
16466 }
16467
16468 /**
16469 * The encrypt/decrypt functions are used to protect
16470 * the user from messing up with some of the sensitive
16471 * data stored for the module as a JSON in the database.
16472 *
16473 * I used the same suggested hack by the theme review team.
16474 * For more details, look at the function `Base64UrlDecode()`
16475 * in `./sdk/FreemiusBase.php`.
16476 *
16477 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16478 *
16479 * @author Vova Feldman (@svovaf)
16480 * @since 1.2.2
16481 */
16482 $fn = 'base64' . '_encode';
16483
16484 return $fn( $str );
16485 }
16486
16487 static function _decrypt( $str ) {
16488 if ( is_null( $str ) ) {
16489 return null;
16490 }
16491
16492 /**
16493 * The encrypt/decrypt functions are used to protect
16494 * the user from messing up with some of the sensitive
16495 * data stored for the module as a JSON in the database.
16496 *
16497 * I used the same suggested hack by the theme review team.
16498 * For more details, look at the function `Base64UrlDecode()`
16499 * in `./sdk/FreemiusBase.php`.
16500 *
16501 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16502 *
16503 * @author Vova Feldman (@svovaf)
16504 * @since 1.2.2
16505 */
16506 $fn = 'base64' . '_decode';
16507
16508 return $fn( $str );
16509 }
16510
16511 /**
16512 * @author Vova Feldman (@svovaf)
16513 * @since 1.0.5
16514 *
16515 * @param FS_Entity $entity
16516 *
16517 * @return FS_Entity Return an encrypted clone entity.
16518 */
16519 private static function _encrypt_entity( FS_Entity $entity ) {
16520 $clone = clone $entity;
16521 $props = get_object_vars( $entity );
16522
16523 foreach ( $props as $key => $val ) {
16524 $clone->{$key} = self::_encrypt( $val );
16525 }
16526
16527 return $clone;
16528 }
16529
16530 /**
16531 * @author Vova Feldman (@svovaf)
16532 * @since 1.0.5
16533 *
16534 * @param FS_Entity $entity
16535 *
16536 * @return FS_Entity Return an decrypted clone entity.
16537 */
16538 private static function decrypt_entity( FS_Entity $entity ) {
16539 $clone = clone $entity;
16540 $props = get_object_vars( $entity );
16541
16542 foreach ( $props as $key => $val ) {
16543 $clone->{$key} = self::_decrypt( $val );
16544 }
16545
16546 return $clone;
16547 }
16548
16549 /**
16550 * @author Vova Feldman (@svovaf)
16551 * @since 1.0.7
16552 *
16553 * @param string $email
16554 *
16555 * @return FS_User|false
16556 */
16557 public static function _get_user_by_email( $email ) {
16558 self::$_static_logger->entrance();
16559
16560 $email = trim( strtolower( $email ) );
16561
16562 $users = self::get_all_users();
16563
16564 if ( is_array( $users ) ) {
16565 foreach ( $users as $user ) {
16566 if ( $email === trim( strtolower( $user->email ) ) ) {
16567 return $user;
16568 }
16569 }
16570 }
16571
16572 return false;
16573 }
16574
16575 #----------------------------------------------------------------------------------
16576 #region Account (Loading, Updates & Activation)
16577 #----------------------------------------------------------------------------------
16578
16579 /***
16580 * Load account information (user + site).
16581 *
16582 * @author Vova Feldman (@svovaf)
16583 * @since 1.0.1
16584 */
16585 private function _load_account() {
16586 $this->_logger->entrance();
16587
16588 $this->do_action( 'before_account_load' );
16589
16590 $users = self::get_all_users();
16591 $plans = self::get_all_plans( $this->_module_type );
16592
16593 if ( $this->_logger->is_on() && is_admin() ) {
16594 $this->_logger->log( 'users = ' . var_export( $users, true ) );
16595 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
16596 }
16597
16598 $site = fs_is_network_admin() ?
16599 $this->get_network_install() :
16600 $this->get_install_by_blog_id();
16601
16602 if ( fs_is_network_admin() &&
16603 $this->is_network_active() &&
16604 ! is_object( $site ) &&
16605 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
16606 ) {
16607 $first_install = $this->find_first_install();
16608
16609 if ( is_null( $first_install ) ) {
16610 unset( $this->_storage->network_install_blog_id );
16611 } else {
16612 $site = $first_install['install'];
16613 $this->_storage->network_install_blog_id = $first_install['blog_id'];
16614 }
16615 }
16616
16617 if ( is_object( $site ) &&
16618 is_numeric( $site->id ) &&
16619 is_numeric( $site->user_id ) &&
16620 FS_Plugin_Plan::is_valid_id( $site->plan_id )
16621 ) {
16622 // Load site.
16623 $this->_site = $site;
16624 }
16625
16626 $user = null;
16627 if ( fs_is_network_admin() && $this->_is_network_active ) {
16628 $user = $this->get_network_user();
16629 }
16630
16631 if ( is_object( $user ) ) {
16632 $this->_user = clone $user;
16633 } else if ( $this->_site ) {
16634 $user = self::_get_user_by_id( $this->_site->user_id );
16635
16636 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
16637 /**
16638 * Try to load the previous owner. This recovery is used for the following use-case:
16639 * 1. Opt-in
16640 * 2. Cloning site1 to site2
16641 * 3. Ownership switch in site1 (same applies for site2)
16642 * 4. Install data sync on site2
16643 * 5. Now site2's install is associated with the new owner which does not exists locally.
16644 */
16645 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
16646 }
16647
16648 if ( ! is_object( $user ) ) {
16649 /**
16650 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
16651 */
16652 if (
16653 ! isset( $this->_storage->user_recovery_from_install_last_attempt_timestamp ) ||
16654 time() > ( $this->_storage->user_recovery_from_install_last_attempt_timestamp + FS_Clone_Manager::CLONE_RESOLUTION_MAX_EXECUTION_TIME )
16655 ) {
16656 $user = $this->sync_user_by_current_install();
16657 } else {
16658 return;
16659 }
16660
16661 if ( is_object( $user ) ) {
16662 $this->_storage->user_was_recovered_from_install = true;
16663 } else {
16664 $this->_storage->user_recovery_from_install_attempts = isset( $this->_storage->user_recovery_from_install_attempts ) ?
16665 ( $this->_storage->user_recovery_from_install_attempts + 1 ) :
16666 1;
16667
16668 if ( $this->_storage->user_recovery_from_install_attempts >= 3 ) {
16669 $this->delete_current_install( false );
16670 } else {
16671 $this->_storage->user_recovery_from_install_last_attempt_timestamp = time();
16672
16673 return;
16674 }
16675 }
16676 }
16677
16678 $this->_user = ( $user instanceof FS_User ) ?
16679 clone $user :
16680 null;
16681 }
16682
16683 if ( is_object( $this->_user ) ) {
16684 // Load licenses.
16685 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16686 }
16687
16688 if ( is_object( $this->_site ) ) {
16689 // Load plans.
16690 $this->_plans = isset( $plans[ $this->_slug ] ) ?
16691 $plans[ $this->_slug ] :
16692 array();
16693
16694 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
16695 $this->_sync_plans();
16696 } else {
16697 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16698 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
16699 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
16700 } else {
16701 unset( $this->_plans[ $i ] );
16702 }
16703 }
16704 }
16705
16706 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
16707
16708 if ( $this->_site->version != $this->get_plugin_version() ) {
16709 // If stored install version is different than current installed plugin version,
16710 // then update plugin version event.
16711 $this->update_plugin_version_event();
16712 }
16713 }
16714
16715 if ( true === $this->_storage->require_license_activation &&
16716 ! fs_request_get_bool( 'require_license', true )
16717 ) {
16718 $this->_storage->require_license_activation = false;
16719 }
16720
16721 if ( $this->is_theme() ) {
16722 $this->_register_account_hooks();
16723 }
16724
16725 if ( $this->is_user_in_admin() && $this->is_clone() ) {
16726 if ( empty( FS_Clone_Manager::instance()->get_clone_identification_timestamp() ) ) {
16727 FS_Clone_Manager::instance()->store_clone_identification_timestamp();
16728 }
16729
16730 FS_Clone_Manager::instance()->maybe_update_clone_resolution_support_flag( $this->_storage->sdk_last_version );
16731 $this->send_pending_clone_update_once();
16732 }
16733 }
16734
16735 /**
16736 * Special user recovery mechanism.
16737 *
16738 * @author Vova Feldman (@svovaf)
16739 * @since 2.0.0
16740 *
16741 * @param number|null $site_user_id
16742 *
16743 * @return \FS_User|mixed
16744 */
16745 private function sync_user_by_current_install( $site_user_id = null ) {
16746 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
16747 $site_user_id :
16748 $this->_site->user_id;
16749
16750 $api = $this->get_api_site_scope();
16751
16752 $uid = $this->get_anonymous_id();
16753 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
16754
16755 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
16756
16757 if ( $this->is_api_result_entity( $result ) ) {
16758 $user = new FS_User( $result );
16759 $this->_user = $user;
16760 $this->_store_user();
16761
16762 return $user;
16763 }
16764
16765 $error_code = FS_Api::get_error_code( $result );
16766
16767 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
16768 /**
16769 * Those API errors will continue coming and are not recoverable with the
16770 * current site's data. Therefore, extend the API call's cached result to 7 days.
16771 */
16772 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
16773 }
16774
16775 return $result;
16776 }
16777
16778 /**
16779 * @author Vova Feldman (@svovaf)
16780 * @since 1.0.1
16781 *
16782 * @param FS_User $user
16783 * @param FS_Site $site
16784 * @param bool|array $plans
16785 */
16786 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
16787 $site->user_id = $user->id;
16788
16789 $this->_site = $site;
16790 $this->_user = $user;
16791 if ( false !== $plans ) {
16792 $this->_plans = $plans;
16793 }
16794
16795 $this->send_install_update();
16796
16797 $this->_store_account();
16798
16799 }
16800
16801 /**
16802 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
16803 * Each version is trimmed after the 16th char.
16804 *
16805 * @author Vova Feldman (@svovaf)
16806 * @since 2.2.1
16807 *
16808 * @return array
16809 */
16810 private function get_versions() {
16811 $versions = array();
16812 $versions['sdk_version'] = $this->version;
16813
16814 // Collect these diagnostic information only if it's allowed.
16815 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
16816 $versions['platform_version'] = get_bloginfo( 'version' );
16817 $versions['programming_language_version'] = phpversion();
16818 }
16819
16820 foreach ( $versions as $k => $version ) {
16821 $versions[ $k ] = self::get_api_sanitized_property( $k, $version );
16822 }
16823
16824 return $versions;
16825 }
16826
16827 /**
16828 * Get sanitized site language.
16829 *
16830 * @param string $language
16831 * @param int $max_len
16832 *
16833 * @since 2.5.1
16834 * @author Vova Feldman (@svovaf)
16835 *
16836 * @return string
16837 */
16838 private static function get_sanitized_language( $language = '', $max_len = self::LANGUAGE_MAX_CHARS ) {
16839 if ( empty( $language ) ) {
16840 $language = get_bloginfo( 'language' );
16841 }
16842
16843 return substr( $language, 0, $max_len );
16844 }
16845
16846 /**
16847 * Get core version stripped from pre-release and build.
16848 *
16849 * @since 2.5.1
16850 * @author Vova Feldman (@svovaf)
16851 *
16852 * @param string $version
16853 * @param int $parts
16854 * @param int $max_len
16855 * @param bool $include_pre_release
16856 *
16857 * @return string
16858 */
16859 private static function get_core_version(
16860 $version,
16861 $parts = 3,
16862 $max_len = self::VERSION_MAX_CHARS,
16863 $include_pre_release = false
16864 ) {
16865 if ( empty( $version ) ) {
16866 // Version is empty.
16867 return '';
16868 }
16869
16870 if ( is_numeric( $version ) ) {
16871 $is_float_version = is_float( $version );
16872
16873 $version = (string) $version;
16874
16875 /**
16876 * 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.
16877 */
16878 if ( $is_float_version && false === strpos( $version, '.' ) ) {
16879 $version .= '.0';
16880 }
16881 }
16882
16883 if ( ! is_string( $version ) ) {
16884 return '';
16885 }
16886
16887 if ( $parts < 1 ) {
16888 return '';
16889 }
16890
16891 $pre_release_regex = $include_pre_release ?
16892 '(\-(alpha|beta|RC)([0-9]+)?)?' :
16893 '';
16894
16895 if ( 0 === preg_match( '/^([0-9]+(\.[0-9]+){0,' . ( $parts - 1 ) . '}' . $pre_release_regex . ')/i', $version, $matches ) ) {
16896 // Version is not starting with a digit.
16897 return '';
16898 }
16899
16900 return substr( $matches[1], 0, $max_len );
16901 }
16902
16903 /**
16904 * @param string $prop
16905 * @param mixed $val
16906 *
16907 * @return mixed
16908 *@author Vova Feldman (@svovaf)
16909 *
16910 * @since 2.5.1
16911 */
16912 private static function get_api_sanitized_property( $prop, $val ) {
16913 if ( ! is_string( $val ) || empty( $val ) ) {
16914 return $val;
16915 }
16916
16917 switch ( $prop ) {
16918 case 'programming_language_version':
16919 // Get core PHP version, which can have up to 3 parts (ignore pre-releases).
16920 return self::get_core_version( $val );
16921 case 'platform_version':
16922 // Get the exact WordPress version, which can have up to 3 parts (including pre-releases).
16923 return self::get_core_version( $val, 3, self::VERSION_MAX_CHARS, true );
16924 case 'sdk_version':
16925 // Get the exact SDK version, which can have up to 4 parts.
16926 return self::get_core_version( $val, 4 );
16927 case 'version':
16928 // Get the entire version but just limited in length.
16929 return substr( $val, 0, self::VERSION_MAX_CHARS );
16930 case 'language':
16931 return self::get_sanitized_language( $val );
16932 default:
16933 return $val;
16934 }
16935 }
16936
16937 /**
16938 * @author Leo Fajardo (@leorw)
16939 * @since 2.3.0
16940 *
16941 * @return bool
16942 */
16943 function has_beta_update() {
16944 return (
16945 ! empty( $this->_storage->beta_data ) &&
16946 ( true === $this->_storage->beta_data['is_beta'] ) &&
16947 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
16948 );
16949 }
16950
16951 /**
16952 * @author Leo Fajardo (@leorw)
16953 * @since 2.3.0
16954 *
16955 * @return bool
16956 */
16957 function is_beta() {
16958 return (
16959 ! empty( $this->_storage->beta_data ) &&
16960 ( true === $this->_storage->beta_data['is_beta'] ) &&
16961 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
16962 );
16963 }
16964
16965 /**
16966 * @author Vova Feldman (@svovaf)
16967 * @since 1.1.7.4
16968 *
16969 * @param array $override_with
16970 * @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.
16971 * @param bool $skip_user_info
16972 *
16973 * @return array
16974 */
16975 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null, $skip_user_info = false ) {
16976 $this->_logger->entrance();
16977
16978 $activation_action = $this->get_unique_affix() . '_activate_new';
16979 $return_url = $this->is_anonymous() ?
16980 // If skipped already, then return to the account page.
16981 $this->get_account_url( $activation_action, array(), false ) :
16982 // Return to the module's main page.
16983 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
16984
16985 $versions = $this->get_versions();
16986
16987 $params = array_merge( $versions, array(
16988 'plugin_slug' => $this->_slug,
16989 'plugin_id' => $this->get_id(),
16990 'plugin_public_key' => $this->get_public_key(),
16991 'plugin_version' => $this->get_plugin_version(),
16992 'return_url' => fs_nonce_url( $return_url, $activation_action ),
16993 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
16994 'account',
16995 array( 'fs_action' => 'sync_user' )
16996 ), 'sync_user' ),
16997 'is_premium' => $this->is_premium(),
16998 'is_active' => true,
16999 'is_uninstalled' => false,
17000 'is_localhost' => WP_FS__IS_LOCALHOST,
17001 ) );
17002
17003 if (
17004 ! $skip_user_info &&
17005 (
17006 empty( $override_with['user_firstname'] ) ||
17007 empty( $override_with['user_lastname'] ) ||
17008 empty( $override_with['user_email'] )
17009 )
17010 ) {
17011 $current_user = self::_get_current_wp_user();
17012
17013 $params['user_firstname'] = $current_user->user_firstname;
17014 $params['user_lastname'] = $current_user->user_lastname;
17015 $params['user_email'] = $current_user->user_email;
17016 }
17017
17018 if ( $this->is_addon() ) {
17019 $parent_fs = $this->get_parent_instance();
17020
17021 $params['parent_plugin_slug'] = $parent_fs->_slug;
17022 $params['parent_plugin_id'] = $parent_fs->get_id();
17023 }
17024
17025 if ( true === $network_level_or_blog_id ) {
17026 if ( ! isset( $override_with['sites'] ) ) {
17027 $params['sites'] = $this->get_sites_for_network_level_optin();
17028 }
17029 } else {
17030 $site = is_numeric( $network_level_or_blog_id ) ?
17031 array( 'blog_id' => $network_level_or_blog_id ) :
17032 null;
17033
17034 $site = $this->get_site_info( $site );
17035
17036 $diagnostic_info = array();
17037 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17038 $diagnostic_info = array(
17039 'site_name' => $site['title'],
17040 'language' => self::get_sanitized_language( $site['language'] ),
17041 );
17042 }
17043
17044 $params = array_merge( $params, $diagnostic_info, array(
17045 'site_uid' => $site['uid'],
17046 'site_url' => $site['url'],
17047 ) );
17048 }
17049
17050 if ( $this->is_pending_activation() &&
17051 ! empty( $this->_storage->pending_license_key )
17052 ) {
17053 $params['license_key'] = $this->_storage->pending_license_key;
17054 }
17055
17056 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
17057 // Even though rand() is known for its security issues,
17058 // the timestamp adds another layer of protection.
17059 // It would be very hard for an attacker to get the secret key form here.
17060 // Plus, this should never run in production since the secret should never
17061 // be included in the production version.
17062 $params['ts'] = WP_FS__SCRIPT_START_TIME;
17063 $params['salt'] = md5( uniqid( rand() ) );
17064 $params['secure'] = md5(
17065 $params['ts'] .
17066 $params['salt'] .
17067 $this->get_secret_key()
17068 );
17069 }
17070
17071 if ( is_multisite() && function_exists( 'get_network' ) ) {
17072 $params['network_uid'] = $this->get_anonymous_network_id();
17073 }
17074
17075 return array_merge( $params, $override_with );
17076 }
17077
17078 /**
17079 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
17080 * 2. If there was an API error, return the API result.
17081 *
17082 * @author Vova Feldman (@svovaf)
17083 * @since 1.1.7.4
17084 *
17085 * @param string|bool $email
17086 * @param string|bool $first
17087 * @param string|bool $last
17088 * @param string|bool $license_key
17089 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
17090 * In this case, the user and site info will be sent to the server but no
17091 * data will be saved to the WP installation's database.
17092 * @param number|bool $trial_plan_id
17093 * @param bool $is_disconnected Whether to opt in without tracking.
17094 * @param null|bool $is_marketing_allowed
17095 * @param array $sites If network-level opt-in, an array of containing details of sites.
17096 * @param bool $redirect
17097 * @param null|number $license_owner_id
17098 *
17099 * @return string|object
17100 * @use WP_Error
17101 */
17102 function opt_in(
17103 $email = false,
17104 $first = false,
17105 $last = false,
17106 $license_key = false,
17107 $is_uninstall = false,
17108 $trial_plan_id = false,
17109 $is_disconnected = false,
17110 $is_marketing_allowed = null,
17111 $sites = array(),
17112 $redirect = true,
17113 $license_owner_id = null
17114 ) {
17115 $this->_logger->entrance();
17116
17117 /**
17118 * @since 1.2.1 If activating with license key, ignore the context-user
17119 * since the user will be automatically loaded from the license.
17120 */
17121 if ( empty( $license_key ) ) {
17122 // Clean up pending license if opt-ing in again.
17123 $this->_storage->remove( 'pending_license_key' );
17124
17125 if ( ! $is_uninstall ) {
17126 if ( false === $email ) {
17127 $current_user = self::_get_current_wp_user();
17128 $email = $current_user->user_email;
17129 }
17130
17131 $fs_user = Freemius::_get_user_by_email( $email );
17132 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
17133 return $this->install_with_user(
17134 $fs_user,
17135 false,
17136 $trial_plan_id,
17137 $redirect,
17138 true,
17139 $sites
17140 );
17141 }
17142 }
17143 }
17144
17145 $skip_user_info = ( ! empty( $license_key ) && FS_User::is_valid_id( $license_owner_id ) );
17146
17147 $user_info = array();
17148
17149 if ( ! $skip_user_info ) {
17150 if ( ! empty( $email ) ) {
17151 $user_info['user_email'] = $email;
17152 }
17153
17154 if ( ! empty( $first ) ) {
17155 $user_info['user_firstname'] = $first;
17156 }
17157
17158 if ( ! empty( $last ) ) {
17159 $user_info['user_lastname'] = $last;
17160 }
17161 }
17162
17163 if ( ! empty( $sites ) ) {
17164 $is_network = true;
17165
17166 $user_info['sites'] = $sites;
17167 } else {
17168 $is_network = false;
17169 }
17170
17171 $params = $this->get_opt_in_params( $user_info, $is_network, $skip_user_info );
17172
17173 $filtered_license_key = false;
17174 if ( is_string( $license_key ) ) {
17175 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
17176 $params['license_key'] = $filtered_license_key;
17177 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17178 $params['trial_plan_id'] = $trial_plan_id;
17179 }
17180
17181 if ( $is_uninstall ) {
17182 $params['uninstall_params'] = array(
17183 'reason_id' => $this->_storage->uninstall_reason->id,
17184 'reason_info' => $this->_storage->uninstall_reason->info
17185 );
17186 }
17187
17188 if ( isset( $params['license_key'] ) ) {
17189 $fs_user = Freemius::_get_user_by_email( $email );
17190
17191 if ( is_object( $fs_user ) ) {
17192 /**
17193 * If opting in with a context license and the context WP Admin user already opted in
17194 * before from the current site, add the user context security params to avoid the
17195 * unnecessary email activation when the context license is owned by the same context user.
17196 *
17197 * @author Leo Fajardo (@leorw)
17198 * @since 1.2.3
17199 */
17200 $params = array_merge( $params, FS_Security::instance()->get_context_params(
17201 $fs_user,
17202 false,
17203 'install_with_existing_user'
17204 ) );
17205 }
17206 }
17207
17208 if ( is_bool( $is_marketing_allowed ) ) {
17209 $params['is_marketing_allowed'] = $is_marketing_allowed;
17210 }
17211
17212 $params['is_disconnected'] = $is_disconnected;
17213 $params['format'] = 'json';
17214 $params['is_extensions_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed();
17215 $params['is_diagnostic_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed();
17216
17217 $request = array(
17218 'method' => 'POST',
17219 'body' => $params,
17220 'timeout' => 60,
17221 );
17222
17223 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
17224 $response = self::safe_remote_post( $url, $request );
17225
17226 if ( is_wp_error( $response ) ) {
17227 /**
17228 * @var WP_Error $response
17229 */
17230 $result = new stdClass();
17231
17232 $error_code = $response->get_error_code();
17233 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
17234
17235 $result->error = (object) array(
17236 'type' => $error_type,
17237 'message' => $response->get_error_message(),
17238 'code' => $error_code,
17239 'http' => 402
17240 );
17241
17242 $this->maybe_modify_api_curl_error_message( $result );
17243
17244 if ( FS_Api::is_blocked( $result ) ) {
17245 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
17246 }
17247
17248 $is_connected = null;
17249
17250 if ( empty( $license_key ) && $this->is_enable_anonymous() ) {
17251 $this->skip_connection( fs_is_network_admin() );
17252
17253 $is_connected = ( ! FS_Api::is_blocked( $result ) );
17254 }
17255
17256 $this->update_connectivity_info( $is_connected );
17257
17258 return $result;
17259 }
17260
17261 $this->update_connectivity_info( true );
17262
17263 // Module is being uninstalled, don't handle the returned data.
17264 if ( $is_uninstall ) {
17265 return true;
17266 }
17267
17268 /**
17269 * 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.
17270 *
17271 * @author Vova Feldman (@svovaf)
17272 * @since 1.2.3
17273 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
17274 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
17275 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
17276 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
17277 */
17278 $decoded = is_string( $response['body'] ) ?
17279 json_decode( $response['body'] ) :
17280 null;
17281
17282 if ( empty( $decoded ) ) {
17283 return false;
17284 }
17285
17286 if ( ! $this->is_api_result_object( $decoded ) ) {
17287 if ( ! empty( $params['license_key'] ) ) {
17288 // Pass the fully entered license key to the failure handler.
17289 $params['license_key'] = $license_key;
17290 }
17291
17292 return $is_uninstall ?
17293 $decoded :
17294 $this->apply_filters( 'after_install_failure', $decoded, $params );
17295 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
17296 if ( $is_network ) {
17297 $site_ids = array();
17298 foreach ( $sites as $site ) {
17299 $site_ids[] = $site['blog_id'];
17300 }
17301
17302 /**
17303 * Store the sites so that they can be installed once the user has clicked on the activation link
17304 * in the email.
17305 *
17306 * @author Leo Fajardo (@leorw)
17307 */
17308 $this->_storage->pending_sites_info = array(
17309 'blog_ids' => $site_ids,
17310 'license_key' => $license_key,
17311 'trial_plan_id' => $trial_plan_id
17312 );
17313 }
17314
17315 // Pending activation, add message.
17316 return $this->set_pending_confirmation(
17317 ( isset( $decoded->email ) ?
17318 $decoded->email :
17319 true ),
17320 false,
17321 $filtered_license_key,
17322 ! empty( $params['trial_plan_id'] ),
17323 isset( $decoded->is_suspicious_email ) && $decoded->is_suspicious_email
17324 );
17325 } else if ( isset( $decoded->install_secret_key ) ) {
17326 return $this->install_with_new_user(
17327 $decoded->user_id,
17328 $decoded->user_public_key,
17329 $decoded->user_secret_key,
17330 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17331 $decoded->is_marketing_allowed :
17332 null ),
17333 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17334 $decoded->is_extensions_tracking_allowed :
17335 null ),
17336 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17337 $decoded->is_diagnostic_tracking_allowed :
17338 null ),
17339 $decoded->install_id,
17340 $decoded->install_public_key,
17341 $decoded->install_secret_key,
17342 false
17343 );
17344 } else if ( is_array( $decoded->installs ) ) {
17345 return $this->install_many_with_new_user(
17346 $decoded->user_id,
17347 $decoded->user_public_key,
17348 $decoded->user_secret_key,
17349 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17350 $decoded->is_marketing_allowed :
17351 null ),
17352 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17353 $decoded->is_extensions_tracking_allowed :
17354 null ),
17355 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17356 $decoded->is_diagnostic_tracking_allowed :
17357 null ),
17358 $decoded->installs,
17359 false
17360 );
17361 }
17362
17363 return $decoded;
17364 }
17365
17366 /**
17367 * Set user and site identities.
17368 *
17369 * @author Vova Feldman (@svovaf)
17370 * @since 1.0.9
17371 *
17372 * @param FS_User $user
17373 * @param FS_Site $site
17374 * @param bool $redirect
17375 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
17376 * redirect (or return a URL) to the account page with a special parameter to
17377 * trigger the auto installation processes.
17378 *
17379 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17380 */
17381 function setup_account(
17382 FS_User $user,
17383 FS_Site $site,
17384 $redirect = true,
17385 $auto_install = false
17386 ) {
17387 return $this->setup_network_account(
17388 $user,
17389 array( $site ),
17390 $redirect,
17391 $auto_install,
17392 false
17393 );
17394 }
17395
17396 /**
17397 * Set user and site identities.
17398 *
17399 * @author Vova Feldman (@svovaf)
17400 * @since 2.0.0
17401 *
17402 * @param FS_User $user
17403 * @param FS_Site[] $installs
17404 * @param bool $redirect
17405 * @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.
17406 * @param bool $is_network_level_opt_in
17407 *
17408 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17409 */
17410 function setup_network_account(
17411 FS_User $user,
17412 array $installs,
17413 $redirect = true,
17414 $auto_install = false,
17415 $is_network_level_opt_in = true
17416 ) {
17417 $first_install = $installs[0];
17418
17419 $this->_user = $user;
17420 $this->_site = $first_install;
17421
17422 $this->_sync_plans();
17423
17424 if ( $this->_storage->handle_gdpr_admin_notice &&
17425 $this->should_handle_gdpr_admin_notice() &&
17426 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
17427 ) {
17428 /**
17429 * Clear user lock after an opt-in.
17430 */
17431 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
17432 FS_User_Lock::instance()->unlock();
17433 }
17434
17435 if ( 1 < count( $installs ) || fs_is_network_admin() ) {
17436 // Only network level opt-in can have more than one install.
17437 $is_network_level_opt_in = true;
17438 }
17439
17440 $this->update_connectivity_info( true );
17441
17442 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
17443 // If Freemius was OFF before, turn it on.
17444 $this->turn_on();
17445
17446 $this->handle_account_connection(
17447 $installs,
17448 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
17449 );
17450
17451 if ( is_numeric( $first_install->license_id ) ) {
17452 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
17453 }
17454
17455 $this->_admin_notices->remove_sticky( 'connect_account' );
17456
17457 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
17458 $this->clear_pending_activation_mode();
17459
17460 if ( ! $this->is_paying_or_trial() ) {
17461 $this->_admin_notices->add_sticky(
17462 sprintf( $this->get_text_inline( '%s opt-in was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
17463 'activation_complete'
17464 );
17465 }
17466 }
17467
17468 if ( $this->is_paying_or_trial() ) {
17469 if ( ! $this->is_premium() ||
17470 ! $this->has_premium_version() ||
17471 ! $this->has_settings_menu()
17472 ) {
17473 if ( $this->is_paying() ) {
17474 $this->add_complete_upgrade_instructions_notice(
17475 sprintf(
17476 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
17477 $this->get_plan_title()
17478 ),
17479 'plan_upgraded'
17480 );
17481 } else {
17482 $trial_plan = $this->get_trial_plan();
17483
17484 $this->add_complete_upgrade_instructions_notice(
17485 sprintf(
17486 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
17487 '<i>' . $this->get_plugin_name() . '</i>'
17488 ),
17489 'trial_started',
17490 $trial_plan->title
17491 );
17492 }
17493 }
17494
17495 $this->_admin_notices->remove_sticky( array(
17496 'trial_promotion',
17497 ) );
17498 }
17499
17500 $plugin_id = fs_request_get( 'plugin_id', false );
17501
17502 // Store activation time ONLY for plugins & themes (not add-ons).
17503 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
17504 if ( empty( $this->_storage->activation_timestamp ) ) {
17505 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
17506 }
17507 }
17508
17509 $next_page = '';
17510
17511 $extra = array();
17512 if ( $auto_install ) {
17513 $extra['auto_install'] = 'true';
17514 }
17515
17516 if ( is_numeric( $plugin_id ) ) {
17517 /**
17518 * @author Leo Fajardo (@leorw)
17519 * @since 1.2.1.6
17520 *
17521 * Also sync the license after an anonymous user subscribes.
17522 */
17523 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
17524 // Add-on was installed - sync license right after install.
17525 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
17526 }
17527 } else {
17528 /**
17529 * @author Vova Feldman (@svovaf)
17530 * @since 1.1.9 If site installed with a valid license, sync license.
17531 */
17532 if ( $this->is_paying() ) {
17533 $this->_sync_plugin_license(
17534 true,
17535 // Installs data is already synced in the beginning of this method directly or via _set_account().
17536 false
17537 );
17538 }
17539
17540 // Reload the page with the keys.
17541 $next_page = $this->is_anonymous() ?
17542 // If user previously skipped, redirect to account page.
17543 $this->get_account_url( false, $extra ) :
17544 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
17545 }
17546
17547 if ( ! empty( $next_page ) && $redirect ) {
17548 fs_redirect( $next_page );
17549 }
17550
17551 return $next_page;
17552 }
17553
17554 /**
17555 * Install plugin with new user information after approval.
17556 *
17557 * @author Vova Feldman (@svovaf)
17558 * @since 1.0.7
17559 */
17560 function _install_with_new_user() {
17561 $this->_logger->entrance();
17562
17563 if ( $this->is_registered() ) {
17564 return;
17565 }
17566
17567 $has_pending_activation_confirmation_param = fs_request_has( 'pending_activation' );
17568
17569 $this->update_license_required_permissions_if_anonymous();
17570
17571 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17572 // @todo This logic should be improved because it's executed on every load of a theme.
17573 $this->is_theme()
17574 ) {
17575 // check_admin_referer( $this->_slug . '_activate_new' );
17576
17577 if ( fs_request_has( 'user_secret_key' ) ) {
17578 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17579 $pending_sites_info = $this->_storage->pending_sites_info;
17580
17581 $this->install_many_pending_with_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 $pending_sites_info['blog_ids'],
17589 $pending_sites_info['license_key'],
17590 $pending_sites_info['trial_plan_id']
17591 );
17592 } else {
17593 $this->install_with_new_user(
17594 fs_request_get( 'user_id' ),
17595 fs_request_get_raw( 'user_public_key' ),
17596 fs_request_get_raw( 'user_secret_key' ),
17597 fs_request_get_bool( 'is_marketing_allowed', null ),
17598 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17599 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17600 fs_request_get( 'install_id' ),
17601 fs_request_get_raw( 'install_public_key' ),
17602 fs_request_get_raw( 'install_secret_key' ),
17603 true,
17604 fs_request_get_bool( 'auto_install' )
17605 );
17606 }
17607 } else if ( $has_pending_activation_confirmation_param ) {
17608 $this->set_pending_confirmation(
17609 fs_request_get( 'user_email' ),
17610 true,
17611 false,
17612 false,
17613 fs_request_get_bool( 'is_suspicious_email' ),
17614 fs_request_get_bool( 'has_upgrade_context' ),
17615 fs_request_get( 'support_email_address' )
17616 );
17617 }
17618 }
17619 }
17620
17621 /**
17622 * @author Vova Feldman (@svovaf)
17623 * @since 2.0.0
17624 *
17625 * @param number $id
17626 * @param string $public_key
17627 * @param string $secret_key
17628 *
17629 * @return \FS_User
17630 */
17631 private function setup_user( $id, $public_key, $secret_key ) {
17632 $user = self::_get_user_by_id( $id );
17633
17634 if ( is_object( $user ) ) {
17635 $this->_user = $user;
17636 } else {
17637 $user = new FS_User();
17638 $user->id = $id;
17639 $user->public_key = $public_key;
17640 $user->secret_key = $secret_key;
17641
17642 $this->_user = $user;
17643 $user_result = $this->get_api_user_scope()->get();
17644 $user = new FS_User( $user_result );
17645
17646 $this->_user = $user;
17647 $this->_store_user();
17648 }
17649
17650 return $user;
17651 }
17652
17653 /**
17654 * Install plugin with new user.
17655 *
17656 * You can use this method to sync activation with the Freemius WP SDK where the activation happened outside of the regular opt-in flow, for example if you're using an external licensing server with our api:
17657 *
17658 * https://docs.freemius.com/api/licenses/activate
17659 *
17660 * In that case you can call this method like following:
17661 *
17662 * ```
17663 *
17664 * my_fs()->install_with_new_user(
17665 * $result['user_id'],
17666 * $result['user_public_key'],
17667 * $result['user_secret_key'],
17668 * $result['is_marketing_allowed'],
17669 * null,
17670 * true,
17671 * $result['install_id'],
17672 * $result['install_public_key'],
17673 * $result['install_secret_key'],
17674 * false
17675 * );
17676 *
17677 * ```
17678 *
17679 * Here `$result` represents the object returned by the API endpoint.
17680 *
17681 * @author Vova Feldman (@svovaf)
17682 * @since 1.1.7.4
17683 *
17684 * @param number $user_id
17685 * @param string $user_public_key
17686 * @param string $user_secret_key
17687 * @param bool|null $is_marketing_allowed
17688 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17689 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17690 * @param number $install_id
17691 * @param string $install_public_key
17692 * @param string $install_secret_key
17693 * @param bool $redirect
17694 * @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.
17695 *
17696 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17697 */
17698 public function install_with_new_user(
17699 $user_id,
17700 $user_public_key,
17701 $user_secret_key,
17702 $is_marketing_allowed,
17703 $is_extensions_tracking_allowed,
17704 $is_diagnostic_tracking_allowed,
17705 $install_id,
17706 $install_public_key,
17707 $install_secret_key,
17708 $redirect = true,
17709 $auto_install = false
17710 ) {
17711 /**
17712 * This method is also executed after opting in with a license key since the
17713 * license can be potentially associated with a different owner.
17714 *
17715 * @since 2.0.0
17716 */
17717 $user = self::_get_user_by_id( $user_id );
17718
17719 if ( ! is_object( $user ) ) {
17720 $user = new FS_User();
17721 $user->id = $user_id;
17722 $user->public_key = $user_public_key;
17723 $user->secret_key = $user_secret_key;
17724
17725 $this->_user = $user;
17726 $user_result = $this->get_api_user_scope()->get();
17727 $user = new FS_User( $user_result );
17728 }
17729
17730 $this->_user = $user;
17731
17732 $site = new FS_Site();
17733 $site->id = $install_id;
17734 $site->public_key = $install_public_key;
17735 $site->secret_key = $install_secret_key;
17736
17737 $this->_site = $site;
17738 $site_result = $this->get_api_site_scope( true )->get();
17739 $site = new FS_Site( $site_result );
17740 $this->_site = $site;
17741
17742 if ( ! is_null( $is_marketing_allowed ) ) {
17743 $this->disable_opt_in_notice_and_lock_user();
17744 }
17745
17746 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17747 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17748 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17749 ) );
17750
17751 return $this->setup_account(
17752 $this->_user,
17753 $this->_site,
17754 $redirect,
17755 $auto_install
17756 );
17757 }
17758
17759 /**
17760 * Install plugin with user.
17761 *
17762 * @author Leo Fajardo (@leorw)
17763 * @since 2.0.0
17764 *
17765 * @param number $user_id
17766 * @param string $user_public_key
17767 * @param string $user_secret_key
17768 * @param bool|null $is_marketing_allowed
17769 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17770 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17771 * @param array $site_ids
17772 * @param bool $license_key
17773 * @param bool $trial_plan_id
17774 * @param bool $redirect
17775 *
17776 * @return void
17777 */
17778 private function install_many_pending_with_user(
17779 $user_id,
17780 $user_public_key,
17781 $user_secret_key,
17782 $is_marketing_allowed,
17783 $is_extensions_tracking_allowed,
17784 $is_diagnostic_tracking_allowed,
17785 $site_ids,
17786 $license_key = false,
17787 $trial_plan_id = false,
17788 $redirect = true
17789 ) {
17790 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17791
17792 if ( ! is_null( $is_marketing_allowed ) ) {
17793 $this->disable_opt_in_notice_and_lock_user();
17794 }
17795
17796 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17797 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17798 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17799 ) );
17800
17801 $sites = array();
17802 foreach ( $site_ids as $site_id ) {
17803 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
17804 }
17805
17806 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17807 }
17808
17809 /**
17810 * Multi-site install with a new user.
17811 *
17812 * @author Vova Feldman (@svovaf)
17813 * @since 2.0.0
17814 *
17815 * @param number $user_id
17816 * @param string $user_public_key
17817 * @param string $user_secret_key
17818 * @param bool|null $is_marketing_allowed
17819 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17820 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17821 * @param object[] $installs
17822 * @param bool $redirect
17823 * @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.
17824 *
17825 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17826 */
17827 private function install_many_with_new_user(
17828 $user_id,
17829 $user_public_key,
17830 $user_secret_key,
17831 $is_marketing_allowed,
17832 $is_extensions_tracking_allowed,
17833 $is_diagnostic_tracking_allowed,
17834 array $installs,
17835 $redirect = true,
17836 $auto_install = false
17837 ) {
17838 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17839
17840 if ( ! is_null( $is_marketing_allowed ) ) {
17841 $this->disable_opt_in_notice_and_lock_user();
17842 }
17843
17844 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17845 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17846 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17847 ) );
17848
17849 $install_ids = array();
17850
17851 foreach ( $installs as $install ) {
17852 $install_ids[] = $install->id;
17853 }
17854
17855 $items_per_request = 25;
17856 $left = count( $install_ids );
17857 $offset = 0;
17858
17859 $installs = array();
17860 while ( $left > 0 ) {
17861 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, $items_per_request ) ) );
17862
17863 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
17864 // @todo Handle API error.
17865 }
17866
17867 $installs = array_merge( $installs, $result->installs );
17868
17869 $left -= $items_per_request;
17870 $offset += $items_per_request;
17871 }
17872
17873 foreach ( $installs as &$install ) {
17874 $install = new FS_Site( $install );
17875 }
17876
17877 return $this->setup_network_account(
17878 $this->_user,
17879 $installs,
17880 $redirect,
17881 $auto_install
17882 );
17883 }
17884
17885 /**
17886 * @author Vova Feldman (@svovaf)
17887 * @since 1.1.7.4
17888 *
17889 * @param string|bool $email
17890 * @param bool $redirect
17891 * @param string|bool $license_key Since 1.2.1.5
17892 * @param bool $is_pending_trial Since 1.2.1.5
17893 * @param bool $is_suspicious_email Since 2.5.0
17894 * @param bool $has_upgrade_context Since 2.5.3
17895 * @param bool|string $support_email_address Since 2.5.3
17896 *
17897 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
17898 */
17899 private function set_pending_confirmation(
17900 $email = false,
17901 $redirect = true,
17902 $license_key = false,
17903 $is_pending_trial = false,
17904 $is_suspicious_email = false,
17905 $has_upgrade_context = false,
17906 $support_email_address = false
17907 ) {
17908 $is_network_admin = fs_is_network_admin();
17909
17910 if ( $this->_ignore_pending_mode && ! $has_upgrade_context ) {
17911 /**
17912 * If explicitly asked to ignore pending mode, set to anonymous mode
17913 * 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).
17914 *
17915 * @author Vova Feldman
17916 * @since 1.2.1.6
17917 */
17918 $this->skip_connection( $is_network_admin );
17919 } else {
17920 // Install must be activated via email since
17921 // user with the same email already exist.
17922 $this->_storage->is_pending_activation = true;
17923 $this->_add_pending_activation_notice(
17924 $email,
17925 $is_pending_trial,
17926 $is_suspicious_email,
17927 $has_upgrade_context,
17928 $support_email_address
17929 );
17930 }
17931
17932 if ( ! empty( $license_key ) ) {
17933 $this->_storage->pending_license_key = $license_key;
17934 }
17935
17936 // Remove the opt-in sticky notice.
17937 $this->_admin_notices->remove_sticky( array(
17938 'connect_account',
17939 'trial_promotion',
17940 ) );
17941
17942 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
17943
17944 if ( $redirect ) {
17945 // Reload the page with a pending activation message.
17946 fs_redirect( $next_page );
17947 }
17948
17949 return $next_page;
17950 }
17951
17952 /**
17953 * Install plugin with current logged WP user info.
17954 *
17955 * @author Vova Feldman (@svovaf)
17956 * @since 1.0.7
17957 */
17958 function _install_with_current_user() {
17959 $this->_logger->entrance();
17960
17961 if ( $this->is_registered() ) {
17962 return;
17963 }
17964
17965 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
17966 check_admin_referer( $this->get_unique_affix() . '_activate_existing' );
17967
17968 /**
17969 * @author Vova Feldman (@svovaf)
17970 * @since 1.1.9 Add license key if given.
17971 */
17972 $license_key = fs_request_get_raw( 'license_secret_key' );
17973
17974 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17975 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17976 FS_Permission_Manager::PERMISSION_EXTENSIONS => fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17977 ) );
17978
17979 $this->install_with_current_user( $license_key );
17980 }
17981 }
17982
17983
17984 /**
17985 * @author Vova Feldman (@svovaf)
17986 * @since 1.1.7.4
17987 *
17988 * @param string|bool $license_key
17989 * @param number|bool $trial_plan_id
17990 * @param array $sites Since 2.0.0
17991 * @param bool $redirect
17992 *
17993 * @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.
17994 */
17995 function install_with_current_user(
17996 $license_key = false,
17997 $trial_plan_id = false,
17998 $sites = array(),
17999 $redirect = true
18000 ) {
18001 // Get current logged WP user.
18002 $current_user = self::_get_current_wp_user();
18003
18004 // Find the relevant FS user by the email.
18005 $user = self::_get_user_by_email( $current_user->user_email );
18006
18007 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
18008 }
18009
18010 /**
18011 * @author Vova Feldman (@svovaf)
18012 * @since 2.0.0
18013 *
18014 * @param \FS_User $user
18015 * @param string|bool $license_key
18016 * @param number|bool $trial_plan_id
18017 * @param bool $redirect
18018 * @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.
18019 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
18020 *
18021 * @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.
18022 */
18023 function install_with_user(
18024 FS_User $user,
18025 $license_key = false,
18026 $trial_plan_id = false,
18027 $redirect = true,
18028 $setup_account = true,
18029 $sites = array()
18030 ) {
18031 // We have to set the user before getting user scope API handler.
18032 $this->_user = $user;
18033
18034 // Install the plugin.
18035 $result = $this->create_installs_with_user(
18036 $user,
18037 $license_key,
18038 $trial_plan_id,
18039 $sites,
18040 $redirect
18041 );
18042
18043 if ( ! $this->is_api_result_entity( $result ) &&
18044 ! $this->is_api_result_object( $result, 'installs' )
18045 ) {
18046 // @todo Handler potential API error of the $result
18047 }
18048
18049 if ( empty( $sites ) ) {
18050 $site = new FS_Site( $result );
18051 $this->_site = $site;
18052
18053 if ( ! $setup_account ) {
18054 $this->_store_site();
18055
18056 $this->sync_plan_if_not_exist( $site->plan_id );
18057
18058 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
18059 $this->sync_license_if_not_exist( $site->license_id, $license_key );
18060 }
18061
18062 $this->_admin_notices->remove_sticky( 'connect_account', false );
18063
18064 return $site;
18065 }
18066
18067 return $this->setup_account( $this->_user, $this->_site, $redirect );
18068 } else {
18069 $installs = array();
18070 foreach ( $result->installs as $install ) {
18071 $installs[] = new FS_Site( $install );
18072 }
18073
18074 return $this->setup_network_account(
18075 $user,
18076 $installs,
18077 $redirect
18078 );
18079 }
18080 }
18081
18082 /**
18083 * Initiate an API request to create a collection of installs.
18084 *
18085 * @author Vova Feldman (@svovaf)
18086 * @since 2.0.0
18087 *
18088 * @param \FS_User $user
18089 * @param bool $license_key
18090 * @param bool $trial_plan_id
18091 * @param array $sites
18092 * @param bool $redirect
18093 * @param bool $silent
18094 *
18095 * @return object|mixed
18096 */
18097 private function create_installs_with_user(
18098 FS_User $user,
18099 $license_key = false,
18100 $trial_plan_id = false,
18101 $sites = array(),
18102 $redirect = false,
18103 $silent = false
18104 ) {
18105 $extra_install_params = array(
18106 'uid' => $this->get_anonymous_id(),
18107 'is_disconnected' => false,
18108 );
18109
18110 if ( ! empty( $license_key ) ) {
18111 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
18112
18113 if ( $silent ) {
18114 $extra_install_params['ignore_license_owner'] = true;
18115 }
18116 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
18117 $extra_install_params['trial_plan_id'] = $trial_plan_id;
18118 }
18119
18120 if ( ! empty( $sites ) ) {
18121 $extra_install_params['sites'] = $sites;
18122 }
18123
18124 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
18125
18126 // Install the plugin.
18127 $result = $this->get_api_user_scope_by_user( $user )->call(
18128 "/plugins/{$this->get_id()}/installs.json",
18129 'post',
18130 $args
18131 );
18132
18133 if ( ! $this->is_api_result_entity( $result ) &&
18134 ! $this->is_api_result_object( $result, 'installs' )
18135 ) {
18136 if ( ! empty( $args['license_key'] ) ) {
18137 // Pass the fully entered license key to the failure handler.
18138 $args['license_key'] = $license_key;
18139 }
18140
18141 $result = $this->apply_filters( 'after_install_failure', $result, $args );
18142
18143 if ( ! $silent ) {
18144 $this->_admin_notices->add(
18145 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18146 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
18147 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18148 'error'
18149 );
18150 }
18151
18152 if ( $redirect ) {
18153 /**
18154 * We set the user before getting the user scope API handler, so the user became temporarily
18155 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
18156 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
18157 * activation page based on the return value of `is_registered()`. In addition, in case the
18158 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
18159 * misleading plugin activation errors will be shown on the `Plugins` page.
18160 *
18161 * @author Leo Fajardo (@leorw)
18162 */
18163 $user = $this->_user;
18164
18165 $this->_user = null;
18166
18167 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
18168
18169 /**
18170 * Restore the user after the redirect, this is relevant when there are cases where the redirect will choose not to do anything.
18171 */
18172 $this->_user = $user;
18173 }
18174 }
18175
18176 return $result;
18177 }
18178
18179 /**
18180 * Tries to activate add-on account based on parent plugin info.
18181 *
18182 * @author Vova Feldman (@svovaf)
18183 * @since 1.0.6
18184 *
18185 * @param Freemius $parent_fs
18186 * @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.
18187 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
18188 */
18189 private function _activate_addon_account(
18190 Freemius $parent_fs,
18191 $network_level_or_blog_id = null,
18192 $bundle_license = null
18193 ) {
18194 if ( $this->is_registered() ) {
18195 // Already activated.
18196 return;
18197 }
18198
18199 $permission_ids = FS_Permission_Manager::get_all_permission_ids();
18200 $permissions = array();
18201 foreach ( $permission_ids as $permission_id ) {
18202 $permissions[ $permission_id ] = FS_Permission_Manager::instance( $parent_fs )->is_permission( $permission_id, true );
18203 }
18204
18205 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( $permissions );
18206
18207 /**
18208 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
18209 * already returns the data for the current blog.
18210 *
18211 * @author Leo Fajardo (@leorw)
18212 * @since 2.3.0
18213 */
18214 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
18215 array() :
18216 array( 'uid' => $this->get_anonymous_id() );
18217
18218 $params = $this->get_install_data_for_api(
18219 $uid_param_to_override,
18220 false,
18221 false,
18222 /**
18223 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
18224 * already includes the data for it.
18225 *
18226 * @author Leo Fajardo (@leorw)
18227 * @since 2.3.0
18228 */
18229 ( true !== $network_level_or_blog_id )
18230 );
18231
18232 if ( true === $network_level_or_blog_id ) {
18233 $params['sites'] = $this->get_sites_for_network_level_optin();
18234
18235 if ( empty( $params['sites'] ) ) {
18236 return;
18237 }
18238 }
18239
18240 if ( is_object( $bundle_license ) ) {
18241 $params['license_key'] = $bundle_license->secret_key;
18242 }
18243
18244 // Activate add-on with parent plugin credentials.
18245 $result = $parent_fs->get_api_site_scope()->call(
18246 "/addons/{$this->_plugin->id}/installs.json",
18247 'post',
18248 $params
18249 );
18250
18251 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18252 if ( is_object( $bundle_license ) ) {
18253 /**
18254 * 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.
18255 *
18256 * @author Leo Fajardo (@leorw)
18257 * @since 2.4.0
18258 */
18259 } else {
18260 $error_message = FS_Api::is_api_error_object( $result ) ?
18261 $result->error->message :
18262 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
18263
18264 $this->_admin_notices->add(
18265 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18266 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
18267 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18268 'error'
18269 );
18270 }
18271
18272 return;
18273 }
18274
18275 $addon_installs = $result->installs;
18276 foreach ( $addon_installs as $key => $addon_install ) {
18277 $addon_installs[ $key ] = new FS_Site( $addon_install );
18278 }
18279
18280 $first_install = $addon_installs[0];
18281
18282 // Get user information based on parent's plugin.
18283 $user = $parent_fs->get_user();
18284
18285 // First of all, set site and user info - otherwise we won't
18286 // be able to invoke API calls.
18287 $this->_site = $first_install;
18288 $this->_user = $user;
18289
18290 // Sync add-on plans.
18291 $this->_sync_plans();
18292
18293 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
18294
18295 // Get site's current plan.
18296 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
18297
18298 // Sync licenses.
18299 $this->_sync_licenses();
18300
18301 if ( ! fs_is_network_admin() ) {
18302 // Try to activate premium license.
18303 $this->_activate_license( true, $bundle_license );
18304
18305 if ( is_object( $bundle_license ) ) {
18306 $this->maybe_activate_bundle_license( $bundle_license );
18307 }
18308 } else {
18309 if ( is_object( $bundle_license ) ) {
18310 $premium_license = $bundle_license;
18311 } else {
18312 $license_id = fs_request_get( 'license_id' );
18313
18314 if ( is_object( $this->_site ) &&
18315 FS_Plugin_License::is_valid_id( $license_id ) &&
18316 $license_id == $this->_site->license_id
18317 ) {
18318 // License is already activated.
18319 return;
18320 }
18321
18322 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
18323 $this->_get_license_by_id( $license_id ) :
18324 $this->_get_available_premium_license();
18325 }
18326
18327 if ( is_object( $premium_license ) ) {
18328 $this->maybe_network_activate_addon_license( $premium_license );
18329 }
18330 }
18331 }
18332
18333 /**
18334 * @author Leo Fajardo (@leorw)
18335 * @since 2.3.0
18336 *
18337 * @param FS_Site[] $installs
18338 * @param bool $is_site_level
18339 */
18340 private function handle_account_connection( $installs, $is_site_level ) {
18341 $first_install = $installs[0];
18342
18343 if ( $is_site_level ) {
18344 $this->_set_account( $this->_user, $first_install );
18345
18346 $this->do_action( 'after_account_connection', $this->_user, $first_install );
18347 } else {
18348 $this->_store_user();
18349
18350 // Map site addresses to their blog IDs.
18351 $address_to_blog_map = $this->get_address_to_blog_map();
18352
18353 $first_blog_id = null;
18354 $blog_2_install_map = array();
18355 foreach ( $installs as $install ) {
18356 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
18357 $blog_id = $address_to_blog_map[ $address ];
18358
18359 $this->_store_site( true, $blog_id, $install );
18360
18361 if ( is_null( $first_blog_id ) ) {
18362 $first_blog_id = $blog_id;
18363 }
18364
18365 $blog_2_install_map[ $blog_id ] = $install;
18366 }
18367
18368 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
18369 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
18370 ) {
18371 // Store network user.
18372 $this->_storage->network_user_id = $this->_user->id;
18373 }
18374
18375 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
18376 $this->_storage->network_install_blog_id = $first_blog_id;
18377 }
18378
18379 if ( count( $installs ) === count( $address_to_blog_map ) ) {
18380 // Super admin opted in for all sites in the network.
18381 $this->_storage->is_network_connected = true;
18382 }
18383
18384 $this->_store_licenses( false );
18385
18386 self::$_accounts->store();
18387
18388 // Don't sync the installs data on network upgrade
18389 if ( ! $this->network_upgrade_mode_completed() ) {
18390 $this->send_installs_update();
18391 }
18392
18393 $current_blog = get_current_blog_id();
18394
18395 foreach ( $blog_2_install_map as $blog_id => $install ) {
18396 $this->switch_to_blog( $blog_id );
18397
18398 $this->do_action( 'after_account_connection', $this->_user, $install );
18399 }
18400
18401 // Switch install context back to the first install.
18402 $this->switch_to_blog(
18403 $current_blog,
18404 $first_install,
18405 ( $this->_site->id != $first_install->id )
18406 );
18407
18408 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
18409 }
18410 }
18411
18412 /**
18413 * Tries to activate parent account based on add-on's info.
18414 *
18415 * @author Vova Feldman (@svovaf)
18416 * @since 1.2.2.7
18417 *
18418 * @param Freemius $parent_fs
18419 */
18420 private function activate_parent_account( Freemius $parent_fs ) {
18421 if ( ! $this->is_addon() ) {
18422 // This is not an add-on.
18423 return;
18424 }
18425
18426 if ( $parent_fs->is_registered() ) {
18427 // Already activated.
18428 return;
18429 }
18430
18431 // Activate parent with add-on's user credentials.
18432 $parent_install = $this->get_api_user_scope()->call(
18433 "/plugins/{$parent_fs->_plugin->id}/installs.json",
18434 'post',
18435 $parent_fs->get_install_data_for_api( array(
18436 'uid' => $parent_fs->get_anonymous_id(),
18437 ), false, false )
18438 );
18439
18440 if ( isset( $parent_install->error ) ) {
18441 $this->_admin_notices->add(
18442 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18443 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
18444 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18445 'error'
18446 );
18447
18448 return;
18449 }
18450
18451 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
18452
18453 if ( $parent_fs->is_pending_activation() ) {
18454 $parent_fs->clear_pending_activation_mode();
18455 }
18456
18457 // Get user information based on parent's plugin.
18458 $user = $this->get_user();
18459
18460 // First of all, set site info - otherwise we won't
18461 // be able to invoke API calls.
18462 $parent_fs->_site = new FS_Site( $parent_install );
18463 $parent_fs->_user = $user;
18464
18465 // Sync add-on plans.
18466 $parent_fs->_sync_plans();
18467
18468 $parent_fs->update_license_required_permissions_if_anonymous();
18469
18470 $parent_fs->_set_account( $user, $parent_fs->_site );
18471 }
18472
18473 #endregion
18474
18475 #----------------------------------------------------------------------------------
18476 #region Admin Menu Items
18477 #----------------------------------------------------------------------------------
18478
18479 private $_menu_items = array();
18480
18481 /**
18482 * @author Vova Feldman (@svovaf)
18483 * @since 1.2.1.8
18484 *
18485 * @return array
18486 */
18487 function get_menu_items() {
18488 return $this->_menu_items;
18489 }
18490
18491 /**
18492 * @author Vova Feldman (@svovaf)
18493 * @since 1.0.7
18494 *
18495 * @return string
18496 */
18497 function get_menu_slug() {
18498 return $this->_menu->get_slug();
18499 }
18500
18501 /**
18502 * @author Vova Feldman (@svovaf)
18503 * @since 1.0.9
18504 */
18505 function _prepare_admin_menu() {
18506 // if ( ! $this->is_on() ) {
18507 // return;
18508 // }
18509
18510 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
18511 return;
18512 }
18513
18514 /**
18515 * When running from a site admin with a network activated module and the connection
18516 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
18517 * site level settings.
18518 *
18519 * @author Vova Feldman (@svovaf)
18520 * @since 2.0.0
18521 */
18522 $should_hide_site_admin_settings = (
18523 $this->_is_network_active &&
18524 ! fs_is_network_admin() &&
18525 ! $this->is_delegated_connection() &&
18526 ! $this->is_anonymous() &&
18527 ! $this->is_registered()
18528 );
18529
18530 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
18531
18532 if ( ( false === $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
18533 $should_hide_site_admin_settings
18534 ) {
18535 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
18536 } else {
18537 $this->do_action( fs_is_network_admin() ?
18538 'before_network_admin_menu_init' :
18539 'before_admin_menu_init'
18540 );
18541
18542 $this->add_menu_action();
18543
18544 $this->add_network_menu_when_missing();
18545
18546 $this->add_submenu_items();
18547 }
18548 }
18549
18550 /**
18551 * Admin dashboard menu items modifications.
18552 *
18553 * NOTE: admin_menu action executed before admin_init.
18554 *
18555 * @author Vova Feldman (@svovaf)
18556 * @since 1.0.7
18557 *
18558 */
18559 private function add_menu_action() {
18560 if ( $this->is_activation_mode() ) {
18561 if ( $this->show_opt_in_on_setting_page() ) {
18562 $this->override_plugin_menu_with_activation();
18563 } else {
18564 /**
18565 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
18566 */
18567 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18568 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
18569 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
18570 fs_request_get_bool( 'pending_activation' )
18571 ) {
18572 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
18573 }
18574 }
18575 } else {
18576 if ( ! $this->is_registered() ) {
18577 // If not registered try to install user.
18578 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18579 $this->_install_with_new_user();
18580 }
18581 } else if (
18582 fs_request_is_action( 'sync_user' ) &&
18583 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
18584 ) {
18585 $this->_handle_account_user_sync();
18586 }
18587 }
18588 }
18589
18590 /**
18591 * @author Vova Feldman (@svovaf)
18592 * @since 1.0.1
18593 */
18594 function _redirect_on_clicked_menu_link() {
18595 $this->_logger->entrance();
18596
18597 $page = fs_request_get('page');
18598 $page = is_string($page) ? strtolower($page) : '';
18599
18600 $this->_logger->log( 'page = ' . $page );
18601
18602 foreach ( $this->_menu_items as $priority => $items ) {
18603 foreach ( $items as $item ) {
18604 if ( isset( $item['url'] ) ) {
18605 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
18606 $this->_logger->log( 'Redirecting to ' . $item['url'] );
18607
18608 fs_redirect( $item['url'] );
18609 }
18610 }
18611 }
18612 }
18613 }
18614
18615 /**
18616 * Remove plugin's all admin menu items & pages, and replace with activation page.
18617 *
18618 * @author Vova Feldman (@svovaf)
18619 * @since 1.0.1
18620 */
18621 private function override_plugin_menu_with_activation() {
18622 $this->_logger->entrance();
18623
18624 $hook = false;
18625
18626 if ( ! $this->has_settings_menu() ) {
18627 // Add the opt-in page without a menu item.
18628 $hook = FS_Admin_Menu_Manager::add_subpage(
18629 '',
18630 $this->get_plugin_name(),
18631 $this->get_plugin_name(),
18632 'manage_options',
18633 $this->_slug,
18634 array( &$this, '_connect_page_render' )
18635 );
18636 } else if ( $this->_menu->is_top_level() ) {
18637 if ( $this->_menu->is_override_exact() ) {
18638 // Make sure the current page is matching the activation page.
18639 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18640 return;
18641 }
18642 }
18643
18644 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18645
18646 if ( false === $hook ) {
18647 // Create new menu item just for the opt-in.
18648 $hook = FS_Admin_Menu_Manager::add_page(
18649 $this->get_plugin_name(),
18650 $this->get_plugin_name(),
18651 'manage_options',
18652 $this->_menu->get_slug(),
18653 array( &$this, '_connect_page_render' )
18654 );
18655 }
18656 } else {
18657 $menus = array( $this->_menu->get_parent_slug() );
18658
18659 if ( $this->_menu->is_override_exact() ) {
18660 // Make sure the current page is matching the activation page.
18661 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18662 return;
18663 }
18664 }
18665
18666 foreach ( $menus as $parent_slug ) {
18667 $hook = $this->_menu->override_submenu_action(
18668 $parent_slug,
18669 $this->_menu->get_raw_slug(),
18670 array( &$this, '_connect_page_render' )
18671 );
18672
18673 if ( false !== $hook ) {
18674 // Found plugin's submenu item.
18675 break;
18676 }
18677 }
18678 }
18679
18680 if ( $this->is_activation_page() ) {
18681 // Clean admin page from distracting content.
18682 self::_clean_admin_content_section();
18683 }
18684
18685 if ( false !== $hook ) {
18686 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18687 $this->_install_with_current_user();
18688 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18689 $this->_install_with_new_user();
18690 }
18691 }
18692 }
18693
18694 /**
18695 * If a plugin was network activated and connected but don't have a network
18696 * level settings, then add an artificial menu item for the Account and other
18697 * Freemius settings.
18698 *
18699 * @author Vova Feldman (@svovaf)
18700 * @since 2.0.0
18701 */
18702 private function add_network_menu_when_missing() {
18703 $this->_logger->entrance();
18704
18705 if ( ! $this->_is_network_active ) {
18706 // Plugin wasn't activated on the network level.
18707 return;
18708 }
18709
18710 if ( ! fs_is_network_admin() ) {
18711 // The context is not the network admin.
18712 return;
18713 }
18714
18715 if ( $this->_menu->has_network_menu() ) {
18716 // Plugin already has a network level menu.
18717 return;
18718 }
18719
18720 if ( $this->is_network_activation_mode() ) {
18721 /**
18722 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
18723 * screen is being shown.
18724 *
18725 * @author Leo Fajardo (@leorw)
18726 */
18727 return;
18728 }
18729
18730 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
18731 if ( $this->is_network_delegated_connection() ) {
18732 // Super-admin delegated the connection to the site admins.
18733 return;
18734 }
18735 }
18736
18737 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
18738
18739 if ( $this->_menu->has_menu() ||
18740 ! $this->is_addon() ||
18741 $this->is_activation_mode()
18742 ) {
18743 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
18744 $this->get_plugin_name(),
18745 $this->get_plugin_name(),
18746 'manage_options',
18747 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
18748 );
18749 }
18750 } else {
18751 $this->_menu->add_subpage_and_update(
18752 $this->_menu->get_parent_slug(),
18753 $this->get_plugin_name(),
18754 $this->get_plugin_name(),
18755 'manage_options',
18756 $this->_menu->get_slug()
18757 );
18758 }
18759 }
18760
18761 /**
18762 * @author Leo Fajardo (@leorw)
18763 * @since 1.2.1
18764 *
18765 * return string
18766 */
18767 function get_top_level_menu_capability() {
18768 global $menu;
18769
18770 $top_level_menu_slug = $this->get_top_level_menu_slug();
18771
18772 foreach ( $menu as $menu_info ) {
18773 /**
18774 * The second element in the menu info array is the capability/role that has access to the menu and the
18775 * third element is the menu slug.
18776 */
18777 if ( $menu_info[2] === $top_level_menu_slug ) {
18778 return $menu_info[1];
18779 }
18780 }
18781
18782 return 'read';
18783 }
18784
18785 /**
18786 * @author Vova Feldman (@svovaf)
18787 * @since 1.0.0
18788 *
18789 * @return string
18790 */
18791 private function get_top_level_menu_slug() {
18792 return ( $this->is_addon() ?
18793 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
18794 $this->_menu->get_top_level_menu_slug() );
18795 }
18796
18797 /**
18798 * @author Vova Feldman (@svovaf)
18799 * @since 1.2.2.7
18800 *
18801 * @return string
18802 */
18803 function get_pricing_cta_label() {
18804 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
18805
18806 if ( $this->is_in_trial_promotion() &&
18807 ! $this->is_paying_or_trial()
18808 ) {
18809 // If running a trial promotion, modify the pricing to load the trial.
18810 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
18811 } else if ( $this->is_paying() ) {
18812 $label = $this->get_text_inline( 'Pricing', 'pricing' );
18813 }
18814
18815 return $label;
18816 }
18817
18818 /**
18819 * @author Vova Feldman (@svovaf)
18820 * @since 1.2.2.7
18821 *
18822 * @return bool
18823 */
18824 function is_pricing_page_visible() {
18825 $visible = (
18826 // Has at least one paid plan.
18827 $this->has_paid_plan() &&
18828 // Didn't ask to hide the pricing page.
18829 $this->is_page_visible( 'pricing' ) &&
18830 // Don't have a valid active license or has more than one plan.
18831 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
18832 );
18833
18834 return $this->apply_filters( 'is_pricing_page_visible', $visible );
18835 }
18836
18837 /**
18838 * @author Leo Fajardo (@leorw)
18839 * @since 2.3.0
18840 *
18841 * @param bool $is_activation_mode
18842 *
18843 * @return bool
18844 */
18845 private function should_add_submenu_or_action_links( $is_activation_mode ) {
18846 if ( $this->is_addon() ) {
18847 // No submenu items or action links for add-ons.
18848 return false;
18849 }
18850
18851 if ( $this->show_opt_in_on_themes_page() ) {
18852 if ( ! fs_is_network_admin() ) {
18853 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
18854 return true;
18855 }
18856 } else if ( $is_activation_mode ) {
18857 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
18858 return false;
18859 }
18860
18861 if ( fs_is_network_admin() ) {
18862 /**
18863 * Add submenu items or action links to network level when plugin was network activated and the super
18864 * admin did NOT delegate the connection of all sites to site admins.
18865 */
18866 return (
18867 $this->_is_network_active &&
18868 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
18869 ! $this->is_network_delegated_connection() )
18870 );
18871 }
18872
18873 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
18874 }
18875
18876 /**
18877 * Add default Freemius menu items.
18878 *
18879 * @author Vova Feldman (@svovaf)
18880 * @since 1.0.0
18881 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
18882 */
18883 private function add_submenu_items() {
18884 $this->_logger->entrance();
18885
18886 $is_activation_mode = $this->is_activation_mode();
18887
18888 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
18889
18890 if ( $add_submenu_items ) {
18891 if ( $this->has_affiliate_program() ) {
18892 // Add affiliation page.
18893 $this->add_submenu_item(
18894 $this->get_text_inline( 'Affiliation', 'affiliation' ),
18895 array( &$this, '_affiliation_page_render' ),
18896 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
18897 'manage_options',
18898 'affiliation',
18899 'Freemius::_clean_admin_content_section',
18900 WP_FS__DEFAULT_PRIORITY,
18901 $this->is_submenu_item_visible( 'affiliation' )
18902 );
18903 }
18904 }
18905
18906 if ( $add_submenu_items ||
18907 ( $is_activation_mode &&
18908 $this->is_only_premium() &&
18909 $this->is_admin_page( 'account' ) &&
18910 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
18911 )
18912 ) {
18913 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
18914 $show_account = (
18915 $this->is_submenu_item_visible( 'account' ) &&
18916 /**
18917 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
18918 */
18919 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
18920 );
18921
18922 // Add user account page.
18923 $this->add_submenu_item(
18924 $this->get_text_inline( 'Account', 'account' ),
18925 array( &$this, '_account_page_render' ),
18926 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
18927 'manage_options',
18928 'account',
18929 array( &$this, '_account_page_load' ),
18930 WP_FS__DEFAULT_PRIORITY,
18931 ( $add_submenu_items && $show_account )
18932 );
18933 }
18934 }
18935
18936 if ( $add_submenu_items ) {
18937 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18938 // Add contact page.
18939 if ( $this->is_premium() ) {
18940 $this->add_submenu_item(
18941 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18942 array( &$this, '_contact_page_render' ),
18943 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
18944 'manage_options',
18945 'contact',
18946 'Freemius::_clean_admin_content_section',
18947 WP_FS__DEFAULT_PRIORITY,
18948 $this->is_submenu_item_visible( 'contact' )
18949 );
18950 } else {
18951 $this->add_submenu_link_item(
18952 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18953 FS_Contact_Form_Manager::instance()->get_standalone_link( $this ),
18954 'contact',
18955 'manage_options',
18956 WP_FS__DEFAULT_PRIORITY,
18957 $this->is_submenu_item_visible( 'contact' ),
18958 'fs_external_contact',
18959 true
18960 );
18961 }
18962 }
18963
18964 if ( $this->has_addons() ) {
18965 $this->add_submenu_item(
18966 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18967 array( &$this, '_addons_page_render' ),
18968 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18969 'manage_options',
18970 'addons',
18971 array( &$this, '_addons_page_load' ),
18972 WP_FS__LOWEST_PRIORITY - 1,
18973 $this->is_submenu_item_visible( 'addons' )
18974 );
18975 }
18976 }
18977
18978 if ( $add_submenu_items ||
18979 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
18980 ) {
18981 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18982 $show_pricing = (
18983 $this->is_submenu_item_visible( 'pricing' ) &&
18984 $this->is_pricing_page_visible()
18985 );
18986
18987 $pricing_cta_text = $this->get_pricing_cta_label();
18988 $pricing_class = 'upgrade-mode';
18989 if ( $show_pricing ) {
18990 if ( $this->is_in_trial_promotion() &&
18991 ! $this->is_paying_or_trial()
18992 ) {
18993 // If running a trial promotion, modify the pricing to load the trial.
18994 $pricing_class = 'trial-mode';
18995 } else if ( $this->is_paying() ) {
18996 $pricing_class = '';
18997 }
18998 }
18999
19000 $custom_pricing_url = $this->get_pricing_url_with_filter( null );
19001 $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' ) );
19002 $show_pricing_submenu_item = ( $add_submenu_items && $show_pricing );
19003
19004 // Add upgrade/pricing submenu item.
19005 if ( ! is_null( $custom_pricing_url ) ) {
19006 $this->add_submenu_link_item(
19007 $pricing_menu_title,
19008 $custom_pricing_url,
19009 'pricing',
19010 'manage_options',
19011 WP_FS__LOWEST_PRIORITY,
19012 $show_pricing_submenu_item,
19013 $pricing_class
19014 );
19015 } else {
19016 $this->add_submenu_item(
19017 $pricing_menu_title,
19018 array( &$this, '_pricing_page_render' ),
19019 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
19020 'manage_options',
19021 'pricing',
19022 'Freemius::_clean_admin_content_section',
19023 WP_FS__LOWEST_PRIORITY,
19024 $show_pricing_submenu_item,
19025 $pricing_class
19026 );
19027 }
19028 }
19029 }
19030
19031 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
19032 /**
19033 * Add the other menu items if there are any when not in activation mode or license activation is not
19034 * required (license activation is required for registered or anonymous users after activating the
19035 * premium version when the site is not in trial mode or there's no active valid license).
19036 *
19037 * @author Leo Fajardo (@leorw)
19038 * @since 2.2.1
19039 */
19040 if ( 0 < count( $this->_menu_items ) ) {
19041 if ( ! $this->_menu->is_top_level() ) {
19042 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
19043
19044 // Append submenu items right after the plugin's submenu item.
19045 $this->order_sub_submenu_items();
19046 } else {
19047 // Append submenu items.
19048 $this->embed_submenu_items();
19049 }
19050 }
19051 }
19052 }
19053
19054 /**
19055 * Moved the actual submenu item additions to a separated function,
19056 * in order to support sub-submenu items when the plugin's settings
19057 * only have a submenu and not top-level menu item.
19058 *
19059 * @author Vova Feldman (@svovaf)
19060 * @since 1.1.4
19061 */
19062 private function embed_submenu_items() {
19063 $item_classes = $this->_menu->is_top_level() ? 'fs-submenu-item' : 'fs-submenu-item fs-sub';
19064
19065 $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>';
19066
19067 $top_level_menu_capability = $this->get_top_level_menu_capability();
19068
19069 ksort( $this->_menu_items );
19070
19071 $is_first_submenu_item = true;
19072
19073 foreach ( $this->_menu_items as $priority => $items ) {
19074 foreach ( $items as $item ) {
19075 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
19076
19077 $menu_item = sprintf(
19078 $item_template,
19079 $this->get_unique_affix(),
19080 $item['menu_slug'],
19081 ! empty( $item['class'] ) ? $item['class'] : '',
19082 $item['menu_title'],
19083 esc_attr( isset( $item['url'] ) ? $item['url'] : '' ),
19084 esc_attr( isset( $item['new_tab'] ) ? 'true' : 'false' )
19085 );
19086
19087 $top_level_menu_slug = $this->get_top_level_menu_slug();
19088 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
19089
19090 if ( ! isset( $item['url'] ) ) {
19091 $hook = FS_Admin_Menu_Manager::add_subpage(
19092 $item['show_submenu'] ?
19093 $top_level_menu_slug :
19094 '',
19095 $item['page_title'],
19096 $menu_item,
19097 $capability,
19098 $menu_slug,
19099 $item['render_function']
19100 );
19101
19102 if ( false !== $item['before_render_function'] ) {
19103 add_action( "load-$hook", $item['before_render_function'] );
19104 }
19105 } else {
19106 FS_Admin_Menu_Manager::add_subpage(
19107 $item['show_submenu'] ?
19108 $top_level_menu_slug :
19109 '',
19110 $item['page_title'],
19111 $menu_item,
19112 $capability,
19113 $menu_slug,
19114 array( $this, '' )
19115 );
19116 }
19117
19118 if ( $item['show_submenu'] && $is_first_submenu_item ) {
19119 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
19120 /**
19121 * If the top-level menu has been dynamically created, remove the first submenu item that
19122 * WordPress automatically creates when there's no submenu item whose slug matches the
19123 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
19124 *
19125 * Awesome Plugin
19126 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
19127 *
19128 * @author Leo Fajardo (@leorw)
19129 */
19130 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
19131 }
19132
19133 $is_first_submenu_item = false;
19134 }
19135 }
19136 }
19137 }
19138
19139 /**
19140 * Re-order the submenu items so all Freemius added new submenu items
19141 * are added right after the plugin's settings submenu item.
19142 *
19143 * @author Vova Feldman (@svovaf)
19144 * @since 1.1.4
19145 */
19146 private function order_sub_submenu_items() {
19147 global $submenu;
19148
19149 $menu_slug = $this->_menu->get_top_level_menu_slug();
19150
19151 /**
19152 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
19153 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
19154 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
19155 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
19156 *
19157 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
19158 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
19159 * capability needed to access the parent menu as the capability for the submenus that we will add.
19160 */
19161 if ( empty( $submenu[ $menu_slug ] ) ) {
19162 return;
19163 }
19164
19165 $top_level_menu = &$submenu[ $menu_slug ];
19166
19167 $all_submenu_items_after = array();
19168
19169 $found_submenu_item = false;
19170
19171 foreach ( $top_level_menu as $submenu_id => $meta ) {
19172 if ( $found_submenu_item ) {
19173 // Remove all submenu items after the plugin's submenu item.
19174 $all_submenu_items_after[] = $meta;
19175 unset( $top_level_menu[ $submenu_id ] );
19176 }
19177
19178 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
19179 // Found the submenu item, put all below.
19180 $found_submenu_item = true;
19181 continue;
19182 }
19183 }
19184
19185 // Embed all plugin's new submenu items.
19186 $this->embed_submenu_items();
19187
19188 // Start with specially high number to make sure it's appended.
19189 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
19190 foreach ( $all_submenu_items_after as $meta ) {
19191 $top_level_menu[ $i ] = $meta;
19192 $i ++;
19193 }
19194
19195 // Sort submenu items.
19196 ksort( $top_level_menu );
19197 }
19198
19199 /**
19200 * Helper method to return the module's support forum URL.
19201 *
19202 * @author Vova Feldman (@svovaf)
19203 * @since 1.2.2.7
19204 *
19205 * @return string
19206 */
19207 function get_support_forum_url() {
19208 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
19209 }
19210
19211 /**
19212 * Displays the Support Forum link when enabled.
19213 *
19214 * Can be filtered like so:
19215 *
19216 * function _fs_show_support_menu( $is_visible, $menu_id ) {
19217 * if ( 'support' === $menu_id ) {
19218 * return _fs->is_registered();
19219 * }
19220 * return $is_visible;
19221 * }
19222 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
19223 *
19224 */
19225 function _add_default_submenu_items() {
19226 if ( ! $this->is_on() ) {
19227 return;
19228 }
19229
19230 if ( ! $this->is_activation_mode() &&
19231 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
19232 ( ! $this->_is_network_active && is_admin() ) )
19233 ) {
19234 $this->add_submenu_link_item(
19235 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
19236 $this->get_support_forum_url(),
19237 'wp-support-forum',
19238 null,
19239 50,
19240 $this->is_submenu_item_visible( 'support' ),
19241 '',
19242 true
19243 );
19244 }
19245 }
19246
19247 /**
19248 * @author Vova Feldman (@svovaf)
19249 * @since 1.0.1
19250 *
19251 * @param string $menu_title
19252 * @param callable $render_function
19253 * @param bool|string $page_title
19254 * @param string $capability
19255 * @param bool|string $menu_slug
19256 * @param bool|callable $before_render_function
19257 * @param int $priority
19258 * @param bool $show_submenu
19259 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
19260 */
19261 function add_submenu_item(
19262 $menu_title,
19263 $render_function,
19264 $page_title = false,
19265 $capability = 'manage_options',
19266 $menu_slug = false,
19267 $before_render_function = false,
19268 $priority = WP_FS__DEFAULT_PRIORITY,
19269 $show_submenu = true,
19270 $class = ''
19271 ) {
19272 $this->_logger->entrance( 'Title = ' . $menu_title );
19273
19274 if ( $this->is_addon() ) {
19275 $parent_fs = $this->get_parent_instance();
19276
19277 if ( is_object( $parent_fs ) ) {
19278 $parent_fs->add_submenu_item(
19279 $menu_title,
19280 $render_function,
19281 $page_title,
19282 $capability,
19283 $menu_slug,
19284 $before_render_function,
19285 $priority,
19286 $show_submenu,
19287 $class
19288 );
19289
19290 return;
19291 }
19292 }
19293
19294 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19295 $this->_menu_items[ $priority ] = array();
19296 }
19297
19298 $this->_menu_items[ $priority ][] = array(
19299 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
19300 'menu_title' => $menu_title,
19301 'capability' => $capability,
19302 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19303 'render_function' => $render_function,
19304 'before_render_function' => $before_render_function,
19305 'show_submenu' => $show_submenu,
19306 'class' => $class,
19307 );
19308 }
19309
19310 /**
19311 * @author Vova Feldman (@svovaf)
19312 * @since 1.0.1
19313 *
19314 * @param string $menu_title
19315 * @param string $url
19316 * @param bool $menu_slug
19317 * @param string $capability
19318 * @param int $priority
19319 * @param bool $show_submenu
19320 * @param string $class
19321 * @param bool $new_tab
19322 */
19323 function add_submenu_link_item(
19324 $menu_title,
19325 $url,
19326 $menu_slug = false,
19327 $capability = 'read',
19328 $priority = WP_FS__DEFAULT_PRIORITY,
19329 $show_submenu = true,
19330 $class = '',
19331 $new_tab = false
19332 ) {
19333 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
19334
19335 if ( $this->is_addon() ) {
19336 $parent_fs = $this->get_parent_instance();
19337
19338 if ( is_object( $parent_fs ) ) {
19339 $parent_fs->add_submenu_link_item(
19340 $menu_title,
19341 $url,
19342 $menu_slug,
19343 $capability,
19344 $priority,
19345 $show_submenu,
19346 $class,
19347 $new_tab
19348 );
19349
19350 return;
19351 }
19352 }
19353
19354 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19355 $this->_menu_items[ $priority ] = array();
19356 }
19357
19358 $this->_menu_items[ $priority ][] = array(
19359 'menu_title' => $menu_title,
19360 'capability' => $capability,
19361 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19362 'url' => $url,
19363 'page_title' => $menu_title,
19364 'render_function' => 'fs_dummy',
19365 'before_render_function' => '',
19366 'show_submenu' => $show_submenu,
19367 'class' => $class,
19368 'new_tab' => $new_tab,
19369 );
19370 }
19371
19372 #endregion ------------------------------------------------------------------
19373
19374 #--------------------------------------------------------------------------------
19375 #region Admin Notices
19376 #--------------------------------------------------------------------------------
19377
19378 /**
19379 * @author Vova Feldman (@svovaf)
19380 * @since 2.3.1
19381 *
19382 * @param string|string[] $ids
19383 * @param int|null $network_level_or_blog_id
19384 *
19385 * @uses FS_Admin_Notices::remove_sticky()
19386 */
19387 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
19388 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
19389 }
19390
19391 #endregion
19392
19393 #--------------------------------------------------------------------------------
19394 #region Actions / Hooks / Filters
19395 #--------------------------------------------------------------------------------
19396
19397 /**
19398 * @author Vova Feldman (@svovaf)
19399 * @since 1.1.7
19400 *
19401 * @param string $tag
19402 *
19403 * @return string
19404 */
19405 public function get_action_tag( $tag ) {
19406 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
19407 }
19408
19409 /**
19410 * @author Vova Feldman (@svovaf)
19411 * @since 1.2.1.6
19412 *
19413 * @param string $tag
19414 * @param string $slug
19415 * @param bool $is_plugin
19416 *
19417 * @return string
19418 */
19419 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
19420 $action = "fs_{$tag}";
19421
19422 if ( ! empty( $slug ) ) {
19423 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
19424 }
19425
19426 return $action;
19427 }
19428
19429 /**
19430 * Returns a string that can be used to generate a unique action name,
19431 * option name, HTML element ID, or HTML element class.
19432 *
19433 * @author Leo Fajardo (@leorw)
19434 * @since 1.2.2
19435 *
19436 * @return string
19437 */
19438 public function get_unique_affix() {
19439 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
19440 }
19441
19442 /**
19443 * Returns a string that can be used to generate a unique action name,
19444 * option name, HTML element ID, or HTML element class.
19445 *
19446 * @author Vova Feldman (@svovaf)
19447 * @since 1.2.2.5
19448 *
19449 * @param string $slug
19450 * @param bool $is_plugin
19451 *
19452 * @return string
19453 */
19454 static function get_module_unique_affix( $slug, $is_plugin = true ) {
19455 $affix = $slug;
19456
19457 if ( ! $is_plugin ) {
19458 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
19459 }
19460
19461 return $affix;
19462 }
19463
19464 /**
19465 * @author Vova Feldman (@svovaf)
19466 * @since 1.2.1
19467 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19468 * based on the slug for backward compatibility.
19469 *
19470 * @param string $tag
19471 *
19472 * @return string
19473 */
19474 function get_ajax_action( $tag ) {
19475 return self::get_ajax_action_static( $tag, $this->_module_id );
19476 }
19477
19478 /**
19479 * @author Vova Feldman (@svovaf)
19480 * @since 1.2.1.7
19481 *
19482 * @param string $tag
19483 *
19484 * @return string
19485 */
19486 function get_ajax_security( $tag ) {
19487 return wp_create_nonce( $this->get_ajax_action( $tag ) );
19488 }
19489
19490 /**
19491 * @author Vova Feldman (@svovaf)
19492 * @since 1.2.1.7
19493 *
19494 * @param string $tag
19495 */
19496 function check_ajax_referer( $tag ) {
19497 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
19498 }
19499
19500 /**
19501 * @author Vova Feldman (@svovaf)
19502 * @since 1.2.1.6
19503 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19504 * based on the slug for backward compatibility.
19505 *
19506 * @param string $tag
19507 * @param number|null $module_id
19508 *
19509 * @return string
19510 */
19511 static function get_ajax_action_static( $tag, $module_id = null ) {
19512 $action = "fs_{$tag}";
19513
19514 if ( ! empty( $module_id ) ) {
19515 $action .= "_{$module_id}";
19516 }
19517
19518 return $action;
19519 }
19520
19521 /**
19522 * Do action, specific for the current context plugin.
19523 *
19524 * @author Vova Feldman (@svovaf)
19525 * @since 1.0.1
19526 *
19527 * @param string $tag The name of the action to be executed.
19528 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
19529 * functions hooked to the action. Default empty.
19530 *
19531 * @uses do_action()
19532 */
19533 function do_action( $tag, $arg = '' ) {
19534 $args = func_get_args();
19535
19536 $this->_logger->entrance( $tag );
19537
19538 call_user_func_array( 'do_action', array_merge(
19539 array( $this->get_action_tag( $tag ) ),
19540 array_slice( $args, 1 ) )
19541 );
19542 }
19543
19544 /**
19545 * Add action, specific for the current context plugin.
19546 *
19547 * @author Vova Feldman (@svovaf)
19548 * @since 1.0.1
19549 *
19550 * @param string $tag
19551 * @param callable $function_to_add
19552 * @param int $priority
19553 * @param int $accepted_args
19554 *
19555 * @uses add_action()
19556 */
19557 function add_action(
19558 $tag,
19559 $function_to_add,
19560 $priority = WP_FS__DEFAULT_PRIORITY,
19561 $accepted_args = 1
19562 ) {
19563 $this->_logger->entrance( $tag );
19564
19565 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19566 }
19567
19568 /**
19569 * Add AJAX action, specific for the current context plugin.
19570 *
19571 * @author Vova Feldman (@svovaf)
19572 * @since 1.2.1
19573 *
19574 * @param string $tag
19575 * @param callable $function_to_add
19576 * @param int $priority
19577 *
19578 * @uses add_action()
19579 *
19580 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19581 */
19582 function add_ajax_action(
19583 $tag,
19584 $function_to_add,
19585 $priority = WP_FS__DEFAULT_PRIORITY
19586 ) {
19587 $this->_logger->entrance( $tag );
19588
19589 return self::add_ajax_action_static(
19590 $tag,
19591 $function_to_add,
19592 $priority,
19593 $this->_module_id
19594 );
19595 }
19596
19597 /**
19598 * Add AJAX action.
19599 *
19600 * @author Vova Feldman (@svovaf)
19601 * @since 1.2.1.6
19602 *
19603 * @param string $tag
19604 * @param callable $function_to_add
19605 * @param int $priority
19606 * @param number|null $module_id
19607 *
19608 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19609 * @uses add_action()
19610 *
19611 */
19612 static function add_ajax_action_static(
19613 $tag,
19614 $function_to_add,
19615 $priority = WP_FS__DEFAULT_PRIORITY,
19616 $module_id = null
19617 ) {
19618 self::$_static_logger->entrance( $tag );
19619
19620 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
19621 return false;
19622 }
19623
19624 add_action(
19625 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
19626 $function_to_add,
19627 $priority,
19628 0
19629 );
19630
19631 self::$_static_logger->info( "$tag AJAX callback action added." );
19632
19633 return true;
19634 }
19635
19636 /**
19637 * Send a JSON response back to an Ajax request.
19638 *
19639 * @author Vova Feldman (@svovaf)
19640 * @since 1.2.1.5
19641 *
19642 * @param mixed $response
19643 */
19644 static function shoot_ajax_response( $response ) {
19645 wp_send_json( $response );
19646 }
19647
19648 /**
19649 * Send a JSON response back to an Ajax request, indicating success.
19650 *
19651 * @author Vova Feldman (@svovaf)
19652 * @since 1.2.1.5
19653 *
19654 * @param mixed $data Data to encode as JSON, then print and exit.
19655 */
19656 static function shoot_ajax_success( $data = null ) {
19657 wp_send_json_success( $data );
19658 }
19659
19660 /**
19661 * Send a JSON response back to an Ajax request, indicating failure.
19662 *
19663 * @author Vova Feldman (@svovaf)
19664 * @since 1.2.1.5
19665 *
19666 * @param mixed $error Optional error message.
19667 */
19668 static function shoot_ajax_failure( $error = '' ) {
19669 $result = array( 'success' => false );
19670 if ( ! empty( $error ) ) {
19671 $result['error'] = $error;
19672 }
19673
19674 wp_send_json( $result );
19675 }
19676
19677 /**
19678 * Returns an AJAX URL with a special extra param to indicate whether the request was triggered from the network admin or blog admin.
19679 *
19680 * @author Vova Feldman (@svovaf)
19681 * @since 2.5.1
19682 *
19683 * @param string $wrap_with By default, returns the AJAX URL wrapped with single quotes.
19684 *
19685 * @return string
19686 */
19687 static function ajax_url( $wrap_with = "'") {
19688 if ( fs_is_network_admin() ) {
19689 $param_name = '_fs_network_admin';
19690 } else {
19691 $param_name = '_fs_blog_admin';
19692 }
19693
19694 $url = admin_url( 'admin-ajax.php', 'relative' );
19695 $url .= ( false === strpos( $url, '?' ) ) ? '?' : '&';
19696 $url .= "{$param_name}=true";
19697
19698 return "{$wrap_with}{$url}{$wrap_with}";
19699 }
19700
19701 /**
19702 * Apply filter, specific for the current context plugin.
19703 *
19704 * @author Vova Feldman (@svovaf)
19705 * @since 1.0.9
19706 *
19707 * @param string $tag The name of the filter hook.
19708 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
19709 *
19710 * @return mixed The filtered value after all hooked functions are applied to it.
19711 *
19712 * @uses apply_filters()
19713 */
19714 function apply_filters( $tag, $value ) {
19715 $args = func_get_args();
19716
19717 $this->_logger->entrance( $tag );
19718
19719 array_unshift( $args, $this->get_unique_affix() );
19720
19721 return call_user_func_array( 'fs_apply_filter', $args );
19722 }
19723
19724 /**
19725 * Add filter, specific for the current context plugin.
19726 *
19727 * @author Vova Feldman (@svovaf)
19728 * @since 1.0.9
19729 *
19730 * @param string $tag
19731 * @param callable $function_to_add
19732 * @param int $priority
19733 * @param int $accepted_args
19734 *
19735 * @uses add_filter()
19736 */
19737 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
19738 $this->_logger->entrance( $tag );
19739
19740 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19741 }
19742
19743 /**
19744 * Check if has filter.
19745 *
19746 * @author Vova Feldman (@svovaf)
19747 * @since 1.1.4
19748 *
19749 * @param string $tag
19750 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
19751 *
19752 * @return false|int
19753 *
19754 * @uses has_filter()
19755 */
19756 function has_filter( $tag, $function_to_check = false ) {
19757 $this->_logger->entrance( $tag );
19758
19759 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
19760 }
19761
19762 #endregion
19763
19764 /**
19765 * Override default i18n text phrases.
19766 *
19767 * @author Vova Feldman (@svovaf)
19768 * @since 1.1.6
19769 *
19770 * @param string[] string $key_value
19771 *
19772 * @uses fs_override_i18n()
19773 */
19774 function override_i18n( $key_value ) {
19775 fs_override_i18n( $key_value, $this->_slug );
19776 }
19777
19778 /* Account Page
19779 ------------------------------------------------------------------------------------------------------------------*/
19780 /**
19781 * Update site information.
19782 *
19783 * @author Vova Feldman (@svovaf)
19784 * @since 1.0.1
19785 *
19786 * @param bool $store Flush to Database if true.
19787 * @param null|int $network_level_or_blog_id Since 2.0.0
19788 * @param \FS_Site $site Since 2.0.0
19789 */
19790 private function _store_site( $store = true, $network_level_or_blog_id = null, $site = null, $is_backup = false ) {
19791 $this->_logger->entrance();
19792
19793 if ( is_null( $site ) ) {
19794 $site = $this->_site;
19795 }
19796
19797 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
19798 $this->_logger->error( "Empty install ID, can't store site." );
19799
19800 return;
19801 }
19802
19803 $site_clone = clone $site;
19804
19805 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id, $is_backup );
19806
19807 if (
19808 ! $is_backup &&
19809 is_object( $this->_user ) && $this->_user->id != $site->user_id
19810 ) {
19811 $this->sync_user_by_current_install( $site->user_id );
19812
19813 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
19814
19815 if ( empty( $prev_stored_user_id ) &&
19816 is_object($this->_user) && $this->_user->id != $site->user_id
19817 ) {
19818 /**
19819 * Store the current user ID as the previous user ID so that the previous user can be used
19820 * as the install's owner while the new owner's details are not yet available.
19821 *
19822 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
19823 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
19824 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
19825 * into `replica`.
19826 *
19827 * @author Leo Fajardo (@leorw)
19828 */
19829 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
19830 }
19831 }
19832
19833 $sites[ $this->_slug ] = $site_clone;
19834
19835 $this->set_account_option(
19836 ( $is_backup ? 'prev_' : '' ) . 'sites',
19837 $sites,
19838 $store,
19839 $network_level_or_blog_id
19840 );
19841 }
19842
19843 /**
19844 * 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).
19845 *
19846 * @author Leo Fajardo (@leorw)
19847 * @since 2.5.0
19848 */
19849 private function back_up_site() {
19850 $this->_logger->entrance();
19851
19852 $site_clone = clone $this->_site;
19853
19854 $this->_store_site( true, null, $site_clone, true );
19855 }
19856
19857 /**
19858 * Update plugin's plans information.
19859 *
19860 * @author Vova Feldman (@svovaf)
19861 * @since 1.0.2
19862 *
19863 * @param bool $store Flush to Database if true.
19864 */
19865 private function _store_plans( $store = true ) {
19866 $this->_logger->entrance();
19867
19868 $plans = self::get_all_plans( $this->_module_type );
19869
19870 // Copy plans.
19871 $encrypted_plans = array();
19872 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
19873 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
19874 }
19875
19876 $plans[ $this->_slug ] = $encrypted_plans;
19877
19878 $this->set_account_option( 'plans', $plans, $store );
19879 }
19880
19881 /**
19882 * Update user's plugin licenses.
19883 *
19884 * @author Vova Feldman (@svovaf)
19885 * @since 1.0.5
19886 *
19887 * @param bool $store
19888 * @param number|bool $module_id
19889 * @param FS_Plugin_License[] $licenses
19890 */
19891 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
19892 $this->_logger->entrance();
19893
19894 $all_licenses = self::get_all_licenses();
19895
19896 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
19897 $module_id = $this->_module_id;
19898
19899 $user_licenses = is_array( $this->_licenses ) ?
19900 $this->_licenses :
19901 array();
19902
19903 if ( empty( $user_licenses ) ) {
19904 // If the context user doesn't have any license, don't update the licenses collection.
19905 return;
19906 }
19907
19908 $new_user_licenses_map = array();
19909 foreach ( $user_licenses as $user_license ) {
19910 $new_user_licenses_map[ $user_license->id ] = $user_license;
19911 }
19912
19913 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
19914
19915 // Update user licenses.
19916 $licenses_to_update_count = count( $new_user_licenses_map );
19917 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
19918 if ( 0 === $licenses_to_update_count ) {
19919 break;
19920 }
19921
19922 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
19923 // Update license.
19924 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
19925 unset( $new_user_licenses_map[ $license->id ] );
19926
19927 $licenses_to_update_count --;
19928 }
19929 }
19930
19931 if ( ! empty( $new_user_licenses_map ) ) {
19932 // Add new licenses.
19933 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
19934 }
19935
19936 $licenses = $all_licenses[ $module_id ];
19937 }
19938
19939 if ( ! isset( $all_licenses[ $module_id ] ) ) {
19940 $all_licenses[ $module_id ] = array();
19941 }
19942
19943 $all_licenses[ $module_id ] = $licenses;
19944
19945 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
19946 }
19947
19948 /**
19949 * Update user information.
19950 *
19951 * @author Vova Feldman (@svovaf)
19952 * @since 1.0.1
19953 *
19954 * @param bool $store Flush to Database if true.
19955 */
19956 private function _store_user( $store = true ) {
19957 $this->_logger->entrance();
19958
19959 if ( empty( $this->_user->id ) ) {
19960 $this->_logger->error( "Empty user ID, can't store user." );
19961
19962 return;
19963 }
19964
19965 $users = self::get_all_users();
19966 $users[ $this->_user->id ] = $this->_user;
19967 self::$_accounts->set_option( 'users', $users, $store );
19968 }
19969
19970 /**
19971 * Update new updates information.
19972 *
19973 * @author Vova Feldman (@svovaf)
19974 * @since 1.0.4
19975 *
19976 * @param FS_Plugin_Tag|null $update
19977 * @param bool $store Flush to Database if true.
19978 * @param bool|number $plugin_id
19979 */
19980 private function _store_update( $update, $store = true, $plugin_id = false ) {
19981 $this->_logger->entrance();
19982
19983 if ( $update instanceof FS_Plugin_Tag ) {
19984 $update->updated = time();
19985 }
19986
19987 if ( ! is_numeric( $plugin_id ) ) {
19988 $plugin_id = $this->_plugin->id;
19989 }
19990
19991 $updates = self::get_all_updates();
19992 $updates[ $plugin_id ] = $update;
19993 self::$_accounts->set_option( 'updates', $updates, $store );
19994 }
19995
19996 /**
19997 * Update new updates information.
19998 *
19999 * @author Vova Feldman (@svovaf)
20000 * @since 1.0.6
20001 *
20002 * @param FS_Plugin[] $plugin_addons
20003 * @param bool $store Flush to Database if true.
20004 */
20005 private function _store_addons( $plugin_addons, $store = true ) {
20006 $this->_logger->entrance();
20007
20008 $addons = self::get_all_addons();
20009 $addons[ $this->_plugin->id ] = $plugin_addons;
20010 self::$_accounts->set_option( 'addons', $addons, $store );
20011 }
20012
20013 /**
20014 * Delete plugin's associated add-ons.
20015 *
20016 * @author Vova Feldman (@svovaf)
20017 * @since 1.0.8
20018 *
20019 * @param bool $store
20020 *
20021 * @return bool
20022 */
20023 private function _delete_account_addons( $store = true ) {
20024 $all_addons = self::get_all_account_addons();
20025
20026 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
20027 return false;
20028 }
20029
20030 unset( $all_addons[ $this->_plugin->id ] );
20031
20032 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20033
20034 return true;
20035 }
20036
20037 /**
20038 * Update account add-ons list.
20039 *
20040 * @author Vova Feldman (@svovaf)
20041 * @since 1.0.6
20042 *
20043 * @param FS_Plugin[] $addons
20044 * @param bool $store Flush to Database if true.
20045 */
20046 private function _store_account_addons( $addons, $store = true ) {
20047 $this->_logger->entrance();
20048
20049 $all_addons = self::get_all_account_addons();
20050 $all_addons[ $this->_plugin->id ] = $addons;
20051 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20052 }
20053
20054 /**
20055 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
20056 * the valid user licenses will be fetched again and the account add-ons may be updated.
20057 *
20058 * @author Leo Fajardo (@leorw)
20059 * @since 2.2.4
20060 */
20061 private function purge_valid_user_licenses_cache() {
20062 if ( ! $this->is_registered() ) {
20063 return;
20064 }
20065
20066 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
20067 }
20068
20069 /**
20070 * @author Leo Fajardo (@leorw)
20071 * @since 2.3.0
20072 *
20073 * @param array $all_licenses
20074 * @param number|null $site_license_id
20075 * @param bool $include_parent_licenses
20076 *
20077 * @return array
20078 */
20079 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
20080 $foreign_licenses = array(
20081 'ids' => array(),
20082 'license_keys' => array()
20083 );
20084
20085 $parent_license_ids_map = array();
20086
20087 foreach ( $all_licenses as $license ) {
20088 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
20089 continue;
20090 }
20091
20092 $foreign_licenses['ids'][] = $license->id;
20093 $foreign_licenses['license_keys'][] = $license->secret_key;
20094
20095 if (
20096 $include_parent_licenses &&
20097 is_object( $this->_license ) &&
20098 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
20099 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
20100 ) {
20101 /**
20102 * Include the parent license's info only if it has not been included before since child licenses
20103 * can have the same parent license.
20104 */
20105 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
20106 $foreign_licenses['license_keys'][] = $license->secret_key;
20107
20108 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
20109 }
20110 }
20111
20112 if ( empty( $foreign_licenses['ids'] ) ) {
20113 $foreign_licenses = array();
20114 }
20115
20116 return $foreign_licenses;
20117 }
20118
20119 /**
20120 * @author Leo Fajardo (@leorw)
20121 * @since 2.3.0
20122 *
20123 * @return string
20124 */
20125 private function get_valid_user_licenses_endpoint() {
20126 $user_licenses_endpoint = '/licenses.json?type=active' .
20127 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
20128
20129 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
20130
20131 if ( ! empty ( $foreign_licenses ) ) {
20132 $foreign_licenses = array(
20133 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20134 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20135 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20136 );
20137
20138 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20139 }
20140
20141 return $user_licenses_endpoint;
20142 }
20143
20144 /**
20145 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
20146 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
20147 * method filters out non–add-on product IDs and stores the add-on IDs.
20148 *
20149 * @author Leo Fajardo (@leorw)
20150 * @since 2.2.4
20151 *
20152 * @return stdClass[] array
20153 */
20154 private function fetch_valid_user_licenses() {
20155 $this->_logger->entrance();
20156
20157 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
20158
20159 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
20160 ! is_array( $result->licenses )
20161 ) {
20162 return array();
20163 }
20164
20165 return $result->licenses;
20166 }
20167
20168 /**
20169 * @author Leo Fajardo (@leorw)
20170 * @since 2.2.4
20171 *
20172 * @return number[] Account add-on IDs.
20173 */
20174 function get_updated_account_addons() {
20175 $addons = $this->get_addons();
20176 if ( empty( $addons ) ) {
20177 return array();
20178 }
20179
20180 $account_addons = $this->get_account_addons();
20181 if ( ! is_array( $account_addons ) ) {
20182 $account_addons = array();
20183 }
20184
20185 $user_licenses = $this->is_registered() ?
20186 $this->fetch_valid_user_licenses() :
20187 array();
20188
20189 if ( empty( $user_licenses ) ) {
20190 return $account_addons;
20191 }
20192
20193 $addon_ids = array();
20194 foreach ( $addons as $addon ) {
20195 $addon_ids[] = $addon->id;
20196 }
20197
20198 $license_product_ids = array();
20199
20200 foreach ( $user_licenses as $license ) {
20201 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
20202 $license_product_ids = array_merge( $license_product_ids, $license->products );
20203 } else {
20204 $license_product_ids[] = $license->plugin_id;
20205 }
20206 }
20207
20208 // Filter out non–add-on IDs.
20209 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
20210 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
20211 $this->_store_account_addons( array_unique( $new_account_addons ) );
20212 }
20213
20214 return $new_account_addons;
20215 }
20216
20217 /**
20218 * Store account params in the Database.
20219 *
20220 * @author Vova Feldman (@svovaf)
20221 * @since 1.0.1
20222 *
20223 * @param null|int $blog_id Since 2.0.0
20224 */
20225 private function _store_account( $blog_id = null ) {
20226 $this->_logger->entrance();
20227
20228 $this->_store_site( false, $blog_id );
20229 $this->_store_user( false );
20230 $this->_store_plans( false );
20231 $this->_store_licenses( false );
20232
20233 self::$_accounts->store( $blog_id );
20234 }
20235
20236 /**
20237 * Sync user's information.
20238 *
20239 * @author Vova Feldman (@svovaf)
20240 * @since 1.0.3
20241 * @uses FS_Api
20242 */
20243 private function _handle_account_user_sync() {
20244 $this->_logger->entrance();
20245
20246 $api = $this->get_api_user_scope();
20247
20248 // Get user's information.
20249 $user = $api->get( '/', true );
20250
20251 if ( isset( $user->id ) ) {
20252 $this->_user->first = $user->first;
20253 $this->_user->last = $user->last;
20254 $this->_user->email = $user->email;
20255
20256 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
20257
20258 if ( $user->is_verified &&
20259 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
20260 ) {
20261 $this->_user->is_verified = true;
20262
20263 $this->do_action( 'account_email_verified', $user->email );
20264
20265 $this->_admin_notices->add(
20266 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
20267 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
20268 'success',
20269 // Make admin sticky if account menu item is invisible,
20270 // since the page will be auto redirected to the plugin's
20271 // main settings page, and the non-sticky message
20272 // will disappear.
20273 ! $is_menu_item_account_visible,
20274 'email_verified'
20275 );
20276 }
20277
20278 // Flush user details to DB.
20279 $this->_store_user();
20280
20281 $this->do_action( 'after_account_user_sync', $user );
20282
20283 /**
20284 * If account menu item is hidden, redirect to plugin's main settings page.
20285 *
20286 * @author Vova Feldman (@svovaf)
20287 * @since 1.1.6
20288 *
20289 * @link https://github.com/Freemius/wordpress-sdk/issues/6
20290 */
20291 if ( ! $is_menu_item_account_visible ) {
20292 fs_redirect( $this->_get_admin_page_url() );
20293 }
20294 }
20295 }
20296
20297 /**
20298 * @author Vova Feldman (@svovaf)
20299 * @since 1.0.9
20300 * @uses FS_Api
20301 *
20302 * @param number|bool $license_id
20303 *
20304 * @return FS_Subscription|object|bool
20305 */
20306 private function _fetch_site_license_subscription( $license_id = false ) {
20307 $this->_logger->entrance();
20308 $api = $this->get_api_site_scope();
20309
20310 if ( ! is_numeric( $license_id ) ) {
20311 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
20312 $this->_license->parent_license_id :
20313 $this->_license->id;
20314 }
20315
20316 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
20317
20318 return ! isset( $result->error ) ?
20319 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
20320 new FS_Subscription( $result->subscriptions[0] ) :
20321 false
20322 ) :
20323 $result;
20324 }
20325
20326 /**
20327 * @author Vova Feldman (@svovaf)
20328 * @since 1.0.4
20329 * @uses FS_Api
20330 *
20331 * @param number|bool $plan_id
20332 *
20333 * @return FS_Plugin_Plan|object
20334 */
20335 private function _fetch_site_plan( $plan_id = false ) {
20336 $this->_logger->entrance();
20337 $api = $this->get_api_site_scope();
20338
20339 if ( ! is_numeric( $plan_id ) ) {
20340 $plan_id = $this->_site->plan_id;
20341 }
20342
20343 $plan = $api->get( "/plans/{$plan_id}.json", true );
20344
20345 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
20346 }
20347
20348 /**
20349 * @author Vova Feldman (@svovaf)
20350 * @since 1.0.5
20351 * @uses FS_Api
20352 *
20353 * @return FS_Plugin_Plan[]|object
20354 */
20355 private function _fetch_plugin_plans() {
20356 $this->_logger->entrance();
20357 $api = $this->get_current_or_network_user_api_scope();
20358
20359 /**
20360 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
20361 */
20362 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
20363
20364 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
20365 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
20366 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
20367 }
20368
20369 $result = $result->plans;
20370 }
20371
20372 return $result;
20373 }
20374
20375 /**
20376 * @author Vova Feldman (@svovaf)
20377 * @since 2.0.0
20378 *
20379 * @param number $plan_id
20380 *
20381 * @return \FS_Plugin_Plan|object
20382 */
20383 private function fetch_plan_by_id( $plan_id ) {
20384 $this->_logger->entrance();
20385 $api = $this->get_current_or_network_user_api_scope();
20386
20387 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
20388
20389 return $this->is_api_result_entity( $result ) ?
20390 new FS_Plugin_Plan( $result ) :
20391 $result;
20392 }
20393
20394 /**
20395 * @author Vova Feldman (@svovaf)
20396 * @since 1.0.5
20397 * @uses FS_Api
20398 *
20399 * @param number|bool $plugin_id
20400 * @param number|bool $site_license_id
20401 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
20402 * @param number|null $blog_id
20403 *
20404 * @return FS_Plugin_License[]|object
20405 */
20406 private function _fetch_licenses(
20407 $plugin_id = false,
20408 $site_license_id = false,
20409 $foreign_licenses = array(),
20410 $blog_id = null
20411 ) {
20412 $this->_logger->entrance();
20413
20414 $api = $this->get_api_user_scope();
20415
20416 if ( ! is_numeric( $plugin_id ) ) {
20417 $plugin_id = $this->_plugin->id;
20418 }
20419
20420 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
20421 if ( ! empty ( $foreign_licenses ) ) {
20422 $foreign_licenses = array(
20423 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20424 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20425 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20426 );
20427
20428 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20429 }
20430
20431 $result = $api->get( $user_licenses_endpoint, true );
20432
20433 $is_site_license_synced = false;
20434
20435 $api_errors = array();
20436
20437 if ( $this->is_api_result_object( $result, 'licenses' ) &&
20438 is_array( $result->licenses )
20439 ) {
20440 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
20441 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
20442
20443 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
20444 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
20445 }
20446 }
20447
20448 $result = $result->licenses;
20449 } else {
20450 $api_errors[] = $result;
20451 $result = array();
20452 }
20453
20454 if ( ! $is_site_license_synced ) {
20455 if ( ! is_null( $blog_id ) ) {
20456 /**
20457 * If blog ID is not null, the request is for syncing of the license of a single site via the
20458 * network-level "Account" page.
20459 *
20460 * @author Leo Fajardo (@leorw)
20461 */
20462 $this->switch_to_blog( $blog_id );
20463 }
20464
20465 $api = $this->get_api_site_scope();
20466
20467 if ( is_numeric( $site_license_id ) ) {
20468 // Try to retrieve a foreign license that is linked to the install.
20469 $api_result = $api->call( '/licenses.json?is_enriched=true' );
20470
20471 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
20472 is_array( $api_result->licenses )
20473 ) {
20474 $licenses = $api_result->licenses;
20475
20476 if ( ! empty( $licenses ) ) {
20477 $result[] = new FS_Plugin_License( $licenses[0] );
20478 }
20479 } else {
20480 $api_errors[] = $api_result;
20481 }
20482 } else if (
20483 is_object( $this->_license ) &&
20484 /**
20485 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
20486 * the FS instance that does the syncing is the parent FS instance.
20487 *
20488 * @author Leo Fajardo (@leorw)
20489 * @since 2.3.0
20490 */
20491 $this->_license->plugin_id == $plugin_id
20492 ) {
20493 $is_license_in_result = false;
20494 if ( ! empty( $result ) ) {
20495 foreach ( $result as $license ) {
20496 if ( $license->id == $this->_license->id ) {
20497 $is_license_in_result = true;
20498 break;
20499 }
20500 }
20501 }
20502
20503 if ( ! $is_license_in_result ) {
20504 // Fetch foreign license by ID and license key.
20505 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
20506 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
20507
20508 if ( $this->is_api_result_entity( $license ) ) {
20509 $result[] = new FS_Plugin_License( $license );
20510 } else {
20511 $api_errors[] = $license;
20512 }
20513 }
20514 }
20515
20516 if ( ! is_null( $blog_id ) ) {
20517 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20518 }
20519 }
20520
20521 if ( is_array( $result ) && 0 < count( $result ) ) {
20522 // If found at least one license, return license collection even if there are errors.
20523 return $result;
20524 }
20525
20526 if ( ! empty( $api_errors ) ) {
20527 // If found any errors and no licenses, return first error.
20528 return $api_errors[0];
20529 }
20530
20531 // Fallback to empty licenses list.
20532 return $result;
20533 }
20534
20535 /**
20536 * @author Vova Feldman (@svovaf)
20537 * @since 2.0.0
20538 *
20539 * @param number $license_id
20540 * @param string $license_key
20541 *
20542 * @return \FS_Plugin_License|object
20543 */
20544 private function fetch_license_by_key( $license_id, $license_key ) {
20545 $this->_logger->entrance();
20546
20547 $api = $this->get_current_or_network_user_api_scope();
20548
20549 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
20550
20551 return $this->is_api_result_entity( $result ) ?
20552 new FS_Plugin_License( $result ) :
20553 $result;
20554 }
20555
20556 /**
20557 * @author Vova Feldman (@svovaf)
20558 * @since 1.2.0
20559 * @uses FS_Api
20560 *
20561 * @param number|bool $plugin_id
20562 * @param bool $flush
20563 *
20564 * @return FS_Payment[]|object
20565 */
20566 function _fetch_payments( $plugin_id = false, $flush = false ) {
20567 $this->_logger->entrance();
20568
20569 $api = $this->get_api_user_scope();
20570
20571 if ( ! is_numeric( $plugin_id ) ) {
20572 $plugin_id = $this->_plugin->id;
20573 }
20574
20575 $include_bundles = (
20576 is_object( $this->_plugin ) &&
20577 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
20578 );
20579
20580 $result = $api->get(
20581 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
20582 $flush
20583 );
20584
20585 if ( ! isset( $result->error ) ) {
20586 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
20587 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
20588 }
20589 $result = $result->payments;
20590 }
20591
20592 return $result;
20593 }
20594
20595 /**
20596 * @author Vova Feldman (@svovaf)
20597 * @since 1.2.1.5
20598 * @uses FS_Api
20599 *
20600 * @param bool $flush
20601 *
20602 * @return \FS_Billing|mixed
20603 */
20604 function _fetch_billing( $flush = false ) {
20605 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
20606
20607 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
20608
20609 if ( $this->is_api_result_entity( $billing ) ) {
20610 $billing = new FS_Billing( $billing );
20611 }
20612
20613 return $billing;
20614 }
20615
20616 /**
20617 * @author Vova Feldman (@svovaf)
20618 * @since 1.0.5
20619 *
20620 * @param FS_Plugin_License[] $licenses
20621 * @param number $module_id
20622 */
20623 private function _update_licenses( $licenses, $module_id ) {
20624 $this->_logger->entrance();
20625
20626 if ( is_array( $licenses ) ) {
20627 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
20628 $licenses[ $i ]->updated = time();
20629 }
20630 }
20631
20632 $this->_store_licenses( true, $module_id, $licenses );
20633 }
20634
20635 /**
20636 * @author Vova Feldman (@svovaf)
20637 * @since 1.0.4
20638 *
20639 * @param bool|number $plugin_id
20640 * @param bool $flush Since 1.1.7.3
20641 * @param int $expiration Since 1.2.2.7
20642 * @param bool|string $newer_than Since 2.2.1
20643 * @param bool $fetch_upgrade_notice Since 2.12.1
20644 *
20645 * @return object|false New plugin tag info if exist.
20646 */
20647 private function _fetch_newer_version(
20648 $plugin_id = false,
20649 $flush = true,
20650 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
20651 $newer_than = false,
20652 $fetch_upgrade_notice = true
20653 ) {
20654 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than, false, $fetch_upgrade_notice );
20655
20656 if ( ! is_object( $latest_tag ) ) {
20657 return false;
20658 }
20659
20660 $plugin_version = $this->get_plugin_version();
20661
20662 // Check if version is actually newer.
20663 $has_new_version =
20664 // If it's an non-installed add-on then always return latest.
20665 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
20666 // Compare versions.
20667 version_compare( $plugin_version, $latest_tag->version, '<' );
20668
20669 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
20670
20671 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
20672
20673 $this->_storage->beta_data = array(
20674 'is_beta' => $is_latest_version_beta,
20675 'version' => $latest_tag->version
20676 );
20677
20678 return $has_new_version ? $latest_tag : false;
20679 }
20680
20681 /**
20682 * @author Vova Feldman (@svovaf)
20683 * @since 1.0.5
20684 *
20685 * @param bool|number $plugin_id
20686 * @param bool $flush Since 1.1.7.3
20687 *
20688 * @return bool|FS_Plugin_Tag
20689 */
20690 function get_update( $plugin_id = false, $flush = true ) {
20691 $this->_logger->entrance();
20692
20693 if ( ! is_numeric( $plugin_id ) ) {
20694 $plugin_id = $this->_plugin->id;
20695 }
20696
20697 $this->check_updates( true, $plugin_id, $flush );
20698
20699 $updates = $this->get_all_updates();
20700
20701 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
20702 }
20703
20704 /**
20705 * Check if site assigned with active license.
20706 *
20707 * @author Vova Feldman (@svovaf)
20708 * @since 1.0.6
20709 *
20710 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
20711 */
20712 function has_active_license() {
20713 return (
20714 is_object( $this->_license ) &&
20715 is_numeric( $this->_license->id ) &&
20716 ! $this->_license->is_expired()
20717 );
20718 }
20719
20720 /**
20721 * Check if site assigned with active & valid (not expired) license.
20722 *
20723 * @author Vova Feldman (@svovaf)
20724 * @since 1.2.1
20725 *
20726 * @param bool $check_expiration
20727 */
20728 function has_active_valid_license( $check_expiration = true ) {
20729 return self::is_active_valid_license( $this->_license, $check_expiration );
20730 }
20731
20732 /**
20733 * @author Leo Fajardo (@leorw)
20734 * @since 2.3.1
20735 */
20736 function is_data_debug_mode() {
20737 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
20738 return false;
20739 }
20740
20741 $fs = $this->is_addon() ?
20742 $this->get_parent_instance() :
20743 $this;
20744
20745 if ( $fs->is_network_active() && fs_is_network_admin() ) {
20746 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
20747 } else {
20748 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
20749 }
20750
20751 return ( 'true' === $is_developer_license_debug_mode );
20752 }
20753
20754 /**
20755 * @author Leo Fajardo (@leorw)
20756 * @since 2.3.1
20757 */
20758 function _set_data_debug_mode() {
20759 if ( ! $this->is_whitelabeled( true ) ) {
20760 return;
20761 }
20762
20763 $license_or_user_key = fs_request_get_raw( 'license_or_user_key' );
20764
20765 $transient_value = ( ! empty( $license_or_user_key ) ) ?
20766 'true' :
20767 'false';
20768
20769 if ( 'true' === $transient_value ) {
20770 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
20771 'last_license_key' :
20772 'last_license_user_key'
20773 );
20774
20775 if ( md5( $license_or_user_key ) !== $stored_key ) {
20776 $this->shoot_ajax_failure( sprintf(
20777 '%s... %s',
20778 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
20779 $this->get_text_inline(
20780 'seems like the key you entered doesn\'t match our records.',
20781 'developer-or-license-not-found'
20782 )
20783 ) );
20784 }
20785 }
20786
20787 if ( $this->is_network_active() && fs_is_network_admin() ) {
20788 set_site_transient(
20789 "fs_{$this->get_id()}_data_debug_mode",
20790 $transient_value,
20791 WP_FS__TIME_24_HOURS_IN_SEC / 24
20792 );
20793 } else {
20794 set_transient(
20795 "fs_{$this->get_id()}_data_debug_mode",
20796 $transient_value,
20797 WP_FS__TIME_24_HOURS_IN_SEC / 24
20798 );
20799 }
20800
20801 if ( 'true' === $transient_value ) {
20802 $this->_admin_notices->add_sticky(
20803 $this->get_text_inline(
20804 '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.',
20805 'data_debug_mode_enabled'
20806 ),
20807 'data_debug_mode_enabled'
20808 );
20809 }
20810
20811 $this->shoot_ajax_success();
20812 }
20813
20814 /**
20815 * Check if a given license is active & valid (not expired).
20816 *
20817 * @author Vova Feldman (@svovaf)
20818 * @since 2.1.3
20819 *
20820 * @param FS_Plugin_License $license
20821 * @param bool $check_expiration
20822 *
20823 * @return bool
20824 */
20825 private static function is_active_valid_license( $license, $check_expiration = true ) {
20826 return (
20827 is_object( $license ) &&
20828 FS_Plugin_License::is_valid_id( $license->id ) &&
20829 $license->is_active() &&
20830 ( ! $check_expiration || $license->is_valid() )
20831 );
20832 }
20833
20834 /**
20835 * Checks if there's any site that is associated with an active & valid license.
20836 * This logic is used to determine if the admin can download the premium code base from a network level admin.
20837 *
20838 * @author Vova Feldman (@svovaf)
20839 * @since 2.1.3
20840 *
20841 * @return bool
20842 */
20843 function has_any_active_valid_license() {
20844 if ( ! fs_is_network_admin() ) {
20845 return $this->has_active_valid_license();
20846 }
20847
20848 $installs = $this->get_blog_install_map();
20849 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
20850
20851 foreach ( $installs as $blog_id => $install ) {
20852 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20853 continue;
20854 }
20855
20856 foreach ( $all_plugin_licenses as $license ) {
20857 if ( $license->id == $install->license_id ) {
20858 if ( self::is_active_valid_license( $license ) ) {
20859 return true;
20860 }
20861 }
20862 }
20863 }
20864
20865 return false;
20866 }
20867
20868 /**
20869 * Check if site assigned with license with enabled features.
20870 *
20871 * @author Vova Feldman (@svovaf)
20872 * @since 1.0.6
20873 *
20874 * @return bool
20875 */
20876 function has_features_enabled_license() {
20877 return (
20878 is_object( $this->_license ) &&
20879 is_numeric( $this->_license->id ) &&
20880 $this->_license->is_features_enabled()
20881 );
20882 }
20883
20884 /**
20885 * Checks if the product is activated with a bundle license.
20886 *
20887 * @author Leo Fajardo (@leorw)
20888 * @since 2.4.0
20889 *
20890 * @return bool
20891 */
20892 function is_activated_with_bundle_license() {
20893 if ( ! $this->has_features_enabled_license() ) {
20894 return false;
20895 }
20896
20897 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
20898 }
20899
20900 /**
20901 * Check if user is a trial or have feature enabled license.
20902 *
20903 * @author Vova Feldman (@svovaf)
20904 * @since 1.1.7
20905 *
20906 * @return bool
20907 */
20908 function can_use_premium_code() {
20909 return $this->is_trial() || $this->has_features_enabled_license();
20910 }
20911
20912 /**
20913 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
20914 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
20915 * the context user is authenticated.
20916 *
20917 * @author Leo Fajardo (@leorw)
20918 * @since 1.2.2
20919 *
20920 * @return bool
20921 */
20922 function is_user_admin() {
20923 /**
20924 * Require a super-admin when network activated, running from the network level OR if
20925 * running from the site level but not delegated the opt-in.
20926 *
20927 * @author Vova Feldman (@svovaf)
20928 * @since 2.0.0
20929 */
20930 if ( $this->_is_network_active &&
20931 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
20932 ) {
20933 return is_super_admin();
20934 }
20935
20936 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
20937 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
20938 }
20939
20940 /**
20941 * Sync site's plan.
20942 *
20943 * @author Vova Feldman (@svovaf)
20944 * @since 1.0.3
20945 *
20946 * @uses FS_Api
20947 *
20948 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
20949 * the admin.
20950 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
20951 * network-level "Account" page.
20952 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
20953 * `_sync_plugin_license` method in order to switch to the previous blog when sending
20954 * updates for a single site in case `execute_cron` has switched to a different blog.
20955 */
20956 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
20957 $this->_logger->entrance();
20958
20959 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
20960
20961 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
20962
20963 if ( $is_addon_sync ) {
20964 $this->_sync_addon_license( $plugin_id, $background );
20965 } else {
20966 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
20967 }
20968
20969 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
20970 }
20971
20972 /**
20973 * Sync plugin's add-on license.
20974 *
20975 * @author Vova Feldman (@svovaf)
20976 * @since 1.0.6
20977 * @uses FS_Api
20978 *
20979 * @param number $addon_id
20980 * @param bool $background
20981 */
20982 private function _sync_addon_license( $addon_id, $background ) {
20983 $this->_logger->entrance();
20984
20985 if ( $this->is_addon_activated( $addon_id ) ) {
20986 // If already installed, use add-on sync.
20987 $fs_addon = self::get_instance_by_id( $addon_id );
20988
20989 if (
20990 // Add-on is network activated and network integrated.
20991 $fs_addon->is_network_active() ||
20992 // Background sync cron.
20993 self::is_cron() ||
20994 // Add-on is not network activated or not network integrated.
20995 ! fs_is_network_admin()
20996 ) {
20997 $fs_addon->_sync_license( $background );
20998
20999 return;
21000 }
21001 }
21002
21003 // Validate add-on exists.
21004 $addon = $this->get_addon( $addon_id );
21005
21006 if ( ! is_object( $addon ) ) {
21007 return;
21008 }
21009
21010 // Add add-on into account add-ons.
21011 $account_addons = $this->get_account_addons();
21012 if ( ! is_array( $account_addons ) ) {
21013 $account_addons = array();
21014 }
21015 $account_addons[] = $addon->id;
21016 $account_addons = array_unique( $account_addons );
21017 $this->_store_account_addons( $account_addons );
21018
21019 // Load add-on licenses.
21020 $licenses = $this->_fetch_licenses( $addon->id );
21021
21022 // Sync add-on licenses.
21023 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
21024 $this->_update_licenses( $licenses, $addon->id );
21025
21026 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
21027 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
21028
21029 if ( ! isset( $plans_result->error ) ) {
21030 $plans = array();
21031 foreach ( $plans_result->plans as $plan ) {
21032 $plans[] = new FS_Plugin_Plan( $plan );
21033 }
21034
21035 $this->_admin_notices->add_sticky(
21036 sprintf(
21037 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
21038 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
21039 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
21040 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
21041 $addon->title
21042 ) . ' ' . $this->get_latest_download_link(
21043 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
21044 $addon_id
21045 ),
21046 'addon_plan_upgraded_' . $addon->slug,
21047 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
21048 );
21049 }
21050 }
21051 }
21052 }
21053
21054 /**
21055 * Sync site's plugin plan.
21056 *
21057 * @author Vova Feldman (@svovaf)
21058 * @since 1.0.6
21059 * @uses FS_Api
21060 *
21061 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
21062 * @param bool $send_installs_update Since 2.0.0
21063 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
21064 * syncing its license from the network-level "Account" page (e.g.: after
21065 * activating a license only for the single install).
21066 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
21067 * can be used here to switch to the previous blog in case `execute_cron`
21068 * has switched to a different blog.
21069 */
21070 private function _sync_plugin_license(
21071 $background = false,
21072 $send_installs_update = true,
21073 $is_context_single_site = false,
21074 $current_blog_id = null
21075 ) {
21076 $this->_logger->entrance();
21077
21078 $plan_change = 'none';
21079
21080 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
21081
21082 if ( ! $send_installs_update ) {
21083 $site = $this->_site;
21084 } else {
21085 /**
21086 * Sync site info.
21087 *
21088 * @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.
21089 */
21090 if ( $is_site_level_sync ) {
21091 /**
21092 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
21093 *
21094 * @author Leo Fajardo (@leorw)
21095 * @since 2.2.3
21096 */
21097 if ( is_numeric( $current_blog_id ) ) {
21098 $this->switch_to_blog( $current_blog_id );
21099 }
21100
21101 $result = $this->send_install_update( array(), true, true );
21102 $is_valid = $this->is_api_result_entity( $result );
21103 } else {
21104 $result = $this->send_installs_update( array(), true, true );
21105 $is_valid = $this->is_api_result_object( $result, 'installs' );
21106 }
21107
21108 if ( ! $is_valid ) {
21109 if ( $is_context_single_site ) {
21110 // Switch back to the main blog so that the following logic will have the right entities.
21111 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21112 }
21113
21114 // Show API message only if not background sync or if paying customer.
21115 if ( ! $background || $this->is_paying() ) {
21116 // Try to ping API to see if not blocked.
21117 if ( FS_Api::is_blocked( $result ) ) {
21118 /**
21119 * @author Vova Feldman (@svovaf)
21120 * @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.
21121 */
21122 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
21123 // Add notice immediately if not a background sync.
21124 $add_notice = ( ! $background );
21125
21126 if ( ! $add_notice ) {
21127 $counter = (int) get_transient( '_fs_api_connection_retry_counter' );
21128
21129 // We only want to add the notice after 3 consecutive failures.
21130 $add_notice = ( 3 <= $counter );
21131
21132 if ( ! $add_notice ) {
21133 /**
21134 * 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.
21135 *
21136 * 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.
21137 */
21138 set_transient( '_fs_api_connection_retry_counter', $counter + 1, WP_FS__TIME_WEEK_IN_SEC );
21139 }
21140 }
21141
21142 // Add notice instantly for not-background sync and only after 3 failed attempts for background sync.
21143 if ( $add_notice ) {
21144 self::$_global_admin_notices->add(
21145 $this->generate_api_blocked_notice_message_from_result( $result ),
21146 '',
21147 'error',
21148 $background,
21149 'api_blocked'
21150 );
21151
21152 add_action( 'admin_footer', array( 'Freemius', '_add_api_connectivity_notice_handler_js' ) );
21153
21154 // Notice was just shown, reset connectivity counter.
21155 delete_transient( '_fs_api_connection_retry_counter' );
21156 }
21157 }
21158 } else if ( is_object( $result ) ) {
21159 // Authentication params are broken.
21160 $this->_admin_notices->add(
21161 $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 ),
21162 '',
21163 'error'
21164 );
21165 }
21166 }
21167
21168 // No reason to continue with license sync while there are API issues.
21169 return;
21170 }
21171
21172 // API is working now. Delete the transient and start afresh.
21173 delete_transient('_fs_api_connection_retry_counter');
21174
21175 if ( $is_site_level_sync ) {
21176 $site = new FS_Site( $result );
21177 } else {
21178 // Map site addresses to their blog IDs.
21179 $address_to_blog_map = $this->get_address_to_blog_map();
21180
21181 // Find the current context install.
21182 $site = null;
21183 foreach ( $result->installs as $install ) {
21184 if ( $install->id == $this->_site->id ) {
21185 $site = new FS_Site( $install );
21186 } else {
21187 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
21188 $blog_id = $address_to_blog_map[ $address ];
21189
21190 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
21191 }
21192 }
21193 }
21194
21195 // Sync plans.
21196 $this->_sync_plans();
21197 }
21198
21199 // Remove sticky API connectivity message.
21200 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
21201
21202 if ( ! $this->has_paid_plan() ) {
21203 $this->_site = $site;
21204 $this->_store_site(
21205 true,
21206 $is_site_level_sync ?
21207 null :
21208 $this->get_network_install_blog_id()
21209 );
21210 } else {
21211 $context_blog_id = 0;
21212
21213 if ( $is_context_single_site ) {
21214 $context_blog_id = get_current_blog_id();
21215
21216 // Switch back to the main blog in order to properly sync the license.
21217 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21218 }
21219
21220 /**
21221 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
21222 * associated with that ID is not included in the user's licenses collection.
21223 * Save previous value to manage remote license renewals.
21224 */
21225 $was_license_expired_before_sync = is_object( $this->_license ) && $this->_license->is_expired();
21226 $this->_sync_licenses(
21227 $site->license_id,
21228 ( $is_context_single_site ?
21229 $context_blog_id :
21230 null
21231 )
21232 );
21233
21234 if ( $is_context_single_site ) {
21235 $this->switch_to_blog( $context_blog_id );
21236 }
21237
21238 // Check if plan / license changed.
21239 if ( $site->plan_id != $this->_site->plan_id ||
21240 // Check if trial started.
21241 $site->trial_plan_id != $this->_site->trial_plan_id ||
21242 $site->trial_ends != $this->_site->trial_ends ||
21243 // Check if license changed.
21244 $site->license_id != $this->_site->license_id
21245 ) {
21246 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
21247 // New trial started.
21248 $this->_site = $site;
21249 $plan_change = 'trial_started';
21250
21251 // For trial with subscription use-case.
21252 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
21253
21254 if ( is_object( $new_license ) && $new_license->is_valid() ) {
21255 $this->_site = $site;
21256 $this->_update_site_license( $new_license );
21257 $this->_store_licenses();
21258
21259 $this->_sync_site_subscription( $this->_license );
21260 }
21261 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
21262 // Was in trial, but now trial expired and no license ID.
21263 // New trial started.
21264 $this->_site = $site;
21265 $plan_change = 'trial_expired';
21266 } else {
21267 $is_free = $this->is_free_plan();
21268
21269 // Make sure license exist and not expired.
21270 $new_license = is_null( $site->license_id ) ?
21271 null :
21272 $this->_get_license_by_id( $site->license_id );
21273
21274 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
21275 // License cancelled.
21276 $this->_site = $site;
21277 $this->_update_site_license( $new_license );
21278 $this->_store_licenses();
21279
21280 $plan_change = 'cancelled';
21281 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
21282 // The license is expired, so ignore upgrade method.
21283 $this->_site = $site;
21284 } else {
21285 // License changed.
21286 $this->_site = $site;
21287
21288 /**
21289 * IMPORTANT:
21290 * 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.
21291 *
21292 * @author Vova Feldman (@svovaf)
21293 * @since 2.0.0
21294 */
21295 $this->_update_site_license( $new_license );
21296
21297 if ( ! $is_context_single_site &&
21298 fs_is_network_admin() &&
21299 $this->_is_network_active &&
21300 $new_license->quota > 1 &&
21301 get_blog_count() > 1
21302 ) {
21303 // See if license can activated on all sites.
21304 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
21305 if ( ! fs_request_get_bool( 'auto_install' ) ) {
21306 // Open the license activation dialog box on the account page.
21307 add_action( 'admin_footer', array(
21308 &$this,
21309 '_open_license_activation_dialog_box'
21310 ) );
21311 }
21312 }
21313 }
21314
21315 $this->_store_licenses();
21316
21317 $plan_change = $is_free ?
21318 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
21319 ( is_object( $new_license ) ?
21320 'changed' :
21321 'downgraded' );
21322 }
21323 }
21324
21325 // Store updated site info.
21326 $this->_store_site(
21327 true,
21328 $is_site_level_sync ?
21329 null :
21330 $this->get_network_install_blog_id()
21331 );
21332 } else {
21333 if ( ! is_object( $this->_license ) ) {
21334 $this->maybe_update_whitelabel_flag(
21335 FS_Plugin_License::is_valid_id( $site->license_id ) ?
21336 $this->get_license_by_id( $site->license_id ) :
21337 null
21338 );
21339 } else {
21340 $this->maybe_update_whitelabel_flag( $this->_license );
21341
21342 if ( $this->_license->is_expired() ) {
21343 if ( ! $this->has_features_enabled_license() ) {
21344 $this->_deactivate_license();
21345 $plan_change = 'downgraded';
21346 } else {
21347 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
21348
21349 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
21350 /**
21351 * Show the expired license notice every 14 days.
21352 *
21353 * @author Leo Fajardo (@leorw)
21354 * @since 2.3.1
21355 */
21356 $plan_change = 'expired';
21357 }
21358 }
21359 } else if ( $was_license_expired_before_sync ) {
21360 /**
21361 * If license was expired but it is not anymore.
21362 *
21363 *
21364 * @author Daniele Alessandra (@danielealessandra)
21365 */
21366 $plan_change = 'extended';
21367 }
21368 }
21369
21370 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
21371 $this->_sync_site_subscription( $this->_license );
21372 }
21373 }
21374
21375 if ( ! $this->is_addon() &&
21376 $this->_site->is_beta() !== $site->is_beta()
21377 ) {
21378 // Beta flag updated.
21379 $this->_site = $site;
21380
21381 $this->_store_site(
21382 true,
21383 $is_site_level_sync ?
21384 null :
21385 $this->get_network_install_blog_id()
21386 );
21387 }
21388
21389 if ( $this->is_addon() || $this->has_addons() ) {
21390 /**
21391 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21392 * an updated valid user licenses collection will be fetched from the server which is used to also
21393 * update the account add-ons (add-ons the user has licenses for).
21394 *
21395 * @author Leo Fajardo (@leorw)
21396 * @since 2.2.4
21397 */
21398 $this->purge_valid_user_licenses_cache();
21399 }
21400 }
21401
21402 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21403
21404 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
21405 switch ( $plan_change ) {
21406 case 'none':
21407 if ( ! $background && is_admin() ) {
21408 $plan = $this->is_trial() ?
21409 $this->get_trial_plan() :
21410 $this->get_plan();
21411
21412 if ( $plan->is_free() ) {
21413 $this->_admin_notices->add(
21414 sprintf(
21415 $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' ),
21416 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
21417 ) . ' ' . sprintf(
21418 '<a href="%s">%s</a>',
21419 $this->contact_url(
21420 'bug',
21421 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' ),
21422 strtoupper( $plan->name )
21423 )
21424 ),
21425 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21426 ),
21427 $hmm_text
21428 );
21429 }
21430 }
21431 break;
21432 case 'upgraded':
21433 case 'activated':
21434 $this->add_after_plan_activation_or_upgrade_instructions_notice( 'upgraded' === $plan_change );
21435
21436 $this->_admin_notices->remove_sticky( array(
21437 'trial_started',
21438 'trial_promotion',
21439 'trial_expired',
21440 'activation_complete',
21441 'license_expired',
21442 ) );
21443 break;
21444 case 'extended':
21445 $this->_admin_notices->remove_sticky( array(
21446 'trial_expired',
21447 'license_expired',
21448 ) );
21449 break;
21450 case 'changed':
21451 $this->_admin_notices->add_sticky(
21452 sprintf(
21453 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
21454 $this->get_plan_title()
21455 ),
21456 'plan_changed'
21457 );
21458
21459 $this->_admin_notices->remove_sticky( array(
21460 'trial_started',
21461 'trial_promotion',
21462 'trial_expired',
21463 'activation_complete',
21464 ) );
21465 break;
21466 case 'downgraded':
21467 $this->_admin_notices->add_sticky(
21468 ($this->has_free_plan() ?
21469 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 ) :
21470 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21471 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) ) ),
21472 'license_expired',
21473 $hmm_text
21474 );
21475 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21476 break;
21477 case 'cancelled':
21478 $this->_admin_notices->add(
21479 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
21480 sprintf(
21481 '<a href="%s">%s</a>',
21482 $this->contact_url( 'bug' ),
21483 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21484 ),
21485 $hmm_text,
21486 'error'
21487 );
21488 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21489 break;
21490 case 'expired':
21491 $this->_admin_notices->add_sticky(
21492 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 ),
21493 'license_expired',
21494 $hmm_text
21495 );
21496
21497 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
21498
21499 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21500 break;
21501 case 'trial_started':
21502 $this->add_complete_upgrade_instructions_notice(
21503 sprintf(
21504 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
21505 '<i>' . $this->get_plugin_name() . '</i>'
21506 ),
21507 'trial_started',
21508 $this->get_trial_plan()->title
21509 );
21510
21511 $this->_admin_notices->remove_sticky( array(
21512 'trial_promotion',
21513 ) );
21514 break;
21515 case 'trial_expired':
21516 $this->_admin_notices->add_sticky(
21517 ($this->has_free_plan() ?
21518 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
21519 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21520 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))),
21521 'trial_expired',
21522 $hmm_text
21523 );
21524 $this->_admin_notices->remove_sticky( array(
21525 'trial_started',
21526 'trial_promotion',
21527 'plan_upgraded',
21528 ) );
21529 break;
21530 }
21531 }
21532
21533 if ( 'none' !== $plan_change ) {
21534 if (
21535 ! is_object( $this->_license ) ||
21536 ! $this->_license->is_whitelabeled
21537 ) {
21538 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
21539 }
21540
21541 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
21542 }
21543 }
21544
21545 /**
21546 * @author Leo Fajardo (@leorw)
21547 * @since 2.5.4
21548 *
21549 * @param mixed $result
21550 *
21551 * @return string
21552 */
21553 private function generate_api_blocked_notice_message_from_result( $result ) {
21554 $api_domains = $this->apply_filters( 'api_domains', array(
21555 'api.freemius.com',
21556 'wp.freemius.com',
21557 ) );
21558
21559 $api_domains_list_items = '';
21560
21561 foreach( $api_domains as $api_domain ) {
21562 $api_domains_list_items .= "<li>{$api_domain}</li>";
21563 }
21564
21565 $error_message = sprintf(
21566 $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' ),
21567 $this->get_plugin_name(),
21568 "<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>"
21569 );
21570
21571 $error_message =
21572 "<div>{$error_message}</div>" .
21573 '<div class="fs-api-request-error-details" style="display: none">' .
21574 '<strong>' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . '</strong><br>' .
21575 $result->error->message .
21576 '</div>';
21577
21578 return $error_message;
21579 }
21580
21581 /**
21582 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
21583 *
21584 * @author Vova Feldman (@svovaf)
21585 * @since 2.0.0
21586 */
21587 public function _open_license_activation_dialog_box() {
21588 $vars = array( 'license_id' => $this->_site->license_id );
21589 fs_require_once_template( 'js/open-license-activation.php', $vars );
21590 }
21591
21592 /**
21593 * @author Vova Feldman (@svovaf)
21594 * @since 1.0.5
21595 *
21596 * @param bool $background
21597 * @param FS_Plugin_License|null $premium_license
21598 */
21599 protected function _activate_license( $background = false, $premium_license = null ) {
21600 $this->_logger->entrance();
21601
21602 if ( is_null( $premium_license ) ) {
21603 $license_id = fs_request_get( 'license_id' );
21604
21605 if ( is_object( $this->_site ) &&
21606 FS_Plugin_License::is_valid_id( $license_id ) &&
21607 $license_id == $this->_site->license_id
21608 ) {
21609 // License is already activated.
21610 return;
21611 }
21612
21613 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
21614 $this->_get_license_by_id( $license_id ) :
21615 $this->_get_available_premium_license();
21616 }
21617
21618 if ( ! is_object( $premium_license ) ) {
21619 return;
21620 }
21621
21622 if ( ! is_object( $this->_site ) ) {
21623 // Not yet opted-in.
21624 $user = $this->get_current_or_network_user();
21625 if ( ! is_object( $user ) ) {
21626 $user = self::_get_user_by_id( $premium_license->user_id );
21627 }
21628
21629 if ( is_object( $user ) ) {
21630 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
21631 } else {
21632 $this->opt_in(
21633 false,
21634 false,
21635 false,
21636 $premium_license->secret_key,
21637 false,
21638 false,
21639 false,
21640 null,
21641 array(),
21642 true,
21643 $premium_license->user_id
21644 );
21645
21646 return;
21647 }
21648 }
21649
21650
21651 /**
21652 * If the premium license is already associated with the install, just
21653 * update the license reference (activation is not required).
21654 *
21655 * @since 1.1.9
21656 */
21657 if ( $premium_license->id == $this->_site->license_id ) {
21658 // License is already activated.
21659 $this->_update_site_license( $premium_license );
21660 $this->_store_account();
21661
21662 return;
21663 }
21664
21665 if ( $this->_site->user_id != $premium_license->user_id ) {
21666 $api_request_params = array( 'license_key' => $premium_license->secret_key );
21667 } else {
21668 $api_request_params = array();
21669 }
21670
21671 $api = $this->get_api_site_scope();
21672 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
21673
21674 if ( ! $this->is_api_result_entity( $license ) ) {
21675 if ( ! $background ) {
21676 $this->_admin_notices->add( sprintf(
21677 '%s %s',
21678 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
21679 ( is_object( $license ) && isset( $license->error ) ?
21680 $license->error->message :
21681 sprintf( '%s<br><code>%s</code>',
21682 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
21683 var_export( $license, true )
21684 )
21685 )
21686 ),
21687 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
21688 'error'
21689 );
21690 }
21691
21692 return;
21693 }
21694
21695 $this->do_action( 'after_license_activation' );
21696
21697 $premium_license = new FS_Plugin_License( $license );
21698
21699 // Updated site plan.
21700 $site = $this->get_api_site_scope()->get( '/', true );
21701 if ( $this->is_api_result_entity( $site ) ) {
21702 $this->_site = new FS_Site( $site );
21703 }
21704 $this->_update_site_license( $premium_license );
21705
21706 $this->_store_account();
21707
21708 if ( $this->is_addon() || $this->has_addons() ) {
21709 /**
21710 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21711 * an updated valid user licenses collection will be fetched from the server which is used to also
21712 * update the account add-ons (add-ons the user has licenses for).
21713 *
21714 * @author Leo Fajardo (@leorw)
21715 * @since 2.2.4
21716 */
21717 $this->purge_valid_user_licenses_cache();
21718 }
21719
21720 if ( ! $background ) {
21721 $this->add_complete_upgrade_instructions_notice(
21722 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ),
21723 'license_activated'
21724 );
21725 }
21726
21727 $this->_admin_notices->remove_sticky( array(
21728 'trial_promotion',
21729 'license_expired',
21730 ) );
21731 }
21732
21733 /**
21734 * @author Vova Feldman (@svovaf)
21735 * @since 1.0.5
21736 *
21737 * @param bool $show_notice
21738 */
21739 protected function _deactivate_license( $show_notice = true ) {
21740 $this->_logger->entrance();
21741
21742 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21743
21744 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
21745 $this->_admin_notices->add(
21746 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() ),
21747 $hmm_text
21748 );
21749
21750 return;
21751 }
21752
21753 $api = $this->get_api_site_scope();
21754 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
21755
21756 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
21757 }
21758
21759 /**
21760 * @author Leo Fajardo (@leorw)
21761 * @since 2.2.1
21762 *
21763 * @param FS_Plugin_License $license
21764 * @param bool|string $hmm_text
21765 * @param bool $show_notice
21766 */
21767 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
21768 if ( isset( $license->error ) ) {
21769 $this->_admin_notices->add(
21770 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
21771 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
21772 $hmm_text,
21773 'error'
21774 );
21775
21776 $this->do_action( 'after_license_deactivation', $license );
21777
21778 return;
21779 }
21780
21781 // Update license cache.
21782 if ( is_array( $this->_licenses ) ) {
21783 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
21784 if ( $license->id == $this->_licenses[ $i ]->id ) {
21785 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
21786 }
21787 }
21788 }
21789
21790 // Update site plan to default.
21791 $this->_sync_plans();
21792 $this->_site->plan_id = $this->_plans[0]->id;
21793 // Unlink license from site.
21794 $this->_update_site_license( null );
21795
21796 $this->_store_account();
21797
21798 $this->do_action( 'after_license_deactivation', $license );
21799
21800 if ( $show_notice ) {
21801 $this->_admin_notices->add(
21802 sprintf( $this->is_only_premium() ?
21803 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
21804 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
21805 $this->get_plan_title()
21806 ),
21807 $this->get_text_inline( 'O.K', 'ok' )
21808 );
21809 }
21810
21811 $this->_admin_notices->remove_sticky( array(
21812 'plan_upgraded',
21813 'license_activated',
21814 ) );
21815 }
21816
21817 /**
21818 * Site plan downgrade.
21819 *
21820 * @author Vova Feldman (@svovaf)
21821 * @since 1.0.4
21822 *
21823 * @return object
21824 *
21825 * @uses FS_Api
21826 */
21827 private function _downgrade_site() {
21828 $this->_logger->entrance();
21829
21830 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
21831
21832 $api = $this->get_api_site_scope();
21833 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
21834
21835 $plan_downgraded = false;
21836 $plan = false;
21837 if ( $this->is_api_result_entity( $site ) ) {
21838 $prev_plan_id = $this->_site->plan_id;
21839
21840 // Update new site plan id.
21841 $this->_site->plan_id = $site->plan_id;
21842
21843 $plan = $this->get_plan();
21844 $subscription = $this->_sync_site_subscription( $this->_license );
21845
21846 // Plan downgraded if plan was changed or subscription was cancelled.
21847 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
21848 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
21849 } else {
21850 // handle different error cases.
21851 $this->handle_license_deactivation_result(
21852 $site,
21853 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
21854 );
21855 }
21856
21857 if ( ! $plan_downgraded ) {
21858 return (object) array(
21859 'error' => (object) array(
21860 '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' )
21861 )
21862 );
21863 }
21864
21865 // Remove previous sticky message about upgrade (if exist).
21866 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21867
21868 $this->_admin_notices->add(
21869 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
21870 $plan->title,
21871 human_time_diff( time(), strtotime( $this->_license->expiration ) )
21872 )
21873 );
21874
21875 // Store site updates.
21876 $this->_store_site();
21877
21878 if ( $deactivate_license &&
21879 ! FS_Plugin_License::is_valid_id( $site->license_id )
21880 ) {
21881 if ( $this->_site->is_localhost() ) {
21882 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
21883 } else {
21884 $this->_license->activated = max( 0, $this->_license->activated - 1 );
21885 }
21886
21887 // Handle successful license deactivation result.
21888 $this->handle_license_deactivation_result( $this->_license );
21889 }
21890
21891 return $site;
21892 }
21893
21894 /**
21895 * @author Vova Feldman (@svovaf)
21896 * @since 1.1.8.1
21897 *
21898 * @param bool|string $plan_name
21899 * @param bool $add_sticky_notice
21900 *
21901 * @return bool If trial was successfully started.
21902 */
21903 function start_trial( $plan_name = false, $add_sticky_notice = false ) {
21904 $this->_logger->entrance();
21905
21906 // Alias.
21907 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
21908
21909 if ( $this->is_trial() ) {
21910 // Already in trial mode.
21911 $this->_admin_notices->add(
21912 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
21913 $oops_text,
21914 'error',
21915 $add_sticky_notice
21916 );
21917
21918 return false;
21919 }
21920
21921 if ( $this->_site->is_trial_utilized() && ! $this->is_payments_sandbox() ) {
21922 // Trial was already utilized.
21923 $this->_admin_notices->add(
21924 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
21925 $oops_text,
21926 'error',
21927 $add_sticky_notice
21928 );
21929
21930 return false;
21931 }
21932
21933 if ( false !== $plan_name ) {
21934 $plan = $this->get_plan_by_name( $plan_name );
21935
21936 if ( false === $plan ) {
21937 // Plan doesn't exist.
21938 $this->_admin_notices->add(
21939 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
21940 $oops_text,
21941 'error',
21942 $add_sticky_notice
21943 );
21944
21945 return false;
21946 }
21947
21948 if ( ! $plan->has_trial() ) {
21949 // Plan doesn't exist.
21950 $this->_admin_notices->add(
21951 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
21952 $oops_text,
21953 'error',
21954 $add_sticky_notice
21955 );
21956
21957 return false;
21958 }
21959 } else {
21960 if ( ! $this->has_trial_plan() ) {
21961 // None of the plans have a trial.
21962 $this->_admin_notices->add(
21963 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
21964 $oops_text,
21965 'error',
21966 $add_sticky_notice
21967 );
21968
21969 return false;
21970 }
21971
21972 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
21973
21974 $plan = $plans_with_trial[0];
21975 }
21976
21977 $trial_params = array();
21978
21979 if ( $this->is_payments_sandbox() ) {
21980 $trial_params['trial_timestamp'] = time();
21981 $trial_params['trial_token'] = FS_Security::instance()->get_trial_token(
21982 $this->get_plugin(),
21983 $plan,
21984 $trial_params['trial_timestamp']
21985 );
21986 }
21987
21988 $api = $this->get_api_site_scope();
21989 $trial = $api->call( "plans/{$plan->id}/trials.json", 'post', $trial_params );
21990
21991 if ( ! $this->is_api_result_entity( $trial ) ) {
21992 // Some API error while trying to start the trial.
21993 $this->_admin_notices->add(
21994 $this->get_api_error_message( $trial ),
21995 $oops_text,
21996 'error',
21997 $add_sticky_notice
21998 );
21999
22000 return false;
22001 }
22002
22003 // Sync license.
22004 $this->_sync_license();
22005
22006 return $this->is_trial();
22007 }
22008
22009 /**
22010 * Cancel site trial.
22011 *
22012 * @author Vova Feldman (@svovaf)
22013 * @since 1.0.9
22014 *
22015 * @return object
22016 *
22017 * @uses FS_Api
22018 */
22019 private function _cancel_trial() {
22020 $this->_logger->entrance();
22021
22022 if ( ! $this->is_trial() ) {
22023 return (object) array(
22024 'error' => (object) array(
22025 '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' )
22026 )
22027 );
22028 }
22029
22030 $trial_plan = $this->get_trial_plan();
22031
22032 $api = $this->get_api_site_scope();
22033 $site = $api->call( 'trials.json', 'delete' );
22034
22035 $trial_cancelled = false;
22036
22037 if ( $this->is_api_result_entity( $site ) ) {
22038 $prev_trial_ends = $this->_site->trial_ends;
22039
22040 if ( $this->is_paid_trial() ) {
22041 $this->_license->expiration = $site->trial_ends;
22042 $this->_license->is_cancelled = true;
22043 $this->_update_site_license( $this->_license );
22044 $this->_store_licenses();
22045
22046 // Clear subscription reference.
22047 $this->_sync_site_subscription( null );
22048 }
22049
22050 // Update site info.
22051 $this->_site = new FS_Site( $site );
22052
22053 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
22054 } else {
22055 // @todo handle different error cases.
22056 }
22057
22058 if ( ! $trial_cancelled ) {
22059 return (object) array(
22060 'error' => (object) array(
22061 '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' )
22062 )
22063 );
22064 }
22065
22066 // Remove previous sticky messages about upgrade or trial (if exist).
22067 $this->_admin_notices->remove_sticky( array(
22068 'trial_started',
22069 'trial_promotion',
22070 'plan_upgraded',
22071 ) );
22072
22073 // Store site updates.
22074 $this->_store_site();
22075
22076 if ( ! $this->is_addon() ||
22077 ! $this->deactivate_premium_only_addon_without_license( true )
22078 ) {
22079 $this->_admin_notices->add(
22080 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
22081 );
22082 }
22083
22084 return $site;
22085 }
22086
22087 /**
22088 * @author Vova Feldman (@svovaf)
22089 * @since 1.0.6
22090 *
22091 * @param bool|number $plugin_id
22092 *
22093 * @return bool
22094 */
22095 private function _is_addon_id( $plugin_id ) {
22096 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
22097 }
22098
22099 /**
22100 * Check if user eligible to download premium version updates.
22101 *
22102 * @author Vova Feldman (@svovaf)
22103 * @since 1.0.6
22104 *
22105 * @return bool
22106 */
22107 private function _can_download_premium() {
22108 return $this->has_any_active_valid_license() ||
22109 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
22110 }
22111
22112 /**
22113 *
22114 * @author Vova Feldman (@svovaf)
22115 * @since 1.0.6
22116 *
22117 * @param bool|number $addon_id
22118 * @param string $type "json" or "zip"
22119 *
22120 * @return string
22121 */
22122 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
22123
22124 $is_addon = $this->_is_addon_id( $addon_id );
22125
22126 $is_premium = null;
22127 if ( ! $is_addon ) {
22128 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
22129 } else if ( $this->is_addon_activated( $addon_id ) ) {
22130 $fs_addon = self::get_instance_by_id( $addon_id );
22131 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
22132 }
22133
22134 // If add-on, then append add-on ID.
22135 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
22136 '/updates/latest.' . $type;
22137
22138 // If add-on and not yet activated, try to fetch based on server licensing.
22139 if ( is_bool( $is_premium ) ) {
22140 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
22141 }
22142
22143 if ( $this->has_secret_key() ) {
22144 $endpoint = add_query_arg( 'type', 'all', $endpoint );
22145 } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) {
22146 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
22147 }
22148
22149 return $endpoint;
22150 }
22151
22152 /**
22153 * @author Vova Feldman (@svovaf)
22154 * @since 1.0.4
22155 *
22156 * @param bool|number $addon_id
22157 * @param bool $flush Since 1.1.7.3
22158 * @param int $expiration Since 1.2.2.7
22159 * @param bool|string $newer_than Since 2.2.1
22160 * @param bool|string $fetch_readme Since 2.2.1
22161 * @param bool $fetch_upgrade_notice Since 2.12.1
22162 *
22163 * @return object|false Plugin latest tag info.
22164 */
22165 function _fetch_latest_version(
22166 $addon_id = false,
22167 $flush = true,
22168 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22169 $newer_than = false,
22170 $fetch_readme = true,
22171 $fetch_upgrade_notice = false
22172 ) {
22173 $this->_logger->entrance();
22174
22175 if ( $this->is_unresolved_clone( true ) ) {
22176 return false;
22177 }
22178
22179 $switch_to_blog_id = null;
22180
22181 /**
22182 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
22183 * @since 1.1.7.4 Also check updates for add-ons.
22184 */
22185 if (
22186 ( ! $this->is_registered() || ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed() ) &&
22187 ! $this->_is_addon_id( $addon_id )
22188 ) {
22189 if ( ! is_multisite() ) {
22190 return false;
22191 }
22192
22193 $installs_map = $this->get_blog_install_map();
22194
22195 foreach ( $installs_map as $blog_id => $install ) {
22196 if ( ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed( $blog_id ) ) {
22197 continue;
22198 }
22199
22200 /**
22201 * @var FS_Site $install
22202 */
22203 if ( $install->is_trial() ) {
22204 $switch_to_blog_id = $blog_id;
22205 break;
22206 }
22207
22208 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
22209 $license = $this->get_license_by_id( $install->license_id );
22210
22211 if ( is_object( $license ) && $license->is_features_enabled() ) {
22212 $switch_to_blog_id = $blog_id;
22213 break;
22214 }
22215 }
22216 }
22217
22218 if ( is_null( $switch_to_blog_id ) ) {
22219 return false;
22220 }
22221 }
22222
22223 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
22224 get_current_blog_id() :
22225 0;
22226
22227 if ( is_numeric( $switch_to_blog_id ) ) {
22228 $this->switch_to_blog( $switch_to_blog_id );
22229 }
22230
22231 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
22232
22233 if ( ! empty( $newer_than ) ) {
22234 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
22235 }
22236
22237 if ( true === $fetch_readme ) {
22238 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
22239
22240 // Don't cache the API response when fetching readme information.
22241 $expiration = null;
22242 }
22243
22244 if ( true === $fetch_upgrade_notice ) {
22245 $latest_version_endpoint = add_query_arg( 'include_upgrade_notice', 'true', $latest_version_endpoint );
22246 }
22247
22248 $tag = $this->get_api_site_or_plugin_scope()->get(
22249 $latest_version_endpoint,
22250 $flush,
22251 $expiration
22252 );
22253
22254 if ( is_numeric( $switch_to_blog_id ) ) {
22255 $this->switch_to_blog( $current_blog_id );
22256 }
22257
22258 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
22259
22260 $this->_logger->departure( 'Latest version ' . $latest_version );
22261
22262 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
22263 }
22264
22265 #----------------------------------------------------------------------------------
22266 #region Download Plugin
22267 #----------------------------------------------------------------------------------
22268
22269 /**
22270 * Download latest plugin version, based on plan.
22271 *
22272 * Not like _download_latest(), this will redirect the page
22273 * to secure download url to prevent dual download (from FS to WP server,
22274 * and then from WP server to the client / browser).
22275 *
22276 * @author Vova Feldman (@svovaf)
22277 * @since 1.0.9
22278 *
22279 * @param bool|number $plugin_id
22280 *
22281 * @uses FS_Api
22282 * @uses wp_redirect()
22283 */
22284 private function download_latest_directly( $plugin_id = false ) {
22285 $this->_logger->entrance();
22286
22287 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
22288 }
22289
22290 /**
22291 * Get latest plugin FS API download URL.
22292 *
22293 * @author Vova Feldman (@svovaf)
22294 * @since 1.0.9
22295 *
22296 * @param bool|number $plugin_id
22297 *
22298 * @return string
22299 */
22300 private function get_latest_download_api_url( $plugin_id = false ) {
22301 $this->_logger->entrance();
22302
22303 $download_api_url = $this->get_api_site_scope()->get_signed_url(
22304 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
22305 );
22306
22307 return str_replace( 'http:', 'https:', $download_api_url );
22308 }
22309
22310 /**
22311 * Get payment invoice URL.
22312 *
22313 * @author Vova Feldman (@svovaf)
22314 * @since 1.2.0
22315 *
22316 * @param bool|number $payment_id
22317 *
22318 * @return string
22319 */
22320 function _get_invoice_api_url( $payment_id = false ) {
22321 $this->_logger->entrance();
22322
22323 $url = $this->get_api_user_scope()->get_signed_url(
22324 "/payments/{$payment_id}/invoice.pdf"
22325 );
22326
22327 if ( ! fs_starts_with( $url, 'https://' ) ) {
22328 // Always use HTTPS for invoices.
22329 $url = 'https' . substr( $url, 4 );
22330 }
22331
22332 return $url;
22333 }
22334
22335 /**
22336 * Get latest plugin download link.
22337 *
22338 * @author Vova Feldman (@svovaf)
22339 * @since 1.0.9
22340 *
22341 * @param string $label
22342 * @param bool|number $plugin_id
22343 *
22344 * @return string
22345 */
22346 private function get_latest_download_link( $label, $plugin_id = false ) {
22347 return sprintf(
22348 '<a target="_blank" rel="noopener" href="%s">%s</a>',
22349 $this->_get_latest_download_local_url( $plugin_id ),
22350 $label
22351 );
22352 }
22353
22354 /**
22355 * Get latest plugin download local URL.
22356 *
22357 * @author Vova Feldman (@svovaf)
22358 * @since 1.0.9
22359 *
22360 * @param bool|number $plugin_id
22361 *
22362 * @return string
22363 */
22364 function _get_latest_download_local_url( $plugin_id = false ) {
22365 // Add timestamp to protect from caching.
22366 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
22367
22368 if ( ! empty( $plugin_id ) ) {
22369 $params['plugin_id'] = $plugin_id;
22370 } else if ( $this->is_addon() ) {
22371 $params['plugin_id'] = $this->get_id();
22372 }
22373
22374 $fs = $this->is_addon() ?
22375 $this->get_parent_instance() :
22376 $this;
22377
22378 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
22379 }
22380
22381 #endregion Download Plugin ------------------------------------------------------------------
22382
22383 /**
22384 * @author Vova Feldman (@svovaf)
22385 * @since 1.0.4
22386 *
22387 * @uses FS_Api
22388 *
22389 * @param bool $background Hints the method if it's a background updates check. If false, it means that
22390 * was initiated by the admin.
22391 * @param bool|number $plugin_id
22392 * @param bool $flush Since 1.1.7.3
22393 */
22394 private function check_updates( $background = false, $plugin_id = false, $flush = true ) {
22395 $this->_logger->entrance();
22396
22397 $newer_than = ( $this->is_premium() ? $this->get_plugin_version() : false );
22398
22399 // Check if there's a newer version for download.
22400 $new_version = $this->_fetch_newer_version(
22401 $plugin_id,
22402 $flush,
22403 FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION,
22404 $newer_than,
22405 ( false !== $newer_than )
22406 );
22407
22408 $update = null;
22409 if ( is_object( $new_version ) ) {
22410 $update = new FS_Plugin_Tag( $new_version );
22411
22412 if ( ! $background ) {
22413 $this->_admin_notices->add(
22414 sprintf(
22415 /* translators: %s: Numeric version number (e.g. '2.1.9' */
22416 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
22417 $update->version,
22418 sprintf(
22419 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22420 $this->get_account_url( 'download_latest' ),
22421 sprintf(
22422 /* translators: %s: plan name (e.g. latest "Professional" version) */
22423 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
22424 $this->get_plan_title()
22425 )
22426 )
22427 ),
22428 $this->get_text_inline( 'New', 'new' ) . '!'
22429 );
22430 }
22431 } else if ( false === $new_version && ! $background ) {
22432 $this->_admin_notices->add(
22433 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
22434 $this->get_text_inline( 'You are all good!', 'you-are-good' )
22435 );
22436 }
22437
22438 $this->_store_update( $update, true, $plugin_id );
22439 }
22440
22441 /**
22442 * @author Vova Feldman (@svovaf)
22443 * @since 1.0.4
22444 *
22445 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
22446 *
22447 * @return FS_Plugin[]
22448 *
22449 * @uses FS_Api
22450 */
22451 private function sync_addons( $flush = false ) {
22452 $this->_logger->entrance();
22453
22454 $api = $this->get_api_site_or_plugin_scope();
22455
22456 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
22457
22458 /**
22459 * @since 1.2.1
22460 *
22461 * If there's a cached version of the add-ons and not asking
22462 * for a flush, just use the currently stored add-ons.
22463 */
22464 if ( ! $flush && $api->is_cached( $path ) ) {
22465 $addons = self::get_all_addons();
22466
22467 return isset( $addons[ $this->_plugin->id ] ) ?
22468 $addons[ $this->_plugin->id ] :
22469 array();
22470 }
22471
22472 $result = $api->get( $path, $flush );
22473
22474 $addons = array();
22475 if ( $this->is_api_result_object( $result, 'plugins' ) &&
22476 is_array( $result->plugins )
22477 ) {
22478 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
22479 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
22480 }
22481
22482 $this->_store_addons( $addons, true );
22483 }
22484
22485 return $addons;
22486 }
22487
22488 /**
22489 * Handle user email update.
22490 *
22491 * @author Vova Feldman (@svovaf)
22492 * @since 1.0.3
22493 * @uses FS_Api
22494 *
22495 * @param string $new_email
22496 *
22497 * @return object
22498 */
22499 private function update_email( $new_email ) {
22500 $this->_logger->entrance();
22501
22502 $api = $this->get_api_user_scope();
22503 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
22504 'email' => $new_email,
22505 'after_email_confirm_url' => $this->_get_admin_page_url(
22506 'account',
22507 array( 'fs_action' => 'sync_user' )
22508 ),
22509 ) );
22510
22511 if ( ! isset( $user->error ) ) {
22512 $this->_user->email = $user->email;
22513 $this->_user->is_verified = $user->is_verified;
22514 $this->_store_user();
22515 } else {
22516 // handle different error cases.
22517 }
22518
22519 return $user;
22520 }
22521
22522 #----------------------------------------------------------------------------------
22523 #region API Error Handling
22524 #----------------------------------------------------------------------------------
22525
22526 /**
22527 * @author Vova Feldman (@svovaf)
22528 * @since 1.1.1
22529 *
22530 * @param mixed $result
22531 *
22532 * @return bool Is API result contains an error.
22533 */
22534 private function is_api_error( $result ) {
22535 return FS_Api::is_api_error( $result );
22536 }
22537
22538 /**
22539 * Checks if given API result is a non-empty and not an error object.
22540 *
22541 * @author Vova Feldman (@svovaf)
22542 * @since 1.2.1.5
22543 *
22544 * @param mixed $result
22545 * @param string|null $required_property Optional property we want to verify that is set.
22546 *
22547 * @return bool
22548 */
22549 function is_api_result_object( $result, $required_property = null ) {
22550 return FS_Api::is_api_result_object( $result, $required_property );
22551 }
22552
22553 /**
22554 * Checks if given API result is a non-empty entity object with non-empty ID.
22555 *
22556 * @author Vova Feldman (@svovaf)
22557 * @since 1.2.1.5
22558 *
22559 * @param mixed $result
22560 *
22561 * @return bool
22562 */
22563 private function is_api_result_entity( $result ) {
22564 return FS_Api::is_api_result_entity( $result );
22565 }
22566
22567 #endregion
22568
22569 /**
22570 * Make sure a given argument is an array of a specific type.
22571 *
22572 * @author Vova Feldman (@svovaf)
22573 * @since 1.2.1.5
22574 *
22575 * @param mixed $array
22576 * @param string $class
22577 *
22578 * @return bool
22579 */
22580 private function is_array_instanceof( $array, $class ) {
22581 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
22582 }
22583
22584 /**
22585 * Start install ownership change.
22586 *
22587 * @author Vova Feldman (@svovaf)
22588 * @since 1.1.1
22589 * @uses FS_Api
22590 *
22591 * @param string $candidate_email
22592 * @param string $transfer_type
22593 *
22594 * @return bool Is ownership change successfully initiated.
22595 */
22596 private function init_change_owner( $candidate_email, $transfer_type ) {
22597 $this->_logger->entrance();
22598
22599 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
22600 $install_ids = array();
22601
22602 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
22603 $install = $install_info['install'];
22604
22605 if ( $this->_user->id != $install->user_id ) {
22606 // Skip add-on installs that are not owned by the parent product's install's owner.
22607 continue;
22608 }
22609
22610 $install_ids[ $slug ] = $install->id;
22611 }
22612
22613 $api = $this->get_api_site_scope();
22614 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
22615 'email' => $candidate_email,
22616 'transfer_type' => $transfer_type,
22617 'install_ids' => implode( ',', array_values( $install_ids ) ),
22618 'after_confirm_url' => $this->_get_admin_page_url(
22619 'account',
22620 array( 'fs_action' => 'change_owner' )
22621 ),
22622 ) );
22623
22624 return ! $this->is_api_error( $result );
22625 }
22626
22627 /**
22628 * Handle install ownership change.
22629 *
22630 * @author Vova Feldman (@svovaf)
22631 * @since 1.1.1
22632 * @uses FS_Api
22633 *
22634 * @return bool Was ownership change successfully complete.
22635 */
22636 private function complete_change_owner() {
22637 $this->_logger->entrance();
22638
22639 $install_ids = fs_request_get( 'install_ids' );
22640
22641 if ( ! empty( $install_ids ) ) {
22642 $install_ids = explode( ',', $install_ids );
22643
22644 foreach ( $install_ids as $key => $install_id ) {
22645 if ( ! FS_Site::is_valid_id( $install_id ) ) {
22646 unset( $install_ids[ $key ] );
22647 }
22648 }
22649 }
22650
22651 if ( ! is_array( $install_ids ) ) {
22652 $install_ids = array();
22653 }
22654
22655 $user = new FS_User();
22656 $user->id = fs_request_get( 'user_id' );
22657 $user->public_key = fs_request_get_raw( 'user_public_key' );
22658 $user->secret_key = fs_request_get_raw( 'user_secret_key' );
22659
22660 $prev_user = $this->_user;
22661 $this->_user = $user;
22662
22663 $result = $this->get_api_user_scope( true )->get(
22664 "/installs.json?install_ids=" . implode( ',', $install_ids )
22665 );
22666
22667 $current_blog_sites = self::get_all_sites( $this->get_module_type() );
22668
22669 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22670 $site_id_slug_map = array();
22671
22672 foreach ( $current_blog_sites as $slug => $site ) {
22673 $site_id_slug_map[ $site->id ] = $slug;
22674 }
22675
22676 foreach ( $result->installs as $install ) {
22677 $site = new FS_Site( $install );
22678
22679 if ( ! isset( $site_id_slug_map[ $install->id ] ) ) {
22680 continue;
22681 }
22682
22683 $current_blog_sites[ $site_id_slug_map[ $install->id ] ] = clone $site;
22684
22685 if ( $this->_site->id == $site->id ) {
22686 $this->_site = $site;
22687 }
22688 }
22689 }
22690
22691 // Validate install's user and given user.
22692 if ( $user->id != $this->_site->user_id ) {
22693 $this->_user = $prev_user;
22694
22695 return false;
22696 }
22697
22698 $this->set_account_option( 'sites', $current_blog_sites, true );
22699
22700 // Fetch new user information.
22701 $user_result = $this->get_api_user_scope( true )->get();
22702 $user = new FS_User( $user_result );
22703 $this->_user = $user;
22704
22705 $this->_set_account( $user, $this->_site );
22706
22707 $remove_user = true;
22708 $all_modules_sites = FS_DebugManager::get_all_modules_sites();
22709
22710 foreach ( $all_modules_sites as $sites_by_module_type ) {
22711 foreach ( $sites_by_module_type as $sites_by_slug ) {
22712 foreach ( $sites_by_slug as $site ) {
22713 if ( $prev_user->id == $site->user_id ) {
22714 $remove_user = false;
22715 break;
22716 }
22717 }
22718
22719 if ( ! $remove_user ) {
22720 break;
22721 }
22722 }
22723
22724 if ( ! $remove_user ) {
22725 break;
22726 }
22727 }
22728
22729 if ( $remove_user ) {
22730 $users = self::get_all_users();
22731
22732 if ( isset( $users[ $prev_user->id ] ) ) {
22733 unset( $users[ $prev_user->id ] );
22734 } else {
22735 // If the prev user wasn't found by the key, iterate over the users collection.
22736 foreach ( $users as $key => $user ) {
22737 if ( $user->id == $prev_user->id ) {
22738 unset( $users[ $key ] );
22739 break;
22740 }
22741 }
22742 }
22743
22744 $this->set_account_option( 'users', $users, true );
22745 }
22746
22747 return true;
22748 }
22749
22750 /**
22751 * Completes ownership change by license.
22752 *
22753 * @author Leo Fajardo (@leorw)
22754 * @since 2.3.2
22755 *
22756 * @param number $user_id
22757 * @param array[string]number $install_ids_by_slug_map
22758 *
22759 */
22760 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
22761 $this->_logger->entrance();
22762
22763 $this->sync_user_by_current_install( $user_id );
22764
22765 $result = $this->get_api_user_scope( true )->get(
22766 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
22767 );
22768
22769 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22770 $sites = self::get_all_sites( $this->get_module_type() );
22771 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
22772
22773 foreach ( $result->installs as $install ) {
22774 $site = new FS_Site( $install );
22775
22776 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
22777 }
22778
22779 $this->set_account_option( 'sites', $sites, true );
22780 }
22781 }
22782
22783 /**
22784 * Handle user name update.
22785 *
22786 * @author Vova Feldman (@svovaf)
22787 * @since 1.0.9
22788 * @uses FS_Api
22789 *
22790 * @return object
22791 */
22792 private function update_user_name() {
22793 $this->_logger->entrance();
22794 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
22795
22796 $api = $this->get_api_user_scope();
22797 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
22798 'name' => $name,
22799 ) );
22800
22801 if ( ! isset( $user->error ) ) {
22802 $this->_user->first = $user->first;
22803 $this->_user->last = $user->last;
22804 $this->_store_user();
22805 } else {
22806 // handle different error cases.
22807
22808 }
22809
22810 return $user;
22811 }
22812
22813 /**
22814 * Verify user email.
22815 *
22816 * @author Vova Feldman (@svovaf)
22817 * @since 1.0.3
22818 * @uses FS_Api
22819 */
22820 private function verify_email() {
22821 $this->_handle_account_user_sync();
22822
22823 if ( $this->_user->is_verified() ) {
22824 return;
22825 }
22826
22827 $api = $this->get_api_site_scope();
22828 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
22829 'after_email_confirm_url' => $this->_get_admin_page_url(
22830 'account',
22831 array( 'fs_action' => 'sync_user' )
22832 )
22833 ) );
22834
22835 if ( ! isset( $result->error ) ) {
22836 $this->_admin_notices->add( sprintf(
22837 $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' ),
22838 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
22839 ) );
22840 } else {
22841 // handle different error cases.
22842
22843 }
22844 }
22845
22846 /**
22847 * @author Vova Feldman (@svovaf)
22848 * @since 1.1.2
22849 *
22850 * @param array $params
22851 * @param bool|null $network
22852 *
22853 * @return string
22854 */
22855 function get_activation_url( $params = array(), $network = null ) {
22856 if ( $this->is_addon() && $this->has_free_plan() ) {
22857 /**
22858 * @author Vova Feldman (@svovaf)
22859 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
22860 */
22861 return $this->get_parent_instance()->get_activation_url( $params );
22862 }
22863
22864 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
22865 }
22866
22867 /**
22868 * @author Vova Feldman (@svovaf)
22869 * @since 1.2.1.5
22870 *
22871 * @param array $params
22872 *
22873 * @return string
22874 */
22875 function get_reconnect_url( $params = array() ) {
22876 $params['fs_action'] = 'reset_anonymous_mode';
22877 $params['fs_unique_affix'] = $this->get_unique_affix();
22878
22879 return $this->get_activation_url( $params );
22880 }
22881
22882 /**
22883 * Get the URL of the page that should be loaded after the user connect
22884 * or skip in the opt-in screen.
22885 *
22886 * @author Vova Feldman (@svovaf)
22887 * @since 1.1.3
22888 *
22889 * @param string $filter Filter name.
22890 * @param array $params Since 1.2.2.7
22891 * @param bool|null $network
22892 *
22893 * @return string
22894 */
22895 function get_after_activation_url( $filter, $params = array(), $network = null ) {
22896 if ( $this->show_opt_in_on_themes_page() &&
22897 ( fs_request_has( 'pending_activation' ) ||
22898 // For cases when the first time path is set, even though it's a WP.org theme.
22899 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
22900 ) {
22901 $first_time_path = '';
22902 } else {
22903 $first_time_path = $this->_menu->get_first_time_path(
22904 fs_is_network_admin() && $this->_is_network_active
22905 );
22906 }
22907
22908 if ( $this->_is_network_active &&
22909 fs_is_network_admin() &&
22910 ! $this->_menu->has_network_menu() &&
22911 $this->is_network_registered()
22912 ) {
22913 $target_url = $this->get_account_url();
22914 } else {
22915 // Default plugin's page.
22916 $target_url = $this->_get_admin_page_url( '', array(), $network );
22917 }
22918
22919 return add_query_arg( $params, $this->apply_filters(
22920 $filter,
22921 empty( $first_time_path ) ?
22922 $target_url :
22923 $first_time_path
22924 ) );
22925 }
22926
22927 /**
22928 * Handle account page updates / edits / actions.
22929 *
22930 * @author Vova Feldman (@svovaf)
22931 * @since 1.0.2
22932 *
22933 */
22934 private function _handle_account_edits() {
22935 if ( ! $this->is_user_admin() ) {
22936 return;
22937 }
22938
22939 $action = fs_get_action();
22940
22941 if ( empty( $action ) ) {
22942 return;
22943 }
22944
22945 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
22946 $install_id = fs_request_get( 'install_id', '' );
22947
22948 // Alias.
22949 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
22950
22951 $is_network_action = $this->is_network_level_action();
22952 $blog_id = $this->is_network_level_site_specific_action();
22953 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
22954
22955 if ( is_numeric( $blog_id ) ) {
22956 $this->switch_to_blog( $blog_id );
22957 } else {
22958 $blog_id = '';
22959 }
22960
22961 switch ( $action ) {
22962 case 'opt_in':
22963 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22964
22965 if ( $is_parent_plugin_action ) {
22966 if ( $is_network_action && ! empty( $blog_id ) ) {
22967 if ( ! $this->is_registered() ) {
22968 $this->install_with_user(
22969 $this->get_network_user(),
22970 false,
22971 false,
22972 false,
22973 false
22974 );
22975
22976 $this->_admin_notices->add(
22977 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
22978 $this->get_text_inline( 'Awesome', 'awesome' )
22979 );
22980 }
22981 }
22982 }
22983 break;
22984
22985 case 'toggle_tracking':
22986 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22987
22988 if ( $is_parent_plugin_action ) {
22989 if ( $is_network_action && ! empty( $blog_id ) ) {
22990 if ( $this->is_registered( true ) ) {
22991 if ( $this->is_tracking_prohibited( $blog_id ) ) {
22992 if ( $this->toggle_site_tracking( true, $blog_id ) ) {
22993 $this->_admin_notices->add(
22994 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>" ),
22995 $this->get_text_inline( 'Thank you!', 'thank-you' )
22996 );
22997 }
22998 } else {
22999 if ( $this->toggle_site_tracking( false, $blog_id ) ) {
23000 $install = $this->get_install_by_blog_id( $blog_id );
23001
23002 $this->_admin_notices->add(
23003 sprintf(
23004 $this->get_text_inline( 'Diagnostic data will no longer be sent from %s to %s.', 'opted-out-successfully' ),
23005 self::get_unfiltered_site_url( $blog_id, true ),
23006 "<b>{$this->get_plugin_title()}</b>"
23007 )
23008 );
23009 }
23010 }
23011 }
23012 }
23013 }
23014
23015 break;
23016
23017 case 'delete_account':
23018 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23019
23020 $is_network_deletion = $is_network_action && empty( $blog_id );
23021
23022 if ( $is_parent_plugin_action ) {
23023 // Delete add-on installs if have any.
23024 $installed_addons = $this->get_installed_addons();
23025 foreach ( $installed_addons as $fs_addon ) {
23026 if ( $is_network_deletion ) {
23027 $fs_addon->delete_network_account_event();
23028 } else {
23029 $fs_addon->delete_account_event();
23030 }
23031 }
23032
23033 if ( $is_network_deletion ) {
23034 $this->delete_network_account_event();
23035 } else {
23036 $this->delete_account_event();
23037 }
23038
23039 // Clear user and site.
23040 $this->_site = null;
23041 $this->_user = null;
23042
23043 $this->maybe_set_slug_and_network_menu_exists_flag();
23044
23045 fs_redirect( $this->get_activation_url() );
23046 } else {
23047 if ( $this->is_addon_activated( $plugin_id ) ) {
23048 $fs_addon = self::get_instance_by_id( $plugin_id );
23049
23050 if ( $is_network_deletion ) {
23051 $fs_addon->delete_network_account_event();
23052 } else {
23053 $fs_addon->delete_account_event();
23054 }
23055
23056 fs_redirect( $this->_get_admin_page_url( 'account' ) );
23057 }
23058 }
23059
23060 return;
23061
23062 case 'downgrade_account':
23063 if ( is_numeric( $blog_id ) ) {
23064 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23065 } else {
23066 check_admin_referer( $action );
23067 }
23068
23069 $switch_to_network_install_blog_after_cancellation = (
23070 is_numeric( $blog_id ) &&
23071 $plugin_id == $this->get_id() &&
23072 ! $this->is_trial()
23073 );
23074
23075 $result = $this->cancel_subscription_or_trial( $plugin_id );
23076 if ( $this->is_api_error( $result ) ) {
23077 $this->_admin_notices->add(
23078 $result->error->message,
23079 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23080 'error'
23081 );
23082 }
23083
23084 if ( $switch_to_network_install_blog_after_cancellation ) {
23085 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23086 }
23087
23088 return;
23089
23090 case 'activate_license':
23091 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23092
23093 $fs = $this;
23094 if ( $plugin_id != $this->get_id() ) {
23095 $fs = $this->is_addon_activated( $plugin_id ) ?
23096 self::get_instance_by_id( $plugin_id ) :
23097 null;
23098 }
23099
23100 if ( is_object( $fs ) ) {
23101 $fs->_activate_license();
23102
23103 /**
23104 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
23105 *
23106 * @author Leo Fajardo (@leorw)
23107 * @since 2.4.0
23108 */
23109 unset( $_REQUEST['plugin_id'] );
23110
23111 if ( $this->is_bundle_license_auto_activation_enabled() ) {
23112 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
23113 }
23114 }
23115
23116 return;
23117
23118 case 'deactivate_license':
23119 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23120
23121 if ( $plugin_id == $this->get_id() ) {
23122 $this->_deactivate_license();
23123
23124 if ( $this->is_only_premium() ) {
23125 // Clear user and site.
23126 $this->_site = null;
23127 $this->_user = null;
23128
23129 if ( ! $is_network_action ) {
23130 fs_redirect( $this->get_activation_url() );
23131 } else if ( is_numeric( $blog_id ) ) {
23132 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23133 }
23134 }
23135 } else {
23136 if ( $this->is_addon_activated( $plugin_id ) ) {
23137 $fs_addon = self::get_instance_by_id( $plugin_id );
23138 $fs_addon->_deactivate_license();
23139 }
23140 }
23141
23142 return;
23143
23144 case 'check_updates':
23145 check_admin_referer( $action );
23146 $this->check_updates();
23147
23148 return;
23149
23150 case 'change_owner':
23151 $state = fs_request_get( 'state', 'init' );
23152 switch ( $state ) {
23153 case 'init':
23154 // The nonce is injected by the error handler in `_email_address_update_ajax_handler` function.
23155 check_admin_referer( 'change_owner' );
23156
23157 $candidate_email = fs_request_get( 'candidate_email' );
23158 $transfer_type = fs_request_get( 'transfer_type' );
23159
23160 if ( $this->init_change_owner( $candidate_email, $transfer_type ) ) {
23161 if ( 'transfer' === $transfer_type ) {
23162 $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>' ) );
23163 } else {
23164 $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>' ) );
23165 }
23166 }
23167 break;
23168 case 'owner_confirmed':
23169 // We cannot (or need not to) check the nonce and referer here, because the link comes from the email sent by our API.
23170 $candidate_email = fs_request_get( 'candidate_email', '' );
23171
23172 if ( ! is_email($candidate_email ) ) {
23173 return;
23174 }
23175
23176 $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>' ) );
23177 break;
23178 case 'candidate_confirmed':
23179 // We do not need to validate the authenticity of this request here, because the `complete_change_owner` does that for us through API calls.
23180 if ( $this->complete_change_owner() ) {
23181 $this->_admin_notices->add_sticky(
23182 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
23183 'ownership_changed',
23184 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
23185 );
23186 } else {
23187 // @todo Handle failed ownership change message.
23188 }
23189 break;
23190 }
23191
23192 return;
23193
23194 case 'update_user_name':
23195 check_admin_referer( 'update_user_name' );
23196
23197 $result = $this->update_user_name();
23198
23199 if ( isset( $result->error ) ) {
23200 $this->_admin_notices->add(
23201 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
23202 $oops_text,
23203 'error'
23204 );
23205 } else {
23206 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
23207 }
23208
23209 return;
23210
23211 #region Actions that might be called from external links (e.g. email)
23212
23213 /**
23214 * !!IMPORTANT!!: We cannot check for a valid nonce in this region, because the links could be coming from emails.
23215 */
23216
23217 case 'cancel_trial':
23218 $result = $this->cancel_subscription_or_trial( $plugin_id );
23219 if ( $this->is_api_error( $result ) ) {
23220 $this->_admin_notices->add(
23221 $result->error->message,
23222 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23223 'error'
23224 );
23225 }
23226
23227 return;
23228
23229 case 'verify_email':
23230 $this->verify_email();
23231
23232 return;
23233
23234 case 'sync_user':
23235 $this->_handle_account_user_sync();
23236
23237 return;
23238
23239 case $this->get_unique_affix() . '_sync_license':
23240 $this->_sync_license();
23241
23242 return;
23243
23244 case 'download_latest':
23245 $this->download_latest_directly( $plugin_id );
23246
23247 return;
23248
23249 #endregion
23250 }
23251
23252 if ( WP_FS__IS_POST_REQUEST ) {
23253 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
23254 foreach ( $properties as $p ) {
23255 if ( 'update_' . $p === $action ) {
23256 check_admin_referer( $action );
23257
23258 $this->_logger->log( $action );
23259
23260 $site_property = substr( $p, strlen( 'site_' ) );
23261 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
23262 $this->get_site()->{$site_property} = $site_property_value;
23263
23264 // Store account after modification.
23265 $this->_store_site();
23266
23267 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
23268
23269 $this->_admin_notices->add( sprintf(
23270 /* translators: %s: User's account property (e.g. email address, name) */
23271 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
23272 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
23273 ) );
23274
23275 return;
23276 }
23277 }
23278 }
23279 }
23280
23281 /**
23282 * Adds CSS classes for the body tag in the admin.
23283 *
23284 * @param string $classes Space-separated string of class names.
23285 *
23286 * @return string $classes FS Admin body tag class names.
23287 */
23288 public function fs_addons_body_class( $classes ) {
23289 $classes .= ' plugins-php';
23290 return $classes;
23291 }
23292
23293 /**
23294 * Account page resources load.
23295 *
23296 * @author Vova Feldman (@svovaf)
23297 * @since 1.0.6
23298 */
23299 function _account_page_load() {
23300 $this->_logger->entrance();
23301
23302 $this->_logger->info( var_export( $_REQUEST, true ) );
23303
23304 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
23305
23306 if ( $this->has_addons() ) {
23307 wp_enqueue_script( 'plugin-install' );
23308 add_thickbox();
23309 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23310 }
23311
23312 if ( $this->has_paid_plan() &&
23313 ! $this->has_any_license() &&
23314 ! $this->is_sync_executed() &&
23315 $this->is_tracking_allowed()
23316 ) {
23317 /**
23318 * If no licenses found and no sync job was executed during the last 24 hours,
23319 * just execute the sync job right away (blocking execution).
23320 *
23321 * @since 1.1.7.3
23322 */
23323 $this->run_manual_sync();
23324 }
23325
23326 $this->_handle_account_edits();
23327
23328 if (
23329 is_object( $this->_license ) &&
23330 $this->_license->user_id == $this->_user->id &&
23331 ! $this->is_whitelabeled( true )
23332 ) {
23333 $this->_admin_notices->add(
23334 sprintf(
23335 $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' ),
23336 sprintf(
23337 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
23338 $this->get_text_inline( 'Click here', 'click-here' )
23339 )
23340 ),
23341 '',
23342 'success',
23343 false,
23344 'license_not_whitelabeled'
23345 );
23346 }
23347
23348 $this->do_action( 'account_page_load_before_departure' );
23349 }
23350
23351 /**
23352 * Renders the "Affiliation" page.
23353 *
23354 * @author Leo Fajardo (@leorw)
23355 * @since 1.2.3
23356 */
23357 function _affiliation_page_render() {
23358 $this->_logger->entrance();
23359
23360 $this->fetch_affiliate_and_terms();
23361
23362 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
23363
23364 $is_bundle_context = $this->has_bundle_context();
23365
23366 $plugin_title = $this->get_plugin_title();
23367
23368 if ( $is_bundle_context ) {
23369 $plugin_title = $this->plugin_affiliate_terms->plugin_title;
23370
23371 // Add the suffix "Bundle" only if the word is not present in the title itself.
23372 if ( false === mb_stripos( $plugin_title, fs_text_inline( 'Bundle', 'bundle' ) ) ) {
23373 $plugin_title = $this->apply_filters(
23374 'formatted_bundle_title',
23375 $plugin_title . ' ' . fs_text_inline( 'Bundle', 'bundle' )
23376 );
23377 }
23378 }
23379
23380 $vars = array(
23381 'id' => $this->_module_id,
23382 'plugin_title' => $plugin_title,
23383 );
23384 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
23385 }
23386
23387
23388 /**
23389 * Render account page.
23390 *
23391 * @author Vova Feldman (@svovaf)
23392 * @since 1.0.0
23393 */
23394 function _account_page_render() {
23395 $this->_logger->entrance();
23396
23397 $template = 'account.php';
23398 $vars = array( 'id' => $this->_module_id );
23399
23400 /**
23401 * Added filter to the template to allow developers wrapping the template
23402 * in custom HTML (e.g. within a wizard/tabs).
23403 *
23404 * @author Vova Feldman (@svovaf)
23405 * @since 1.2.1.6
23406 */
23407 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
23408 }
23409
23410 /**
23411 * Render account connect page.
23412 *
23413 * @author Vova Feldman (@svovaf)
23414 * @since 1.0.7
23415 */
23416 function _connect_page_render() {
23417 $this->_logger->entrance();
23418
23419 $vars = array( 'id' => $this->_module_id );
23420
23421 /**
23422 * Added filter to the template to allow developers wrapping the template
23423 * in custom HTML (e.g. within a wizard/tabs).
23424 *
23425 * @author Vova Feldman (@svovaf)
23426 * @since 1.2.1.6
23427 */
23428 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
23429 }
23430
23431 /**
23432 * Load required resources before add-ons page render.
23433 *
23434 * @author Vova Feldman (@svovaf)
23435 * @since 1.0.6
23436 */
23437 function _addons_page_load() {
23438 $this->_logger->entrance();
23439
23440 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
23441
23442 wp_enqueue_script( 'plugin-install' );
23443 add_thickbox();
23444 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23445
23446 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
23447 $this->_admin_notices->add(
23448 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>' ),
23449 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
23450 'update-nag'
23451 );
23452 }
23453 }
23454
23455 /**
23456 * Render add-ons page.
23457 *
23458 * @author Vova Feldman (@svovaf)
23459 * @since 1.0.6
23460 */
23461 function _addons_page_render() {
23462 $this->_logger->entrance();
23463
23464 $vars = array( 'id' => $this->_module_id );
23465
23466 /**
23467 * Added filter to the template to allow developers wrapping the template
23468 * in custom HTML (e.g. within a wizard/tabs).
23469 *
23470 * @author Vova Feldman (@svovaf)
23471 * @since 1.2.1.6
23472 */
23473 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
23474 }
23475
23476 /* Pricing & Upgrade
23477 ------------------------------------------------------------------------------------------------------------------*/
23478 /**
23479 * Render pricing page.
23480 *
23481 * @author Vova Feldman (@svovaf)
23482 * @since 1.0.0
23483 */
23484 function _pricing_page_render() {
23485 $this->_logger->entrance();
23486
23487 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
23488 fs_enqueue_local_style( 'fs_checkout', '/admin/checkout.css' );
23489
23490 $vars = array( 'id' => $this->_module_id );
23491
23492 if ( 'true' === fs_request_get( 'checkout', false ) ) {
23493 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
23494 } else {
23495 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
23496 }
23497 }
23498
23499 /**
23500 * @author Leo Fajardo (@leorw)
23501 * @since 2.3.1
23502 */
23503 function _add_pricing_ajax_handler() {
23504 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
23505 }
23506
23507 /**
23508 * @author Leo Fajardo (@leorw)
23509 * @since 2.3.1
23510 */
23511 function _fs_pricing_ajax_action_handler() {
23512 $this->check_ajax_referer( 'pricing_ajax_action' );
23513
23514 $result = null;
23515 $pricing_action = fs_request_get( 'pricing_action' );
23516
23517 switch ( $pricing_action ) {
23518 case 'fetch_pricing_data':
23519 $params = array(
23520 'is_enriched' => true,
23521 'trial' => fs_request_get_bool( 'trial' ),
23522 'sandbox' => fs_request_get_raw( 'sandbox' ),
23523 's_ctx_type' => fs_request_get_raw( 's_ctx_type' ),
23524 's_ctx_id' => fs_request_get_raw( 's_ctx_id' ),
23525 's_ctx_ts' => fs_request_get_raw( 's_ctx_ts' ),
23526 's_ctx_secure' => fs_request_get_raw( 's_ctx_secure' ),
23527 );
23528
23529 $bundle_id = $this->get_bundle_id();
23530 $bundle_public_key = $this->get_bundle_public_key();
23531
23532 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
23533
23534 if ( ! $has_bundle_context ) {
23535 $api = $this->get_api_plugin_scope();
23536 } else {
23537 $api = FS_Api::instance(
23538 $bundle_id,
23539 'plugin',
23540 $bundle_id,
23541 $bundle_public_key,
23542 ! $this->is_live(),
23543 false,
23544 $this->get_sdk_version()
23545 );
23546
23547 $params['plugin_id'] = $this->get_id();
23548 $params['plugin_public_key'] = $this->get_public_key();
23549 }
23550
23551 $result = $api->get( $this->add_show_pending( 'pricing.json?' . http_build_query( $params ) ) );
23552 break;
23553 case 'start_trial':
23554 $trial_plan_id = fs_request_get( 'plan_id' );
23555
23556 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
23557 $plan = $this->_get_plan_by_id( $trial_plan_id );
23558
23559 if ( ! $plan ) {
23560 $this->shoot_ajax_failure( 'Invalid plan ID.' );
23561 return;
23562 }
23563
23564 $result = $this->start_trial( $plan->name, true );
23565 } else {
23566 // @todo - This fails for sandbox trial at the moment if the trial was already utilized.
23567 $result = $this->opt_in(
23568 false,
23569 false,
23570 false,
23571 false,
23572 false,
23573 $trial_plan_id
23574 );
23575 }
23576 }
23577
23578 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
23579 $this->_logger->api_error( $result );
23580
23581 self::shoot_ajax_failure(
23582 isset( $result->error ) ?
23583 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
23584 var_export( $result, true )
23585 );
23586 }
23587
23588 $this->shoot_ajax_success( $result );
23589 }
23590
23591 #----------------------------------------------------------------------------------
23592 #region Contact Us
23593 #----------------------------------------------------------------------------------
23594
23595 /**
23596 * Render contact-us page.
23597 *
23598 * @author Vova Feldman (@svovaf)
23599 * @since 1.0.3
23600 */
23601 function _contact_page_render() {
23602 $this->_logger->entrance();
23603
23604 $vars = array( 'id' => $this->_module_id );
23605
23606 /**
23607 * Added filter to the template to allow developers wrapping the template
23608 * in custom HTML (e.g. within a wizard/tabs).
23609 *
23610 * @author Vova Feldman (@svovaf)
23611 * @since 2.1.3
23612 */
23613 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
23614 }
23615
23616 #endregion ------------------------------------------------------------------------
23617
23618 /**
23619 * Hide all admin notices to prevent distractions.
23620 *
23621 * @author Vova Feldman (@svovaf)
23622 * @since 1.0.3
23623 *
23624 * @uses remove_all_actions()
23625 */
23626 private static function _hide_admin_notices() {
23627 remove_all_actions( 'admin_notices' );
23628 remove_all_actions( 'network_admin_notices' );
23629 remove_all_actions( 'all_admin_notices' );
23630 remove_all_actions( 'user_admin_notices' );
23631 }
23632
23633 static function _clean_admin_content_section_hook() {
23634 $hide_admin_notices = true;
23635
23636 if ( fs_request_is_action( 'allow_clone_resolution_notice' ) ) {
23637 check_admin_referer( 'fs_allow_clone_resolution_notice' );
23638
23639 $hide_admin_notices = false;
23640 }
23641
23642 if ( $hide_admin_notices ) {
23643 self::_hide_admin_notices();
23644 }
23645
23646 // Hide footer.
23647 echo '<style>#wpfooter { display: none !important; }</style>';
23648 }
23649
23650 /**
23651 * Attach to admin_head hook to hide all admin notices.
23652 *
23653 * @author Vova Feldman (@svovaf)
23654 * @since 1.0.3
23655 */
23656 static function _clean_admin_content_section() {
23657 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
23658 }
23659
23660 /* CSS & JavaScript
23661 ------------------------------------------------------------------------------------------------------------------*/
23662 /* function _enqueue_script($handle, $src) {
23663 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
23664
23665 $this->_logger->entrance( 'script = ' . $url );
23666
23667 wp_enqueue_script( $handle, $url );
23668 }*/
23669
23670 /* SDK
23671 ------------------------------------------------------------------------------------------------------------------*/
23672 private $_user_api;
23673
23674 /**
23675 *
23676 * @author Vova Feldman (@svovaf)
23677 * @since 1.0.2
23678 *
23679 * @param bool $flush
23680 *
23681 * @return FS_Api
23682 */
23683 function get_api_user_scope( $flush = false ) {
23684 if ( ! isset( $this->_user_api ) || $flush ) {
23685 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
23686 }
23687
23688 return $this->_user_api;
23689 }
23690
23691 /**
23692 * @author Vova Feldman (@svovaf)
23693 * @since 2.0.0
23694 *
23695 * @param \FS_User $user
23696 *
23697 * @return \FS_Api
23698 */
23699 private function get_api_user_scope_by_user( FS_User $user ) {
23700 return FS_Api::instance(
23701 $this->_module_id,
23702 'user',
23703 $user->id,
23704 $user->public_key,
23705 ! $this->is_live(),
23706 $user->secret_key,
23707 $this->get_sdk_version()
23708 );
23709 }
23710
23711 /**
23712 *
23713 * @author Leo Fajardo (@leorw)
23714 * @since 2.0.0
23715 *
23716 * @param bool $flush
23717 *
23718 * @return FS_Api
23719 */
23720 private function get_current_or_network_user_api_scope( $flush = false ) {
23721 if ( ! $this->_is_network_active ||
23722 ( isset( $this->_user ) && $this->_user instanceof FS_User )
23723 ) {
23724 return $this->get_api_user_scope( $flush );
23725 }
23726
23727 $user = $this->get_current_or_network_user();
23728
23729 $this->_user_api = FS_Api::instance(
23730 $this->_module_id,
23731 'user',
23732 $user->id,
23733 $user->public_key,
23734 ! $this->is_live(),
23735 $user->secret_key,
23736 $this->get_sdk_version()
23737 );
23738
23739 return $this->_user_api;
23740 }
23741
23742 private $_site_api;
23743
23744 /**
23745 *
23746 * @author Vova Feldman (@svovaf)
23747 * @since 1.0.2
23748 *
23749 * @param bool $flush
23750 *
23751 * @return FS_Api
23752 */
23753 private function get_api_site_scope( $flush = false ) {
23754 if ( ! isset( $this->_site_api ) || $flush ) {
23755 $this->_site_api = FS_Api::instance(
23756 $this->_module_id,
23757 'install',
23758 $this->_site->id,
23759 $this->_site->public_key,
23760 ! $this->is_live(),
23761 $this->_site->secret_key,
23762 $this->get_sdk_version(),
23763 self::get_unfiltered_site_url()
23764 );
23765 }
23766
23767 return $this->_site_api;
23768 }
23769
23770 /**
23771 * @author Leo Fajardo (@leorw)
23772 * @since 2.5.0
23773 *
23774 * @param string $path
23775 * @param string $method
23776 * @param array $params
23777 * @param bool $flush_instance
23778 *
23779 * @return array|mixed|string|void
23780 * @throws Freemius_Exception
23781 */
23782 private function api_site_call( $path, $method = 'GET', $params = array(), $flush_instance = false ) {
23783 $result = $this->get_api_site_scope( $flush_instance )->call( $path, $method, $params );
23784
23785 /**
23786 * 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.
23787 *
23788 * @author Leo Fajardo (@leorw)
23789 * @since 2.5.0
23790 */
23791 if (
23792 $this->is_registered() &&
23793 FS_Api::is_api_result_entity( $result ) &&
23794 isset( $result->url )
23795 ) {
23796 $stored_local_url = trailingslashit( $this->_site->url );
23797 $stored_remote_url = trailingslashit( $result->url );
23798
23799 if ( $stored_local_url !== $stored_remote_url ) {
23800 $this->_site->url = $result->url;
23801 $this->_store_site();
23802 }
23803
23804 if ( fs_strip_url_protocol( $stored_remote_url ) !== self::get_unfiltered_site_url( null, true, true ) ) {
23805 FS_Clone_Manager::instance()->maybe_run_clone_resolution();
23806 }
23807 }
23808
23809 return $result;
23810 }
23811
23812 private $_plugin_api;
23813
23814 /**
23815 * Get plugin public API scope.
23816 *
23817 * @author Vova Feldman (@svovaf)
23818 * @since 1.0.7
23819 *
23820 * @return FS_Api
23821 */
23822 function get_api_plugin_scope() {
23823 if ( ! isset( $this->_plugin_api ) ) {
23824 $this->_plugin_api = FS_Api::instance(
23825 $this->_module_id,
23826 'plugin',
23827 $this->_plugin->id,
23828 $this->_plugin->public_key,
23829 ! $this->is_live(),
23830 false,
23831 $this->get_sdk_version()
23832 );
23833 }
23834
23835 return $this->_plugin_api;
23836 }
23837
23838 /**
23839 * Get bundle public API scope.
23840 *
23841 * @author Vova Feldman (@svovaf)
23842 * @since 2.3.1
23843 *
23844 * @return FS_Api
23845 */
23846 function get_api_bundle_scope() {
23847 return FS_Api::instance(
23848 $this->get_bundle_id(),
23849 'plugin',
23850 $this->get_bundle_id(),
23851 $this->get_bundle_public_key(),
23852 ! $this->is_live(),
23853 false,
23854 $this->get_sdk_version()
23855 );
23856 }
23857
23858 /**
23859 * Get site API scope object (fallback to public plugin scope when not registered).
23860 *
23861 * @author Vova Feldman (@svovaf)
23862 * @since 1.0.7
23863 *
23864 * @return FS_Api
23865 */
23866 function get_api_site_or_plugin_scope() {
23867 return $this->is_registered() ?
23868 $this->get_api_site_scope() :
23869 $this->get_api_plugin_scope();
23870 }
23871
23872 /**
23873 * @author Leo Fajardo (@leorw)
23874 * @since 2.2.3.1
23875 *
23876 * @param object $result
23877 */
23878 private function maybe_modify_api_curl_error_message( $result ) {
23879 if (
23880 'cUrlMissing' !== $result->error->type &&
23881 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
23882 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
23883 ) {
23884 return;
23885 }
23886
23887 $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' ) .
23888 ' ' .
23889 $this->esc_html_inline(
23890 sprintf(
23891 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
23892 implode(
23893 ', ',
23894 $this->apply_filters( 'api_domains', array(
23895 'api.freemius.com',
23896 'wp.freemius.com'
23897 ) )
23898 )
23899 ),
23900 'connectivity-whitelist'
23901 ) .
23902 ' ' .
23903 sprintf(
23904 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
23905 $this->get_module_type()
23906 );
23907 }
23908
23909 /**
23910 * Show trial promotional notice (if any trial exist).
23911 *
23912 * @author Vova Feldman (@svovaf)
23913 * @since 1.0.9
23914 *
23915 * @param FS_Plugin_Plan[] $plans
23916 */
23917 function _check_for_trial_plans( $plans ) {
23918 /**
23919 * 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.
23920 *
23921 * @author Vova Feldman (@svovaf)
23922 * @since 2.1.2
23923 */
23924 if ( ! is_array( $plans ) && is_object( $plans ) ) {
23925 $plans = array( $plans );
23926 }
23927
23928 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
23929 $plans = array();
23930 }
23931
23932 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
23933 }
23934
23935 /**
23936 * During trial promotion the "upgrade" submenu item turns to
23937 * "start trial" to encourage the trial. Since we want to keep
23938 * the same menu item handler and there's no robust way to
23939 * add new arguments to the menu item link's querystring,
23940 * use JavaScript to find the menu item and update the href of
23941 * the link.
23942 *
23943 * @author Vova Feldman (@svovaf)
23944 * @since 1.2.1.5
23945 */
23946 function _fix_start_trial_menu_item_url() {
23947 $template_args = array( 'id' => $this->_module_id );
23948 fs_require_template( 'add-trial-to-pricing.php', $template_args );
23949 }
23950
23951 /**
23952 * Check if module is currently in a trial promotion mode.
23953 *
23954 * @author Vova Feldman (@svovaf)
23955 * @since 1.2.2.7
23956 *
23957 * @return bool
23958 */
23959 function is_in_trial_promotion() {
23960 return $this->_admin_notices->has_sticky( 'trial_promotion' );
23961 }
23962
23963 /**
23964 * Show trial promotional notice (if any trial exist).
23965 *
23966 * @author Vova Feldman (@svovaf)
23967 * @since 1.0.9
23968 *
23969 * @return bool If trial notice added.
23970 */
23971 function _add_trial_notice() {
23972 if ( ! $this->is_user_admin() ) {
23973 return false;
23974 }
23975
23976 if ( ! $this->is_user_in_admin() ) {
23977 return false;
23978 }
23979
23980 if ( $this->_is_network_active ) {
23981 if ( fs_is_network_admin() ) {
23982 // Network level trial is disabled at the moment.
23983 return false;
23984 }
23985
23986 if ( ! $this->is_delegated_connection() ) {
23987 // Only delegated sites should support trials.
23988 return false;
23989 }
23990 }
23991
23992 // Check if trial message is already shown.
23993 if ( $this->is_in_trial_promotion() ) {
23994 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
23995
23996 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
23997
23998 return false;
23999 }
24000
24001 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
24002 // Don't show trial if running the premium code, unless running in DEV mode.
24003 return false;
24004 }
24005
24006 if ( ! $this->has_trial_plan() ) {
24007 // No plans with trial.
24008 return false;
24009 }
24010
24011 if ( ! $this->apply_filters( 'show_trial', true ) ) {
24012 // Developer explicitly asked not to show the trial promo.
24013 return false;
24014 }
24015
24016 if ( $this->is_registered() ) {
24017 // Check if trial already utilized.
24018 if ( $this->_site->is_trial_utilized() ) {
24019 return false;
24020 }
24021
24022 if ( $this->is_paying_or_trial() ) {
24023 // Don't show trial if paying or already in trial.
24024 return false;
24025 }
24026 }
24027
24028 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24029 // If not yet opted-in/skipped, or pending activation, don't show trial.
24030 return false;
24031 }
24032
24033 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
24034 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
24035
24036 // Show promotion if never shown before and 24 hours after initial activation with FS.
24037 if ( ! $was_promotion_shown_before &&
24038 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
24039 ) {
24040 return false;
24041 }
24042
24043 // OR if promotion was shown before, try showing it every 30 days.
24044 if ( $was_promotion_shown_before &&
24045 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
24046 ) {
24047 return false;
24048 }
24049
24050 $trial_period = $this->_trial_days;
24051 $require_payment = $this->_is_trial_require_payment;
24052 $trial_url = $this->get_trial_url();
24053 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
24054
24055 if ( $this->is_registered() ) {
24056 // If opted-in, override trial with up to date data from API.
24057 $trial_plans = FS_Plan_Manager::instance()->get_visible_trial_plans( $this->_plans );
24058 $trial_plans_count = count( $trial_plans );
24059
24060 if ( 0 === $trial_plans_count ) {
24061 // If there's no plans with a trial just exit.
24062 return false;
24063 }
24064
24065 /**
24066 * @var FS_Plugin_Plan $paid_plan
24067 */
24068 $paid_plan = $trial_plans[0];
24069 $require_payment = $paid_plan->is_require_subscription;
24070 $trial_period = $paid_plan->trial_period;
24071
24072 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
24073
24074 if ( $total_paid_plans !== $trial_plans_count ) {
24075 // Not all paid plans have a trial - generate a string of those that have it.
24076 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
24077 $plans_string .= sprintf(
24078 ' <a href="%s">%s</a>',
24079 $trial_url,
24080 $trial_plans[ $i ]->title
24081 );
24082
24083 if ( $i < $trial_plans_count - 2 ) {
24084 $plans_string .= ', ';
24085 } else if ( $i == $trial_plans_count - 2 ) {
24086 $plans_string .= ' and ';
24087 }
24088 }
24089 }
24090 }
24091
24092 $message = sprintf(
24093 $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' ),
24094 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
24095 $plans_string,
24096 $trial_period
24097 );
24098
24099 // "No Credit-Card Required" or "No Commitment for N Days".
24100 $cc_string = $require_payment ?
24101 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
24102 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
24103
24104
24105 // Start trial button.
24106 $button = ' ' . sprintf(
24107 '<div><a class="button button-primary" href="%s">%s &nbsp;&#10140;</a></div>',
24108 $trial_url,
24109 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
24110 );
24111
24112 $message_text = $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string}" );
24113
24114 $this->_admin_notices->add_sticky(
24115 "<div class=\"fs-trial-message-container\"><div>{$message_text}</div> {$button}</div>",
24116 'trial_promotion',
24117 '',
24118 'promotion'
24119 );
24120
24121 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
24122
24123 return true;
24124 }
24125
24126 /**
24127 * Lets users/customers know that the product has an affiliate program.
24128 *
24129 * @author Leo Fajardo (@leorw)
24130 * @since 1.2.2.11
24131 *
24132 * @return bool Returns true if the notice has been added.
24133 */
24134 function _add_affiliate_program_notice() {
24135 if ( ! $this->is_user_admin() ) {
24136 return false;
24137 }
24138
24139 if ( ! $this->is_user_in_admin() ) {
24140 return false;
24141 }
24142
24143 // Check if the notice is already shown.
24144 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
24145 return false;
24146 }
24147
24148 if (
24149 // Product has no affiliate program.
24150 ! $this->has_affiliate_program() ||
24151 // User has applied for an affiliate account.
24152 ! empty( $this->_storage->affiliate_application_data )
24153 ) {
24154 return false;
24155 }
24156
24157 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
24158 // Developer explicitly asked not to show the notice about the affiliate program.
24159 return false;
24160 }
24161
24162 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24163 // If not yet opted in/skipped, or pending activation, don't show the notice.
24164 return false;
24165 }
24166
24167 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
24168 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
24169
24170 /**
24171 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
24172 * activation with FS.
24173 */
24174 if ( $was_notice_shown_before ||
24175 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
24176 ) {
24177 return false;
24178 }
24179
24180 if ( ! $this->is_paying() &&
24181 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
24182 ) {
24183 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
24184 return false;
24185 }
24186
24187 $message = sprintf(
24188 $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' ),
24189 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
24190 $this->get_module_label( true )
24191 );
24192
24193 // HTML code for the "Learn more..." button.
24194 $button = ' ' . sprintf(
24195 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24196 $this->_get_admin_page_url( 'affiliation' ),
24197 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
24198 );
24199
24200 $this->_admin_notices->add_sticky(
24201 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
24202 'affiliate_program',
24203 '',
24204 'promotion'
24205 );
24206
24207 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
24208
24209 return true;
24210 }
24211
24212 /**
24213 * @author Vova Feldman (@svovaf)
24214 * @since 1.2.1.5
24215 */
24216 function _enqueue_common_css() {
24217 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
24218 // Add basic CSS for admin-notices and menu-item colors.
24219 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
24220 }
24221 }
24222
24223 /**
24224 * @author Leo Fajardo (@leorw)
24225 * @since 1.2.2
24226 */
24227 function _show_theme_activation_optin_dialog() {
24228 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
24229
24230 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
24231 }
24232
24233 /**
24234 * @author Leo Fajardo (@leorw)
24235 * @since 1.2.2
24236 */
24237 function _add_fs_theme_activation_dialog() {
24238 global $pagenow;
24239
24240 if ( 'themes.php' !== $pagenow ) {
24241 return;
24242 }
24243
24244 $vars = array( 'id' => $this->_module_id );
24245 fs_require_once_template( 'connect.php', $vars );
24246 }
24247
24248 /* Action Links
24249 ------------------------------------------------------------------------------------------------------------------*/
24250 private $_action_links_hooked = false;
24251 private $_action_links = array();
24252
24253 /**
24254 * Hook to plugin action links filter.
24255 *
24256 * @author Vova Feldman (@svovaf)
24257 * @since 1.0.0
24258 */
24259 private function hook_plugin_action_links() {
24260 $this->_logger->entrance();
24261
24262 $this->_action_links_hooked = true;
24263
24264 $this->_logger->log( 'Adding action links hooks.' );
24265
24266 // Add action link to settings page.
24267 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
24268 &$this,
24269 '_modify_plugin_action_links_hook'
24270 ), WP_FS__DEFAULT_PRIORITY, 2 );
24271 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
24272 &$this,
24273 '_modify_plugin_action_links_hook'
24274 ), WP_FS__DEFAULT_PRIORITY, 2 );
24275 }
24276
24277 /**
24278 * Add plugin action link.
24279 *
24280 * @author Vova Feldman (@svovaf)
24281 * @since 1.0.0
24282 *
24283 * @param $label
24284 * @param $url
24285 * @param bool $external
24286 * @param int $priority
24287 * @param bool $key
24288 */
24289 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
24290 $this->_logger->entrance();
24291
24292 if ( ! isset( $this->_action_links[ $priority ] ) ) {
24293 $this->_action_links[ $priority ] = array();
24294 }
24295
24296 if ( false === $key ) {
24297 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
24298 }
24299
24300 $this->_action_links[ $priority ][] = array(
24301 'label' => $label,
24302 'href' => $url,
24303 'key' => $key,
24304 'external' => $external
24305 );
24306 }
24307
24308 /**
24309 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
24310 *
24311 * @author Vova Feldman (@svovaf)
24312 * @since 1.0.0
24313 */
24314 function _add_upgrade_action_link() {
24315 $this->_logger->entrance();
24316
24317 $is_activation_mode = $this->is_activation_mode();
24318
24319 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
24320
24321 /**
24322 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
24323 * and "Add-Ons" menus should be added.
24324 *
24325 * @author Leo Fajardo (@leorw)
24326 * @since 2.3.0
24327 */
24328 $add_upgrade_link = (
24329 $add_action_links ||
24330 ( $is_activation_mode && $this->is_only_premium() )
24331 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
24332
24333 $add_addons_link = ( $add_action_links && $this->has_addons() );
24334
24335 if ( ! $add_upgrade_link && ! $add_addons_link ) {
24336 return;
24337 }
24338
24339 if (
24340 $add_upgrade_link &&
24341 $this->is_pricing_page_visible() &&
24342 $this->is_submenu_item_visible( 'pricing' )
24343 ) {
24344 $this->add_plugin_action_link(
24345 $this->get_text_inline( 'Upgrade', 'upgrade' ),
24346 $this->get_upgrade_url(),
24347 false,
24348 7,
24349 'upgrade'
24350 );
24351 }
24352
24353 if (
24354 $add_addons_link &&
24355 $this->has_addons() &&
24356 $this->is_submenu_item_visible( 'addons' )
24357 ) {
24358 $this->add_plugin_action_link(
24359 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
24360 $this->_get_admin_page_url( 'addons' ),
24361 false,
24362 9,
24363 'addons'
24364 );
24365 }
24366 }
24367
24368 /**
24369 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
24370 *
24371 * @author Leo Fajardo (@leorw)
24372 * @since 1.1.9
24373 */
24374 function _add_license_action_link() {
24375 $this->_logger->entrance();
24376
24377 if ( ! self::is_ajax() ) {
24378 // Inject license activation dialog UI and client side code.
24379 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
24380 }
24381
24382 $link_text = $this->is_free_plan() ?
24383 $this->get_text_inline( 'Activate License', 'activate-license' ) :
24384 $this->get_text_inline( 'Change License', 'change-license' );
24385
24386 $this->add_plugin_action_link(
24387 $link_text,
24388 '#',
24389 false,
24390 11,
24391 ( 'activate-license ' . $this->get_unique_affix() )
24392 );
24393 }
24394
24395 /**
24396 * @author Leo Fajardo (@leorw)
24397 * @since 2.0.2
24398 */
24399 function _add_premium_version_upgrade_selection_action() {
24400 $this->_logger->entrance();
24401
24402 if ( ! self::is_ajax() ) {
24403 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
24404 }
24405 }
24406
24407 /**
24408 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
24409 *
24410 * @author Leo Fajardo (@leorw)
24411 * @since 1.2.1.5
24412 */
24413 function _add_tracking_links() {
24414 if ( ! current_user_can( 'manage_options' ) ) {
24415 return;
24416 }
24417
24418 $this->_logger->entrance();
24419
24420 if ( $this->is_only_premium() && $this->is_free_plan() ) {
24421 // 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.
24422 return;
24423 }
24424
24425 if (
24426 $this->is_addon() &&
24427 ! $this->is_only_premium()
24428 ) {
24429 $parent = $this->get_parent_instance();
24430
24431 if ( is_object( $parent ) && $parent->is_anonymous() ) {
24432 return;
24433 }
24434 }
24435
24436 if ( fs_is_network_admin() ) {
24437 if ( ! $this->_is_network_active ) {
24438 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
24439 return;
24440 } else if ( $this->is_network_delegated_connection() ) {
24441 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
24442 return;
24443 }
24444 } else {
24445 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
24446 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
24447 return;
24448 }
24449 }
24450
24451 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
24452 if ( ! $this->is_registered() && $this->is_anonymous() ) {
24453 $this->connect_again();
24454
24455 return;
24456 }
24457 }
24458
24459 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
24460 ( $this->is_theme() && ! self::is_themes_page() )
24461 ) {
24462 // Only show tracking links on the plugins and themes pages.
24463 return;
24464 }
24465
24466 if (
24467 $this->is_activation_mode() &&
24468 $this->is_premium() &&
24469 ! $this->is_registered()
24470 ) {
24471 // If not yet registered and running the premium code base, a license activation link will already be shown.
24472 return;
24473 }
24474
24475 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24476 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
24477 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
24478 return;
24479 }
24480 }
24481
24482 if ( $this->add_ajax_action( 'toggle_permission_tracking', array( &$this, '_toggle_permission_tracking_callback' ) ) ) {
24483 return;
24484 }
24485
24486 $link_text_id = '';
24487 $url = '#';
24488
24489 if ( $this->is_registered( true ) ) {
24490 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24491 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
24492 } else {
24493 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24494 }
24495 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
24496 /**
24497 * Show opt-in link only if skipped or in activation mode.
24498 */
24499 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24500
24501 $params = ! $this->is_anonymous() ?
24502 array() :
24503 array(
24504 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
24505 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
24506 );
24507
24508 $url = $this->get_activation_url( $params );
24509 }
24510
24511 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
24512
24513 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
24514 $this->add_plugin_action_link(
24515 $link_text_id,
24516 $url,
24517 false,
24518 13,
24519 "opt-in-or-opt-out {$this->_slug}"
24520 );
24521 }
24522 }
24523
24524 /**
24525 * Get the URL of the page that should be loaded right after the plugin activation.
24526 *
24527 * @author Vova Feldman (@svovaf)
24528 * @since 1.1.7.4
24529 *
24530 * @return string
24531 */
24532 function get_after_plugin_activation_redirect_url() {
24533 $url = false;
24534
24535 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
24536 $first_time_path = $this->_menu->get_first_time_path(
24537 fs_is_network_admin() && $this->_is_network_active
24538 );
24539
24540 if ( $this->is_activation_mode() ) {
24541 $url = $this->get_activation_url();
24542 } else if ( ! empty( $first_time_path ) ) {
24543 $url = $first_time_path;
24544 } else {
24545 $page = '';
24546 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
24547 if ( $this->is_network_registered() ) {
24548 $page = 'account';
24549 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
24550 $this->maybe_set_slug_and_network_menu_exists_flag();
24551 }
24552 }
24553
24554 $url = $this->_get_admin_page_url( $page );
24555 }
24556 } else {
24557 $plugin_fs = false;
24558
24559 if ( $this->is_parent_plugin_installed() ) {
24560 $plugin_fs = self::get_parent_instance();
24561 }
24562
24563 if ( is_object( $plugin_fs ) ) {
24564 if ( ! $plugin_fs->is_registered() ) {
24565 // Forward to parent plugin connect when parent not registered.
24566 $url = $plugin_fs->get_activation_url();
24567 } else {
24568 // Forward to account page.
24569 $url = $plugin_fs->_get_admin_page_url( 'account' );
24570 }
24571 }
24572 }
24573
24574 return $url;
24575 }
24576
24577 /**
24578 * Forward page to activation page.
24579 *
24580 * @author Vova Feldman (@svovaf)
24581 * @since 1.0.3
24582 */
24583 function _redirect_on_activation_hook() {
24584 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
24585 $url = $this->get_after_plugin_activation_redirect_url();
24586
24587 if ( is_string( $url ) ) {
24588 fs_redirect( $url );
24589 }
24590 }
24591 }
24592
24593 /**
24594 * Modify plugin's page action links collection.
24595 *
24596 * @author Vova Feldman (@svovaf)
24597 * @since 1.0.0
24598 *
24599 * @param array $links
24600 * @param $file
24601 *
24602 * @return array
24603 */
24604 function _modify_plugin_action_links_hook( $links, $file ) {
24605 $this->_logger->entrance();
24606
24607 $passed_deactivate = false;
24608 $deactivate_link = '';
24609 $before_deactivate = array();
24610 $after_deactivate = array();
24611 foreach ( $links as $key => $link ) {
24612 if ( 'deactivate' === $key ) {
24613 $deactivate_link = $link;
24614 $passed_deactivate = true;
24615 continue;
24616 }
24617
24618 if ( ! $passed_deactivate ) {
24619 $before_deactivate[ $key ] = $link;
24620 } else {
24621 $after_deactivate[ $key ] = $link;
24622 }
24623 }
24624
24625 ksort( $this->_action_links );
24626
24627 foreach ( $this->_action_links as $new_links ) {
24628 foreach ( $new_links as $link ) {
24629 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
24630 }
24631 }
24632
24633 if ( ! empty( $deactivate_link ) ) {
24634 /**
24635 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
24636 *
24637 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
24638 */
24639 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
24640
24641 // Append deactivation link.
24642 $before_deactivate['deactivate'] = $deactivate_link;
24643 }
24644
24645 return array_merge( $before_deactivate, $after_deactivate );
24646 }
24647
24648 /**
24649 * Adds admin message.
24650 *
24651 * @author Vova Feldman (@svovaf)
24652 * @since 1.0.4
24653 *
24654 * @param string $message
24655 * @param string $title
24656 * @param string $type
24657 */
24658 function add_admin_message( $message, $title = '', $type = 'success' ) {
24659 $this->_admin_notices->add( $message, $title, $type );
24660 }
24661
24662 /**
24663 * Adds sticky admin message.
24664 *
24665 * @author Vova Feldman (@svovaf)
24666 * @since 1.1.0
24667 *
24668 * @param string $message
24669 * @param string $id
24670 * @param string $title
24671 * @param string $type
24672 */
24673 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
24674 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
24675 }
24676
24677 /**
24678 * Check if the paid version of the module is installed.
24679 *
24680 * @author Vova Feldman (@svovaf)
24681 * @since 2.2.0
24682 *
24683 * @return bool
24684 */
24685 private function is_premium_version_installed() {
24686 $premium_plugin_basename = $this->premium_plugin_basename();
24687
24688 if ( $this->is_theme() ) {
24689 return $this->can_activate_theme( $this->get_premium_slug() );
24690 }
24691
24692 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_plugin_basename ) );
24693 }
24694
24695 /**
24696 * Helper function that returns the final steps for the upgrade completion.
24697 *
24698 * If the module is already running the premium code, returns an empty string.
24699 *
24700 * @author Vova Feldman (@svovaf)
24701 * @since 1.2.1
24702 *
24703 * @param string $plan_title
24704 *
24705 * @return string
24706 */
24707 private function get_complete_upgrade_instructions( $plan_title = '' ) {
24708 $this->_logger->entrance();
24709
24710 $activate_license_string = $this->get_license_network_activation_notice();
24711
24712 if ( ! $this->has_premium_version() || $this->is_premium() ) {
24713 return '' . $activate_license_string;
24714 }
24715
24716 if ( empty( $plan_title ) ) {
24717 $plan_title = $this->get_plan_title();
24718 }
24719
24720 if ( $this->is_premium_version_installed() ) {
24721 /**
24722 * If the premium version is already installed, instead of showing the installation instructions,
24723 * tell the current user to activate it.
24724 *
24725 * @author Leo Fajardo (@leorw)
24726 * @since 2.2.1
24727 */
24728 $premium_theme_slug_or_plugin_basename = $this->is_theme() ?
24729 $this->get_premium_slug() :
24730 $this->premium_plugin_basename();
24731
24732 if ( is_admin() ) {
24733 return sprintf(
24734 /* translators: %1$s: Product title; %2$s: Plan title */
24735 $this->get_text_inline( ' The paid version of %1$s is already installed. Please activate it to start benefiting from the %2$s features. %3$s', 'activate-premium-version' ),
24736 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24737 $plan_title,
24738 sprintf(
24739 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
24740 ( $this->is_theme() ?
24741 wp_nonce_url( 'themes.php?action=activate&amp;stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) :
24742 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ),
24743 esc_html( sprintf(
24744 /* translators: %s: Plan title */
24745 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
24746 $plan_title
24747 ) )
24748 )
24749 );
24750 } else {
24751 return sprintf(
24752 /* translators: %1$s: Product title; %3$s: Plan title */
24753 $this->get_text_inline( ' The paid version of %1$s is already installed. Please navigate to the %2$s to activate it and start benefiting from the %3$s features.', 'activate-premium-version-plugins-page' ),
24754 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24755 sprintf(
24756 '<a href="%s">%s</a>',
24757 admin_url( $this->is_theme() ? 'themes.php' : 'plugins.php' ),
24758 ( $this->is_theme() ?
24759 $this->get_text_inline( 'Themes page', 'themes-page' ) :
24760 $this->get_text_inline( 'Plugins page', 'plugins-page' ) )
24761 ),
24762 $plan_title
24763 );
24764 }
24765 } else {
24766 // @since 1.2.1.5 The free version is auto deactivated.
24767 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
24768 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
24769 '';
24770
24771 return sprintf(
24772 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
24773 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
24774 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
24775 $this->get_latest_download_link( sprintf(
24776 /* translators: %s: Plan title */
24777 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
24778 $plan_title
24779 ) ),
24780 $deactivation_step,
24781 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
24782 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/wp-' . $this->_module_type . '-upload' ),
24783 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
24784 );
24785 }
24786 }
24787
24788 /**
24789 * @author Leo Fajardo (@leorw)
24790 * @since 2.5.3
24791 *
24792 * @param string $message_before_the_instructions
24793 * @param string $message_id
24794 * @param string $plan_title
24795 */
24796 private function add_complete_upgrade_instructions_notice(
24797 $message_before_the_instructions,
24798 $message_id,
24799 $plan_title = ''
24800 ) {
24801 $this->_admin_notices->add_sticky(
24802 $message_before_the_instructions .
24803 $this->get_complete_upgrade_instructions( $plan_title ),
24804 $message_id,
24805 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
24806 );
24807 }
24808
24809 /**
24810 * @author Leo Fajardo (@leorw)
24811 * @since 2.5.3
24812 *
24813 * @param bool $is_upgrade
24814 */
24815 private function add_after_plan_activation_or_upgrade_instructions_notice( $is_upgrade = true ) {
24816 $this->add_complete_upgrade_instructions_notice(
24817 $is_upgrade ?
24818 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) :
24819 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ),
24820 'plan_upgraded'
24821 );
24822 }
24823
24824 /**
24825 * @author Leo Fajardo (@leorw)
24826 * @since 2.1.0
24827 *
24828 * @param string $url
24829 * @param array $request
24830 * @param int $success_cache_expiration
24831 * @param int $failure_cache_expiration
24832 * @param bool $maybe_enrich_request_for_debug
24833 *
24834 * @return WP_Error|array
24835 */
24836 static function safe_remote_post(
24837 &$url,
24838 $request,
24839 $success_cache_expiration = 0,
24840 $failure_cache_expiration = 0,
24841 $maybe_enrich_request_for_debug = true
24842 ) {
24843 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
24844
24845 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
24846
24847 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
24848 get_transient( $cache_key ) :
24849 false;
24850
24851 if ( false === $response ) {
24852 if ( $maybe_enrich_request_for_debug ) {
24853 FS_DebugManager::enrich_request_for_debug( $url, $request );
24854 }
24855
24856 if ( ! isset( $request['method'] ) ) {
24857 $request['method'] = 'POST';
24858 }
24859
24860 $response = FS_Api::remote_request( $url, $request );
24861
24862 if (
24863 'https://' === substr( $url, 0, 8 ) &&
24864 FS_Api::is_ssl_error_response( $response )
24865 ) {
24866 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
24867 $url = 'http://' . substr( $url, 8 );
24868
24869 $request['timeout'] = 15;
24870 $response = FS_Api::remote_request( $url, $request );
24871 }
24872
24873 if ( false !== $cache_key ) {
24874 set_transient(
24875 $cache_key,
24876 $response,
24877 ( ( $response instanceof WP_Error ) ?
24878 $failure_cache_expiration :
24879 $success_cache_expiration )
24880 );
24881 }
24882 }
24883
24884 return $response;
24885 }
24886
24887 /**
24888 * This method is used to enrich the after upgrade notice instructions when the upgraded
24889 * license cannot be activated network wide (license quota isn't large enough).
24890 *
24891 * @author Vova Feldman (@svovaf)
24892 * @since 2.0.0
24893 *
24894 * @return string
24895 */
24896 private function get_license_network_activation_notice() {
24897 if ( ! $this->_is_network_active ) {
24898 // Module isn't network level activated.
24899 return '';
24900 }
24901
24902 if ( ! fs_is_network_admin() ) {
24903 // Not network level admin.
24904 return '';
24905 }
24906
24907 if ( get_blog_count() == 1 ) {
24908 // There's only a single site in the network so if there's a context license it was already activated.
24909 return '';
24910 }
24911
24912 if ( ! is_object( $this->_license ) ) {
24913 // No context license.
24914 return '';
24915 }
24916
24917 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
24918 // 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).
24919 return '';
24920 }
24921
24922 if ( $this->can_activate_license_on_network( $this->_license ) ) {
24923 // 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).
24924 return '';
24925 }
24926
24927 return sprintf(
24928 $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' ),
24929 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
24930 '</a>'
24931 );
24932 }
24933
24934 /**
24935 * @author Vova Feldman (@svovaf)
24936 * @since 1.2.1.7
24937 *
24938 * @param string $key
24939 *
24940 * @return string
24941 */
24942 function get_text( $key ) {
24943 return fs_text( $key, $this->_slug );
24944 }
24945
24946 /**
24947 * @author Vova Feldman (@svovaf)
24948 * @since 1.2.3
24949 *
24950 * @param string $text Translatable string.
24951 * @param string $key String key for overrides.
24952 *
24953 * @return string
24954 */
24955 function get_text_inline( $text, $key = '' ) {
24956 return _fs_text_inline( $text, $key, $this->_slug );
24957 }
24958
24959 /**
24960 * @author Vova Feldman (@svovaf)
24961 * @since 1.2.3
24962 *
24963 * @param string $text Translatable string.
24964 * @param string $context Context information for the translators.
24965 * @param string $key String key for overrides.
24966 *
24967 * @return string
24968 */
24969 function get_text_x_inline( $text, $context, $key ) {
24970 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
24971 }
24972
24973 /**
24974 * @author Vova Feldman (@svovaf)
24975 * @since 1.2.3
24976 *
24977 * @param string $text Translatable string.
24978 * @param string $key String key for overrides.
24979 *
24980 * @return string
24981 */
24982 function esc_html_inline( $text, $key ) {
24983 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
24984 }
24985
24986 #----------------------------------------------------------------------------------
24987 #region Versioning
24988 #----------------------------------------------------------------------------------
24989
24990 /**
24991 * Check if Freemius in SDK upgrade mode.
24992 *
24993 * @author Vova Feldman (@svovaf)
24994 * @since 1.0.9
24995 *
24996 * @return bool
24997 */
24998 function is_sdk_upgrade_mode() {
24999 return isset( $this->_storage->sdk_upgrade_mode ) ?
25000 $this->_storage->sdk_upgrade_mode :
25001 false;
25002 }
25003
25004 /**
25005 * Turn SDK upgrade mode off.
25006 *
25007 * @author Vova Feldman (@svovaf)
25008 * @since 1.0.9
25009 */
25010 function set_sdk_upgrade_complete() {
25011 $this->_storage->sdk_upgrade_mode = false;
25012 }
25013
25014 /**
25015 * Check if plugin upgrade mode.
25016 *
25017 * @author Vova Feldman (@svovaf)
25018 * @since 1.0.9
25019 *
25020 * @return bool
25021 */
25022 function is_plugin_upgrade_mode() {
25023 return isset( $this->_storage->plugin_upgrade_mode ) ?
25024 $this->_storage->plugin_upgrade_mode :
25025 false;
25026 }
25027
25028 /**
25029 * Turn plugin upgrade mode off.
25030 *
25031 * @author Vova Feldman (@svovaf)
25032 * @since 1.0.9
25033 */
25034 function set_plugin_upgrade_complete() {
25035 $this->_storage->plugin_upgrade_mode = false;
25036
25037 $license_migration = ! empty( $this->_storage->license_migration ) ?
25038 $this->_storage->license_migration :
25039 array();
25040
25041 $license_migration['is_migrating'] = false;
25042
25043 $this->_storage->license_migration = $license_migration;
25044 }
25045
25046 #endregion
25047
25048 #----------------------------------------------------------------------------------
25049 #region Permissions
25050 #----------------------------------------------------------------------------------
25051
25052 /**
25053 * Check if specific permission requested.
25054 *
25055 * @author Vova Feldman (@svovaf)
25056 * @since 1.1.6
25057 *
25058 * @param string $permission
25059 *
25060 * @return bool
25061 */
25062 function is_permission_requested( $permission ) {
25063 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
25064 }
25065
25066 #endregion
25067
25068 #----------------------------------------------------------------------------------
25069 #region Auto Activation
25070 #----------------------------------------------------------------------------------
25071
25072 /**
25073 * Hints the SDK if running an auto-installation.
25074 *
25075 * @var bool
25076 */
25077 private $_isAutoInstall = false;
25078
25079 /**
25080 * After upgrade callback to install and auto activate a plugin.
25081 * This code will only be executed on explicit request from the user,
25082 * following the practice Jetpack are using with their theme installations.
25083 *
25084 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
25085 *
25086 * @author Vova Feldman (@svovaf)
25087 * @since 1.2.1.7
25088 */
25089 function _install_premium_version_ajax_action() {
25090 $this->_logger->entrance();
25091
25092 $this->check_ajax_referer( 'install_premium_version' );
25093
25094 if ( ! $this->is_registered() ) {
25095 // Not registered.
25096 self::shoot_ajax_failure( array(
25097 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
25098 'code' => 'premium_installed',
25099 ) );
25100 }
25101
25102 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
25103
25104 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25105 // Invalid ID.
25106 self::shoot_ajax_failure( array(
25107 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25108 'code' => 'invalid_module_id',
25109 ) );
25110 }
25111
25112 if ( $plugin_id == $this->get_id() ) {
25113 if ( $this->is_premium() ) {
25114 // Already using the premium code version.
25115 self::shoot_ajax_failure( array(
25116 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
25117 'code' => 'premium_installed',
25118 ) );
25119 }
25120 if ( ! $this->can_use_premium_code() ) {
25121 // Don't have access to the premium code.
25122 self::shoot_ajax_failure( array(
25123 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
25124 'code' => 'invalid_license',
25125 ) );
25126 }
25127 if ( ! $this->has_release_on_freemius() ) {
25128 // Plugin is a serviceware, no premium code version.
25129 self::shoot_ajax_failure( array(
25130 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
25131 'code' => 'premium_version_missing',
25132 ) );
25133 }
25134 } else {
25135 $addon = $this->get_addon( $plugin_id );
25136
25137 if ( ! is_object( $addon ) ) {
25138 // Invalid add-on ID.
25139 self::shoot_ajax_failure( array(
25140 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25141 'code' => 'invalid_module_id',
25142 ) );
25143 }
25144
25145 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25146 // Premium add-on version is already activated.
25147 self::shoot_ajax_failure( array(
25148 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
25149 'code' => 'premium_installed',
25150 ) );
25151 }
25152 }
25153
25154 $this->_isAutoInstall = true;
25155
25156 // Try to install and activate.
25157 $updater = FS_Plugin_Updater::instance( $this );
25158 $result = $updater->install_and_activate_plugin( $plugin_id );
25159
25160 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
25161 self::shoot_ajax_failure( array(
25162 'message' => $result['message'],
25163 'code' => $result['code'],
25164 ) );
25165 }
25166
25167 self::shoot_ajax_success( $result );
25168 }
25169
25170 /**
25171 * Displays module activation dialog box after a successful upgrade
25172 * where the user explicitly requested to auto download and install
25173 * the premium version.
25174 *
25175 * @author Vova Feldman (@svovaf)
25176 * @since 1.2.1.7
25177 */
25178 function _add_auto_installation_dialog_box() {
25179 $this->_logger->entrance();
25180
25181 if ( ! $this->is_registered() ) {
25182 // Not registered.
25183 return;
25184 }
25185
25186 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
25187
25188 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25189 // Invalid module ID.
25190 return;
25191 }
25192
25193 if ( $plugin_id == $this->get_id() ) {
25194 if ( $this->is_premium() ) {
25195 // Already using the premium code version.
25196 return;
25197 }
25198 if ( ! $this->can_use_premium_code() ) {
25199 // Don't have access to the premium code.
25200 return;
25201 }
25202 if ( ! $this->has_release_on_freemius() ) {
25203 // Plugin is a serviceware, no premium code version.
25204 return;
25205 }
25206 } else {
25207 $addon = $this->get_addon( $plugin_id );
25208
25209 if ( ! is_object( $addon ) ) {
25210 // Invalid add-on ID.
25211 return;
25212 }
25213
25214 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25215 // Premium add-on version is already activated.
25216 return;
25217 }
25218 }
25219
25220 $vars = array(
25221 'id' => $this->_module_id,
25222 'target_module_id' => $plugin_id,
25223 'slug' => $this->_slug,
25224 );
25225
25226 fs_require_template( 'auto-installation.php', $vars );
25227 }
25228
25229 #endregion
25230
25231 #--------------------------------------------------------------------------------
25232 #region Tabs Integration
25233 #--------------------------------------------------------------------------------
25234
25235 #region Module's Original Tabs
25236
25237 /**
25238 * Inject a JavaScript logic to capture the theme tabs HTML.
25239 *
25240 * @author Vova Feldman (@svovaf)
25241 * @since 1.2.2.7
25242 */
25243 function _tabs_capture() {
25244 $this->_logger->entrance();
25245
25246 if (
25247 ! $this->is_product_settings_page() ||
25248 ! $this->should_page_include_tabs() ||
25249 ! $this->is_matching_url( $this->main_menu_url() )
25250 ) {
25251 return;
25252 }
25253
25254 $params = array(
25255 'id' => $this->_module_id,
25256 );
25257
25258 fs_require_once_template( 'tabs-capture-js.php', $params );
25259 }
25260
25261 /**
25262 * Cache theme's tabs HTML for a week. The cache will also be set as expired
25263 * after version and type (free/premium) changes, in addition to the week period.
25264 *
25265 * @author Vova Feldman (@svovaf)
25266 * @since 1.2.2.7
25267 */
25268 function _store_tabs_ajax_action() {
25269 $this->_logger->entrance();
25270
25271 $this->check_ajax_referer( 'store_tabs' );
25272
25273 // Init filesystem if not yet initiated.
25274 WP_Filesystem();
25275
25276 // Get POST body HTML data.
25277 global $wp_filesystem;
25278 $tabs_html = $wp_filesystem->get_contents( "php://input" );
25279
25280 if ( is_string( $tabs_html ) ) {
25281 $tabs_html = trim( $tabs_html );
25282 }
25283
25284 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
25285 self::shoot_ajax_failure();
25286 }
25287
25288 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25289
25290 self::shoot_ajax_success();
25291 }
25292
25293 /**
25294 * Cache theme's settings page custom styles. The cache will also be set as expired
25295 * after version and type (free/premium) changes, in addition to the week period.
25296 *
25297 * @author Vova Feldman (@svovaf)
25298 * @since 1.2.2.7
25299 */
25300 function _store_tabs_styles() {
25301 $this->_logger->entrance();
25302
25303 if (
25304 ! $this->is_product_settings_page() ||
25305 ! $this->should_page_include_tabs() ||
25306 ! $this->is_matching_url( $this->main_menu_url() )
25307 ) {
25308 return;
25309 }
25310
25311 $wp_styles = wp_styles();
25312
25313 $theme_styles_url = get_template_directory_uri();
25314
25315 $stylesheets = array();
25316 foreach ( $wp_styles->queue as $handler ) {
25317 if ( fs_starts_with( $handler, 'fs_' ) ) {
25318 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
25319 continue;
25320 }
25321
25322 /**
25323 * @var _WP_Dependency $stylesheet
25324 */
25325 $stylesheet = $wp_styles->registered[ $handler ];
25326
25327 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
25328 $stylesheets[] = $stylesheet->src;
25329 }
25330 }
25331
25332 if ( ! empty( $stylesheets ) ) {
25333 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25334 }
25335 }
25336
25337 /**
25338 * Check if module's original settings page has any tabs.
25339 *
25340 * @author Vova Feldman (@svovaf)
25341 * @since 1.2.2.7
25342 *
25343 * @return bool
25344 */
25345 private function has_tabs() {
25346 return $this->_cache->has( 'tabs' );
25347 }
25348
25349 /**
25350 * Get module's settings page HTML content, starting
25351 * from the beginning of the <div class="wrap"> element,
25352 * until the tabs HTML (including).
25353 *
25354 * @author Vova Feldman (@svovaf)
25355 * @since 1.2.2.7
25356 *
25357 * @return string
25358 */
25359 private function get_tabs_html() {
25360 $this->_logger->entrance();
25361
25362 return $this->_cache->get( 'tabs' );
25363 }
25364
25365 /**
25366 * Check if page should include tabs.
25367 *
25368 * @author Vova Feldman (@svovaf)
25369 * @since 1.2.2.7
25370 *
25371 * @return bool
25372 */
25373 private function should_page_include_tabs() {
25374 if ( ! $this->has_settings_menu() ) {
25375 // Don't add tabs if no settings at all.
25376 return false;
25377 }
25378
25379 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
25380 // Only add tabs to themes for now.
25381 return false;
25382 }
25383
25384 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
25385 // Only add tabs to monetizing themes.
25386 return false;
25387 }
25388
25389 if ( ! $this->is_product_settings_page() ) {
25390 // Only add tabs if browsing one of the product's setting pages.
25391 return false;
25392 }
25393
25394 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
25395 // Don't include tabs in the activation page.
25396 return false;
25397 }
25398
25399 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
25400 // Don't add tabs on checkout page, we want to reduce distractions
25401 // as much as possible.
25402 return false;
25403 }
25404
25405 return true;
25406 }
25407
25408 /**
25409 * Add the tabs HTML before the setting's page content and
25410 * enqueue any required stylesheets.
25411 *
25412 * @author Vova Feldman (@svovaf)
25413 * @since 1.2.2.7
25414 *
25415 * @return bool If tabs were included.
25416 */
25417 function _add_tabs_before_content() {
25418 $this->_logger->entrance();
25419
25420 if ( ! $this->should_page_include_tabs() ) {
25421 return false;
25422 }
25423
25424 $tabs_html = $this->get_tabs_html();
25425
25426 if ( empty( $tabs_html ) ) {
25427 return false;
25428 }
25429
25430 /**
25431 * Enqueue the original stylesheets that are included in the
25432 * theme settings page. That way, if the theme settings has
25433 * some custom _styled_ content above the tabs UI, this
25434 * will make sure that the styling is preserved.
25435 */
25436 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
25437 if ( is_array( $stylesheets ) ) {
25438 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
25439 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
25440 }
25441 }
25442
25443 // Cut closing </div> tag.
25444 echo substr( trim( $tabs_html ), 0, - 6 );
25445
25446 return true;
25447 }
25448
25449 /**
25450 * Add the tabs closing HTML after the setting's page content.
25451 *
25452 * @author Vova Feldman (@svovaf)
25453 * @since 1.2.2.7
25454 *
25455 * @return bool If tabs closing HTML was included.
25456 */
25457 function _add_tabs_after_content() {
25458 $this->_logger->entrance();
25459
25460 if ( ! $this->should_page_include_tabs() ) {
25461 return false;
25462 }
25463
25464 echo '</div>';
25465
25466 return true;
25467 }
25468
25469 #endregion
25470
25471 /**
25472 * Add in-page JavaScript to inject the Freemius tabs into
25473 * the module's setting tabs section.
25474 *
25475 * @author Vova Feldman (@svovaf)
25476 * @since 1.2.2.7
25477 */
25478 function _add_freemius_tabs() {
25479 $this->_logger->entrance();
25480
25481 if ( ! $this->should_page_include_tabs() ) {
25482 return;
25483 }
25484
25485 $params = array( 'id' => $this->_module_id );
25486 fs_require_once_template( 'tabs.php', $params );
25487 }
25488
25489 #endregion
25490
25491 #--------------------------------------------------------------------------------
25492 #region Customizer Integration for Themes
25493 #--------------------------------------------------------------------------------
25494
25495 /**
25496 * @author Vova Feldman (@svovaf)
25497 * @since 1.2.2.7
25498 *
25499 * @param WP_Customize_Manager $customizer
25500 */
25501 function _customizer_register( $customizer ) {
25502 $this->_logger->entrance();
25503
25504 if ( $this->is_pricing_page_visible() ) {
25505 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
25506
25507 $customizer->add_section( 'freemius_upsell', array(
25508 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
25509 'priority' => 1,
25510 ) );
25511 $customizer->add_setting( 'freemius_upsell', array(
25512 'sanitize_callback' => 'esc_html',
25513 ) );
25514
25515 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
25516 'fs' => $this,
25517 'section' => 'freemius_upsell',
25518 'priority' => 100,
25519 ) ) );
25520 }
25521
25522 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
25523 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
25524
25525 // Main Documentation Link In Customizer Root.
25526 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
25527 'fs' => $this,
25528 'priority' => 1000,
25529 ) ) );
25530 }
25531 }
25532
25533 #endregion
25534
25535 /**
25536 * If the theme has a paid version, add some custom
25537 * styling to the theme's premium version (if exists)
25538 * to highlight that it's the premium version of the
25539 * same theme, making it easier for identification
25540 * after the user upgrades and upload it to the site.
25541 *
25542 * @author Vova Feldman (@svovaf)
25543 * @since 1.2.2.7
25544 */
25545 function _style_premium_theme() {
25546 $this->_logger->entrance();
25547
25548 if ( ! self::is_themes_page() ) {
25549 // Only include in the themes page.
25550 return;
25551 }
25552
25553 if ( ! $this->has_paid_plan() ) {
25554 // Only include if has any paid plans.
25555 return;
25556 }
25557
25558 $params = null;
25559 fs_require_once_template( '/js/jquery.content-change.php', $params );
25560
25561 $params = array(
25562 'slug' => $this->_slug,
25563 'id' => $this->_module_id,
25564 );
25565
25566 fs_require_template( '/js/style-premium-theme.php', $params );
25567 }
25568
25569 /**
25570 * This method will return the absolute URL of the module's local icon.
25571 *
25572 * When you are running your plugin or theme on a **localhost** environment, if the icon
25573 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
25574 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
25575 * If an icon is found, this method will automatically attempt to download the icon and store it
25576 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
25577 *
25578 * It's important to mention that this method is NOT phoning home since the developer will deploy
25579 * the product with the local icon in the assets folder. The download process just simplifies
25580 * the process for the developer.
25581 *
25582 * @author Vova Feldman (@svovaf)
25583 * @since 2.0.0
25584 *
25585 * @return string
25586 */
25587 function get_local_icon_url() {
25588 global $fs_active_plugins;
25589
25590 /**
25591 * @since 1.1.7.5
25592 */
25593 $local_path = $this->apply_filters( 'plugin_icon', false );
25594
25595 if ( is_string( $local_path ) ) {
25596 $icons = array( $local_path );
25597 } else {
25598 $img_dir = WP_FS__DIR_IMG;
25599
25600 // Locate the main assets folder.
25601 if ( ! empty( $fs_active_plugins->plugins ) ) {
25602 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
25603
25604 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
25605 if ( $data->plugin_path == $this->get_plugin_basename() ) {
25606 $img_dir = $plugin_or_theme_img_dir
25607 . '/'
25608 /**
25609 * The basename will be `themes` or the basename of a custom themes directory.
25610 *
25611 * @author Leo Fajardo (@leorw)
25612 * @since 2.2.3
25613 */
25614 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
25615 . '/assets/img';
25616
25617 break;
25618 }
25619 }
25620 }
25621
25622 // Try to locate the icon in the assets folder.
25623 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
25624
25625 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
25626 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
25627 $icons = array(
25628 fs_normalize_path( $img_dir . '/theme-icon.png' )
25629 );
25630 } else {
25631 $icon_found = false;
25632 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
25633
25634 if ( ! function_exists( 'get_filesystem_method' ) ) {
25635 require_once ABSPATH . 'wp-admin/includes/file.php';
25636 }
25637
25638 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
25639
25640 /**
25641 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
25642 *
25643 * This code will only be executed once during the testing
25644 * of the plugin in a local environment. The plugin icon file WILL
25645 * already exist in the assets folder when the plugin is deployed to
25646 * the repository.
25647 */
25648 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
25649 // Fetch icon from Freemius.
25650 $icon = $this->fetch_remote_icon_url();
25651
25652 // Fetch icon from WordPress.org.
25653 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
25654 if ( ! function_exists( 'plugins_api' ) ) {
25655 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
25656 }
25657
25658 $plugin_information = plugins_api( 'plugin_information', array(
25659 'slug' => $this->_slug,
25660 'fields' => array(
25661 'sections' => false,
25662 'tags' => false,
25663 'icons' => true
25664 )
25665 ) );
25666
25667 if (
25668 ! is_wp_error( $plugin_information )
25669 && isset( $plugin_information->icons )
25670 && ! empty( $plugin_information->icons )
25671 ) {
25672 /**
25673 * Get the smallest icon.
25674 *
25675 * @author Leo Fajardo (@leorw)
25676 * @since 1.2.2
25677 */
25678 $icon = end( $plugin_information->icons );
25679 }
25680 }
25681
25682 if ( ! empty( $icon ) ) {
25683 if ( 0 !== strpos( $icon, 'http' ) ) {
25684 $icon = 'http:' . $icon;
25685 }
25686
25687 /**
25688 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
25689 *
25690 * @author Leo Fajardo (@leorw)
25691 * @since 1.2.2
25692 */
25693 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
25694
25695 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
25696
25697 // Try to download the icon.
25698 $icon_found = fs_download_image( $icon, $local_path );
25699 }
25700 }
25701
25702 if ( ! $icon_found ) {
25703 // No icons found, fallback to default icon.
25704 if ( $have_write_permissions ) {
25705 // If have write permissions, copy default icon.
25706 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
25707 } else {
25708 // If doesn't have write permissions, use default icon path.
25709 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
25710 }
25711 }
25712
25713 $icons = array( $local_path );
25714 }
25715 }
25716 }
25717
25718 $icon_dir = dirname( $icons[0] );
25719
25720 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
25721 }
25722
25723 /**
25724 * Fetch module's extended info.
25725 *
25726 * @author Vova Feldman (@svovaf)
25727 * @since 2.0.0
25728 *
25729 * @return object|mixed
25730 */
25731 private function fetch_module_info() {
25732 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
25733 }
25734
25735 /**
25736 * Fetch module's remote icon URL.
25737 *
25738 * @author Vova Feldman (@svovaf)
25739 * @since 2.0.0
25740 *
25741 * @return string
25742 */
25743 function fetch_remote_icon_url() {
25744 $info = $this->fetch_module_info();
25745
25746 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
25747 $info->icon :
25748 '';
25749 }
25750
25751 #--------------------------------------------------------------------------------
25752 #region GDPR
25753 #--------------------------------------------------------------------------------
25754
25755 /**
25756 * @author Leo Fajardo (@leorw)
25757 * @since 2.1.0
25758 *
25759 * @param array $user_plugins
25760 *
25761 * @return string
25762 */
25763 private function get_gdpr_admin_notice_string( $user_plugins ) {
25764 $this->_logger->entrance();
25765
25766 $addons = self::get_all_addons();
25767
25768 foreach ( $user_plugins as $user_plugin ) {
25769 $has_addons = isset( $addons[ $user_plugin->id ] );
25770
25771 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
25772 if ( $this->_module_id == $user_plugin->id ) {
25773 $addons = $this->get_addons();
25774 $has_addons = ( ! empty( $addons ) );
25775 } else {
25776 $plugin_api = FS_Api::instance(
25777 $user_plugin->id,
25778 'plugin',
25779 $user_plugin->id,
25780 $user_plugin->public_key,
25781 ! $user_plugin->is_live,
25782 false,
25783 $this->get_sdk_version()
25784 );
25785
25786 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
25787
25788 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
25789 is_array( $addons_result->plugins ) &&
25790 ! empty( $addons_result->plugins )
25791 ) {
25792 $has_addons = true;
25793 }
25794 }
25795 }
25796
25797 $user_plugin->has_addons = $has_addons;
25798 }
25799
25800 $is_single_parent_product = ( 1 === count( $user_plugins ) );
25801
25802 $multiple_products_text = '';
25803
25804 if ( $is_single_parent_product ) {
25805 $single_parent_product = reset( $user_plugins );
25806
25807 $thank_you = sprintf(
25808 "<span data-plugin-id='%d'>%s</span>",
25809 $single_parent_product->id,
25810 sprintf(
25811 $single_parent_product->has_addons ?
25812 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
25813 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
25814 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
25815 )
25816 );
25817
25818 $already_opted_in = sprintf(
25819 $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' ),
25820 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
25821 );
25822 } else {
25823 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
25824 $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' );
25825
25826 $products_and_add_ons = '';
25827 foreach ( $user_plugins as $user_plugin ) {
25828 if ( ! empty( $products_and_add_ons ) ) {
25829 $products_and_add_ons .= ', ';
25830 }
25831
25832 if ( ! $user_plugin->has_addons ) {
25833 $products_and_add_ons .= sprintf(
25834 "<span data-plugin-id='%d'>%s</span>",
25835 $user_plugin->id,
25836 $user_plugin->title
25837 );
25838 } else {
25839 $products_and_add_ons .= sprintf(
25840 "<span data-plugin-id='%d'>%s</span>",
25841 $user_plugin->id,
25842 sprintf(
25843 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
25844 $user_plugin->title
25845 )
25846 );
25847 }
25848 }
25849
25850 $multiple_products_text = sprintf(
25851 "<small class='products'><strong>%s:</strong> %s</small>",
25852 $this->get_text_inline( 'Products', 'products' ),
25853 $products_and_add_ons
25854 );
25855 }
25856
25857 $actions = sprintf(
25858 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
25859 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
25860 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
25861 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
25862 sprintf(
25863 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
25864 '<span class="underlined">',
25865 '</span>'
25866 )
25867 );
25868
25869 return sprintf(
25870 '%s %s %s',
25871 $thank_you,
25872 $already_opted_in,
25873 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>' ) .
25874 '<br><br>' .
25875 '<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>' .
25876 $actions .
25877 ( $is_single_parent_product ? '' : $multiple_products_text )
25878 );
25879 }
25880
25881 /**
25882 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
25883 * plugins and themes they've opted in to.
25884 *
25885 * @author Leo Fajardo (@leorw)
25886 * @since 2.1.0
25887 *
25888 * @param string $user_email
25889 * @param string $license_key
25890 * @param array $plugin_ids
25891 * @param string|null $license_key
25892 *
25893 * @return array|false
25894 */
25895 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
25896 $request = array(
25897 'method' => 'POST',
25898 'body' => array(),
25899 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
25900 );
25901
25902 if ( is_string( $user_email ) ) {
25903 $request['body']['email'] = $user_email;
25904 } else {
25905 $request['body']['license_key'] = $license_key;
25906 }
25907
25908 $result = array();
25909
25910 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
25911 $total_plugin_ids = count( $plugin_ids );
25912
25913 $plugin_ids_count_per_request = 10;
25914 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
25915 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
25916
25917 $request['body']['plugin_ids'] = $plugin_ids_set;
25918
25919 $response = self::safe_remote_post(
25920 $url,
25921 $request,
25922 WP_FS__TIME_24_HOURS_IN_SEC,
25923 WP_FS__TIME_12_HOURS_IN_SEC
25924 );
25925
25926 if ( ! is_wp_error( $response ) ) {
25927 $decoded = is_string( $response['body'] ) ?
25928 json_decode( $response['body'] ) :
25929 null;
25930
25931 if (
25932 !is_object($decoded) ||
25933 !isset($decoded->success) ||
25934 true !== $decoded->success ||
25935 !isset( $decoded->data ) ||
25936 !is_array( $decoded->data )
25937 ) {
25938 return false;
25939 }
25940
25941 $result = array_merge( $result, $decoded->data );
25942 }
25943 }
25944
25945 return $result;
25946 }
25947
25948 /**
25949 * @author Leo Fajardo (@leorw)
25950 * @since 2.1.0
25951 */
25952 function _maybe_show_gdpr_admin_notice() {
25953 if ( ! $this->is_user_in_admin() ) {
25954 return;
25955 }
25956
25957 if ( ! $this->should_handle_gdpr_admin_notice() ) {
25958 return;
25959 }
25960
25961 if ( ! $this->is_user_admin() ) {
25962 return;
25963 }
25964
25965 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25966
25967 $lock = FS_User_Lock::instance();
25968
25969 /**
25970 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
25971 */
25972 if ( ! $lock->try_lock( 60 ) ) {
25973 return;
25974 }
25975
25976 /**
25977 * @var $current_wp_user WP_User
25978 */
25979 $current_wp_user = self::_get_current_wp_user();
25980
25981 /**
25982 * @var FS_User $current_fs_user
25983 */
25984 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
25985
25986 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
25987
25988 if ( ! is_object( $current_fs_user ) ) {
25989 // 10-year lock.
25990 $lock->lock( $ten_years_in_sec );
25991
25992 return;
25993 }
25994
25995 $gdpr = FS_GDPR_Manager::instance();
25996
25997 if ( $gdpr->is_opt_in_notice_shown() ) {
25998 // 30-day lock.
25999 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26000
26001 return;
26002 }
26003
26004 if ( ! $gdpr->should_show_opt_in_notice() ) {
26005 // 10-year lock.
26006 $lock->lock( $ten_years_in_sec );
26007
26008 return;
26009 }
26010
26011 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
26012 $was_notice_shown_before = ( false !== $last_time_notice_shown );
26013
26014 if ( $was_notice_shown_before &&
26015 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
26016 ) {
26017 // If the notice was shown before, show it again after 30 days from the last time it was shown.
26018 return;
26019 }
26020
26021 /**
26022 * Find all plugin IDs that were installed by the current admin.
26023 */
26024 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
26025
26026 if ( empty( $plugin_ids_map )) {
26027 $lock->lock( $ten_years_in_sec );
26028
26029 return;
26030 }
26031
26032 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26033 $current_fs_user->email,
26034 null,
26035 array_keys( $plugin_ids_map )
26036 );
26037
26038 if ( empty( $user_plugins ) ) {
26039 $lock->lock(
26040 is_array($user_plugins) ?
26041 $ten_years_in_sec :
26042 // Lock for 24-hours on errors.
26043 WP_FS__TIME_24_HOURS_IN_SEC
26044 );
26045
26046 return;
26047 }
26048
26049 $has_unset_marketing_optin = false;
26050
26051 foreach ( $user_plugins as $user_plugin ) {
26052 if ( true == $user_plugin->is_marketing_allowed ) {
26053 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
26054 }
26055
26056 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
26057 $has_unset_marketing_optin = true;
26058 }
26059 }
26060
26061 if ( empty( $plugin_ids_map ) ||
26062 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
26063 ) {
26064 $lock->lock( $ten_years_in_sec );
26065
26066 return;
26067 }
26068
26069 $modules = array_merge(
26070 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26071 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26072 );
26073
26074 foreach ( $modules as $module ) {
26075 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
26076 $plugin_ids_map[ $module->id ] = $module;
26077 }
26078 }
26079
26080 $plugin_title = null;
26081 if ( 1 === count( $plugin_ids_map ) ) {
26082 $module = reset( $plugin_ids_map );
26083 $plugin_title = $module->title;
26084 }
26085
26086 $gdpr->add_opt_in_sticky_notice(
26087 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
26088 $plugin_title
26089 );
26090
26091 $this->add_gdpr_optin_ajax_handler_and_style();
26092
26093 $gdpr->notice_was_just_shown();
26094
26095 // 30-day lock.
26096 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26097 }
26098
26099 /**
26100 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
26101 * marketing.
26102 *
26103 * @author Leo Fajardo (@leorw)
26104 * @since 2.1.0
26105 */
26106 private function disable_opt_in_notice_and_lock_user() {
26107 FS_GDPR_Manager::instance()->disable_opt_in_notice();
26108
26109 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26110
26111 // 10-year lock.
26112 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26113 }
26114
26115 /**
26116 * @author Leo Fajardo (@leorw)
26117 * @since 2.5.4
26118 */
26119 static function _add_api_connectivity_notice_handler_js() {
26120 fs_require_once_template( 'api-connectivity-message-js.php' );
26121 }
26122
26123 /**
26124 * @author Leo Fajardo (@leorw)
26125 * @since 2.1.0
26126 */
26127 function _add_gdpr_optin_js() {
26128 $vars = array( 'id' => $this->_module_id );
26129
26130 fs_require_once_template( 'gdpr-optin-js.php', $vars );
26131 }
26132
26133 /**
26134 * @author Leo Fajardo (@leorw)
26135 * @since 2.1.0
26136 */
26137 function enqueue_gdpr_optin_notice_style() {
26138 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
26139 }
26140
26141 /**
26142 * @author Leo Fajardo (@leorw)
26143 * @since 2.1.0
26144 */
26145 function _maybe_add_gdpr_optin_ajax_handler() {
26146 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
26147
26148 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
26149 $this->add_gdpr_optin_ajax_handler_and_style();
26150 }
26151 }
26152
26153 /**
26154 * @author Leo Fajardo (@leorw)
26155 * @since 2.1.0
26156 */
26157 function _fetch_is_marketing_required_flag_value_ajax_action() {
26158 $this->_logger->entrance();
26159
26160 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
26161
26162 $license_key = fs_request_get_raw( 'license_key' );
26163
26164 if ( empty($license_key) ) {
26165 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
26166 }
26167
26168 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26169 null,
26170 $license_key,
26171 array( $this->_module_id )
26172 );
26173
26174 if ( ! is_array( $user_plugins ) ||
26175 empty($user_plugins) ||
26176 !isset($user_plugins[0]->plugin_id) ||
26177 $user_plugins[0]->plugin_id != $this->_module_id
26178 ) {
26179 /**
26180 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
26181 *
26182 * @author Vova Feldman (@svovaf)
26183 */
26184 self::shoot_ajax_success( array(
26185 'is_marketing_allowed' => null,
26186 'license_owner_id' => null
26187 ) );
26188 }
26189
26190 self::shoot_ajax_success( array(
26191 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
26192 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
26193 ) );
26194 }
26195
26196 /**
26197 * @author Leo Fajardo (@leorw)
26198 * @since 2.3.2
26199 *
26200 * @param number[] $install_ids
26201 *
26202 * @return array {
26203 * An array of objects containing the installs' licenses owners data.
26204 *
26205 * @property number $id User ID.
26206 * @property string $email User email (can be masked email).
26207 * }
26208 */
26209 private function fetch_installs_licenses_owners_data( $install_ids ) {
26210 $this->_logger->entrance();
26211
26212 $response = $this->get_api_user_scope()->get(
26213 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
26214 );
26215
26216 $license_owners = array();
26217
26218 if ( $this->is_api_result_object( $response, 'owners' ) ) {
26219 $license_owners = $response->owners;
26220 }
26221
26222 return $license_owners;
26223 }
26224
26225 /**
26226 * @author Leo Fajardo (@leorw)
26227 * @since 2.1.0
26228 */
26229 private function add_gdpr_optin_ajax_handler_and_style() {
26230 // Add GDPR action AJAX callback.
26231 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
26232
26233 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
26234 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
26235 }
26236
26237 /**
26238 * @author Leo Fajardo (@leorw)
26239 * @since 2.1.0
26240 */
26241 function _gdpr_optin_ajax_action() {
26242 $this->_logger->entrance();
26243
26244 $this->check_ajax_referer( 'gdpr_optin_action' );
26245
26246 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
26247 self::shoot_ajax_failure();
26248 }
26249
26250 $current_wp_user = self::_get_current_wp_user();
26251
26252 $plugin_ids = fs_request_get( 'plugin_ids', array() );
26253 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
26254 self::shoot_ajax_failure();
26255 }
26256
26257 $modules = array_merge(
26258 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26259 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26260 );
26261
26262 foreach ( $modules as $key => $module ) {
26263 if ( ! in_array( $module->id, $plugin_ids ) ) {
26264 unset( $modules[ $key ] );
26265 }
26266 }
26267
26268 if ( empty( $modules ) ) {
26269 self::shoot_ajax_failure();
26270 }
26271
26272 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
26273
26274 foreach ( $modules as $module ) {
26275 $user_api->call( "?plugin_id={$module->id}", 'put', array(
26276 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
26277 ) );
26278 }
26279
26280 FS_GDPR_Manager::instance()->remove_opt_in_notice();
26281
26282 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26283
26284 // 10-year lock.
26285 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26286
26287 self::shoot_ajax_success();
26288 }
26289
26290 /**
26291 * 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.
26292 *
26293 * @author Vova Feldman (@svovaf)
26294 * @since 2.1.0
26295 *
26296 * @return bool
26297 */
26298 private function should_handle_gdpr_admin_notice() {
26299 return $this->apply_filters(
26300 'handle_gdpr_admin_notice',
26301 // Default to false.
26302 false
26303 );
26304 }
26305
26306 #endregion
26307
26308 #----------------------------------------------------------------------------------
26309 #region Marketing
26310 #----------------------------------------------------------------------------------
26311
26312 /**
26313 * Check if current user purchased any other plugins before.
26314 *
26315 * @author Vova Feldman (@svovaf)
26316 * @since 1.0.9
26317 *
26318 * @return bool
26319 */
26320 function has_purchased_before() {
26321 // TODO: Implement has_purchased_before() method.
26322 throw new Exception( 'not implemented' );
26323 }
26324
26325 /**
26326 * Check if current user classified as an agency.
26327 *
26328 * @author Vova Feldman (@svovaf)
26329 * @since 1.0.9
26330 *
26331 * @return bool
26332 */
26333 function is_agency() {
26334 // TODO: Implement is_agency() method.
26335 throw new Exception( 'not implemented' );
26336 }
26337
26338 /**
26339 * Check if current user classified as a developer.
26340 *
26341 * @author Vova Feldman (@svovaf)
26342 * @since 1.0.9
26343 *
26344 * @return bool
26345 */
26346 function is_developer() {
26347 // TODO: Implement is_developer() method.
26348 throw new Exception( 'not implemented' );
26349 }
26350
26351 /**
26352 * Check if current user classified as a business.
26353 *
26354 * @author Vova Feldman (@svovaf)
26355 * @since 1.0.9
26356 *
26357 * @return bool
26358 */
26359 function is_business() {
26360 // TODO: Implement is_business() method.
26361 throw new Exception( 'not implemented' );
26362 }
26363
26364 #endregion
26365
26366 #----------------------------------------------------------------------------------
26367 #region Helper
26368 #----------------------------------------------------------------------------------
26369
26370 /**
26371 * If running with a secret key, assume it's the developer and show pending plans as well.
26372 *
26373 * @author Vova Feldman (@svovaf)
26374 * @since 2.1.2
26375 *
26376 * @param string $path
26377 *
26378 * @return string
26379 */
26380 function add_show_pending( $path ) {
26381 if ( ! $this->has_secret_key() ) {
26382 return $path;
26383 }
26384
26385 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
26386 }
26387
26388 #endregion
26389 }
26390