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

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

26,270 lines 988.8 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_key = false;
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_key = $this->_license->secret_key;
3641 }
3642
3643 return $this->opt_in(
3644 false,
3645 false,
3646 false,
3647 $license_key,
3648 false,
3649 false,
3650 false,
3651 null,
3652 array(),
3653 false
3654 );
3655 }
3656
3657 #endregion
3658
3659 /**
3660 * @author Leo Fajardo (@leorw)
3661 *
3662 * @since 2.1.3
3663 */
3664 public static function migrate_options_to_network() {
3665 self::migrate_accounts_to_network();
3666
3667 // Migrate API options from site level to network level.
3668 $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
3669 $api_network_options->migrate_to_network();
3670
3671 // Migrate API cache to network level storage.
3672 FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
3673
3674 self::$_accounts->set_option( 'ms_migration_complete', true, true );
3675 }
3676
3677 #----------------------------------------------------------------------------------
3678 #region Localization
3679 #----------------------------------------------------------------------------------
3680
3681 /**
3682 * Load framework's text domain.
3683 *
3684 * @author Vova Feldman (@svovaf)
3685 * @since 1.2.1
3686 */
3687 static function _load_textdomain() {
3688 if ( ! is_admin() ) {
3689 return;
3690 }
3691
3692 global $fs_active_plugins;
3693
3694 // Works both for plugins and themes.
3695 load_plugin_textdomain(
3696 'freemius',
3697 false,
3698 $fs_active_plugins->newest->sdk_path . '/languages/'
3699 );
3700 }
3701
3702 #endregion
3703
3704 #----------------------------------------------------------------------------------
3705 #region Connectivity Issues
3706 #----------------------------------------------------------------------------------
3707
3708 /**
3709 * Check if Freemius should be turned on for the current plugin install.
3710 *
3711 * Note:
3712 * $this->_is_on is updated in has_api_connectivity()
3713 *
3714 * @author Vova Feldman (@svovaf)
3715 * @since 1.0.9
3716 *
3717 * @return bool
3718 */
3719 function is_on() {
3720 self::$_static_logger->entrance();
3721
3722 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
3723 return false;
3724 }
3725
3726 if ( isset( $this->_is_on ) ) {
3727 return $this->_is_on;
3728 }
3729
3730 // If already installed or pending then sure it's on :)
3731 if ( $this->is_registered() || $this->is_pending_activation() ) {
3732 $this->_is_on = true;
3733
3734 return true;
3735 }
3736
3737 return false;
3738 }
3739
3740 /**
3741 * @author Vova Feldman (@svovaf)
3742 * @since 1.1.7.3
3743 *
3744 * @param bool $flush_if_no_connectivity
3745 *
3746 * @return bool
3747 */
3748 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
3749 if ( ! isset( $this->_storage->connectivity_test ) ) {
3750 // Connectivity test was never executed, or cache was cleared.
3751 return true;
3752 }
3753
3754 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
3755 if ( WP_FS__IS_HTTP_REQUEST ) {
3756 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
3757 // Domain changed.
3758 return true;
3759 }
3760
3761 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
3762 // Server IP changed.
3763 return true;
3764 }
3765 }
3766 }
3767
3768 if ( $this->_storage->connectivity_test['is_connected'] &&
3769 $this->_storage->connectivity_test['is_active']
3770 ) {
3771 // API connected and Freemius is active - no need to run connectivity check.
3772 return false;
3773 }
3774
3775 if ( $flush_if_no_connectivity ) {
3776 /**
3777 * If explicitly asked to flush when no connectivity - do it only
3778 * if at least 10 sec passed from the last API connectivity test.
3779 */
3780 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
3781 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
3782 }
3783
3784 /**
3785 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
3786 */
3787 $version = $this->get_plugin_version();
3788 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
3789 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
3790 return true;
3791 }
3792
3793 return false;
3794 }
3795
3796 /**
3797 * @author Leo Fajardo (@leorw)
3798 * @since 2.5.4
3799 *
3800 * @param bool $is_update
3801 *
3802 * @return bool
3803 */
3804 private function should_turn_fs_on( $is_update = true ) {
3805 if (
3806 empty( $this->_plugin->opt_in_moderation ) ||
3807 ! is_array( $this->_plugin->opt_in_moderation )
3808 ) {
3809 return true;
3810 }
3811
3812 $optin_config = $this->_plugin->opt_in_moderation;
3813
3814 if (
3815 WP_FS__IS_LOCALHOST &&
3816 ( ! isset( $optin_config['localhost'] ) || false !== $optin_config['localhost'] )
3817 ) {
3818 return true;
3819 }
3820
3821 $optin_config_key = $is_update ?
3822 'updates' :
3823 'new';
3824
3825 if ( ! isset( $optin_config[ $optin_config_key ] ) ) {
3826 return true;
3827 }
3828
3829 $visibility_percentage = $optin_config[ $optin_config_key ];
3830
3831 if ( 0 == $visibility_percentage ) {
3832 return false;
3833 }
3834
3835 if ( ! is_numeric( $visibility_percentage ) ) {
3836 return true;
3837 }
3838
3839 $min = 1;
3840 $max = 100;
3841
3842 if ( function_exists( 'random_int' ) ) {
3843 $random = random_int( $min, $max ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.random_intFound
3844 } else {
3845 $random = rand( $min, $max );
3846 }
3847
3848 return ( $random <= $visibility_percentage );
3849 }
3850
3851 /**
3852 * Check if there's any connectivity issue to Freemius API.
3853 *
3854 * @author Vova Feldman (@svovaf)
3855 * @since 1.0.9
3856 *
3857 * @param bool $flush_if_no_connectivity
3858 *
3859 * @return bool|null
3860 */
3861 function has_api_connectivity( $flush_if_no_connectivity = false ) {
3862 $this->_logger->entrance();
3863
3864 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
3865 return $this->_has_api_connection;
3866 }
3867
3868 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
3869 isset( $this->_storage->connectivity_test ) &&
3870 true === $this->_storage->connectivity_test['is_connected']
3871 ) {
3872 $this->clear_connectivity_info();
3873 }
3874
3875 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
3876 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
3877 /**
3878 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
3879 *
3880 * @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.
3881 */
3882 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
3883 $this->is_premium() ||
3884 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3885
3886 return $this->_has_api_connection;
3887 }
3888
3889 if (
3890 ! empty( $this->_storage->connectivity_test ) &&
3891 isset( $this->_storage->connectivity_test['is_active'] )
3892 ) {
3893 $is_connected = isset( $this->_storage->connectivity_test['is_connected'] ) ?
3894 $this->_storage->connectivity_test['is_connected'] :
3895 null;
3896 $is_active = ( $this->_storage->connectivity_test['is_active'] || is_object( $this->_site ) );
3897 } else {
3898 $is_connected = null;
3899 $is_active = $this->should_turn_fs_on( $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() ) );
3900 }
3901
3902 $this->store_connectivity_info( (object) array( 'is_active' => $is_active ), $is_connected );
3903
3904 if ( $is_active ) {
3905 $this->_is_on = true;
3906 }
3907
3908 return $this->_has_api_connection;
3909 }
3910
3911 /**
3912 * @author Leo Fajardo (@leorw)
3913 * @since 2.5.4
3914 */
3915 private function clear_connectivity_info() {
3916 unset( $this->_storage->connectivity_test );
3917
3918 FS_Api::clear_force_http_flag();
3919 }
3920
3921 /**
3922 * @author Vova Feldman (@svovaf)
3923 * @since 1.1.7.4
3924 *
3925 * @param object $pong
3926 * @param bool|null $is_connected
3927 */
3928 private function store_connectivity_info( $pong, $is_connected ) {
3929 $this->_logger->entrance();
3930
3931 $version = $this->get_plugin_version();
3932
3933 if ( false === $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
3934 $is_active = false;
3935 } else {
3936 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
3937 }
3938
3939 $is_active = $this->apply_filters(
3940 'is_on',
3941 $is_active,
3942 $this->is_plugin_update(),
3943 $version
3944 );
3945
3946 $this->_storage->connectivity_test = array(
3947 'is_connected' => $is_connected,
3948 'host' => $_SERVER['HTTP_HOST'],
3949 'server_ip' => WP_FS__REMOTE_ADDR,
3950 'is_active' => $is_active,
3951 'timestamp' => WP_FS__SCRIPT_START_TIME,
3952 // Last version with connectivity attempt.
3953 'version' => $version,
3954 );
3955
3956 $this->_has_api_connection = $is_connected;
3957 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3958 }
3959
3960 /**
3961 * @author Leo Fajardo (@leorw)
3962 * @since 2.5.4
3963 *
3964 * @param bool $is_connected
3965 */
3966 private function update_connectivity_info( $is_connected ) {
3967 $this->store_connectivity_info(
3968 // 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.
3969 (object) array( 'is_active' => true ),
3970 $is_connected
3971 );
3972 }
3973
3974 /**
3975 * Force turning Freemius on.
3976 *
3977 * @author Vova Feldman (@svovaf)
3978 * @since 1.1.8.1
3979 *
3980 * @return bool TRUE if successfully turned on.
3981 */
3982 private function turn_on() {
3983 $this->_logger->entrance();
3984
3985 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
3986 return false;
3987 }
3988
3989 $updated_connectivity = $this->_storage->connectivity_test;
3990 $updated_connectivity['is_active'] = true;
3991 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
3992 $this->_storage->connectivity_test = $updated_connectivity;
3993
3994 $this->_is_on = true;
3995
3996 return true;
3997 }
3998
3999 /**
4000 * Anonymous and unique site identifier (Hash).
4001 *
4002 * @author Vova Feldman (@svovaf)
4003 * @since 1.1.0
4004 *
4005 * @param null|int $blog_id Since 2.0.0
4006 *
4007 * @return string
4008 */
4009 function get_anonymous_id( $blog_id = null ) {
4010 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
4011
4012 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
4013 $key = self::get_unfiltered_site_url( $blog_id, true );
4014
4015 $secure_auth = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
4016 if ( empty( $secure_auth ) ||
4017 false !== strpos( $secure_auth, ' ' ) ||
4018 'put your unique phrase here' === $secure_auth
4019 ) {
4020 // Protect against default auth key.
4021 $secure_auth = md5( microtime() );
4022 }
4023
4024 /**
4025 * Base the unique identifier on the WP secure authentication key. Which
4026 * turns the key into a secret anonymous identifier. This will help us
4027 * to avoid duplicate installs generation on the backend upon opt-in.
4028 *
4029 * @author Vova Feldman (@svovaf)
4030 * @since 1.2.3
4031 */
4032 $unique_id = md5( $key . $secure_auth );
4033
4034 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
4035 }
4036
4037 $this->_logger->departure( $unique_id );
4038
4039 return $unique_id;
4040 }
4041
4042 /**
4043 * Returns anonymous network ID.
4044 *
4045 * @since 2.4.3
4046 *
4047 * @return string
4048 */
4049 function get_anonymous_network_id() {
4050 return $this->get_anonymous_id( get_network()->site_id );
4051 }
4052
4053 /**
4054 * @author Vova Feldman (@svovaf)
4055 * @since 1.1.7.4
4056 *
4057 * @return \WP_User
4058 */
4059 static function _get_current_wp_user() {
4060 self::require_pluggable_essentials();
4061 self::wp_cookie_constants();
4062
4063 return wp_get_current_user();
4064 }
4065
4066 /**
4067 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
4068 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
4069 * is network activated the cookie constants are only configured after the network
4070 * plugins activation, therefore, if we don't define those constants WP will throw
4071 * PHP warnings/notices.
4072 *
4073 * @author Vova Feldman (@svovaf)
4074 * @since 2.1.1
4075 */
4076 private static function wp_cookie_constants() {
4077 if ( defined( 'LOGGED_IN_COOKIE' ) &&
4078 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
4079 ) {
4080 return;
4081 }
4082
4083 /**
4084 * Used to guarantee unique hash cookies
4085 *
4086 * @since 1.5.0
4087 */
4088 if ( ! defined( 'COOKIEHASH' ) ) {
4089 $siteurl = get_site_option( 'siteurl' );
4090 if ( $siteurl ) {
4091 define( 'COOKIEHASH', md5( $siteurl ) );
4092 } else {
4093 define( 'COOKIEHASH', '' );
4094 }
4095 }
4096
4097 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
4098 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
4099 }
4100
4101 /**
4102 * @since 2.5.0
4103 */
4104 if ( ! defined( 'AUTH_COOKIE' ) ) {
4105 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
4106 }
4107
4108 /**
4109 * @since 2.6.0
4110 */
4111 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
4112 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
4113 }
4114 }
4115
4116 /**
4117 * @author Vova Feldman (@svovaf)
4118 * @since 2.1.0
4119 *
4120 * @return int
4121 */
4122 static function get_current_wp_user_id() {
4123 $wp_user = self::_get_current_wp_user();
4124
4125 return $wp_user->ID;
4126 }
4127
4128 /**
4129 * @author Vova Feldman (@svovaf)
4130 * @since 1.2.1.7
4131 *
4132 * @param string $email
4133 *
4134 * @return bool
4135 */
4136 static function is_valid_email( $email ) {
4137 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4138 return false;
4139 }
4140
4141 $parts = explode( '@', $email );
4142
4143 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4144 return false;
4145 }
4146
4147 $blacklist = array(
4148 'admin.',
4149 'webmaster.',
4150 'localhost.',
4151 'dev.',
4152 'development.',
4153 'test.',
4154 'stage.',
4155 'staging.',
4156 );
4157
4158 // Make sure domain is not one of the blacklisted.
4159 foreach ( $blacklist as $invalid ) {
4160 if ( 0 === strpos( $parts[1], $invalid ) ) {
4161 return false;
4162 }
4163 }
4164
4165 // Get the UTF encoded domain name.
4166 /**
4167 * @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.
4168 * @author - @swashata
4169 */
4170 $is_new_idn_available = (
4171 version_compare( PHP_VERSION, '5.6.40') > 0 &&
4172 defined( 'IDNA_DEFAULT' ) &&
4173 defined( 'INTL_IDNA_VARIANT_UTS46' )
4174 );
4175 if ( $is_new_idn_available ) {
4176 $domain = idn_to_ascii( $parts[1], IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 );
4177 } else {
4178 $domain = idn_to_ascii( $parts[1] ); // phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet
4179 }
4180
4181 $domain = $domain . '.';
4182
4183 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4184 }
4185
4186 #endregion
4187
4188 #----------------------------------------------------------------------------------
4189 #region Email
4190 #----------------------------------------------------------------------------------
4191
4192 /**
4193 * Generates and sends an HTML email with customizable sections.
4194 *
4195 * @author Leo Fajardo (@leorw)
4196 * @since 1.1.2
4197 *
4198 * @param string $to_address
4199 * @param string $subject
4200 * @param array $sections
4201 * @param array $headers
4202 *
4203 * @return bool Whether the email contents were sent successfully.
4204 */
4205 private function send_email(
4206 $to_address,
4207 $subject,
4208 $sections = array(),
4209 $headers = array()
4210 ) {
4211 $default_sections = $this->get_email_sections();
4212
4213 // Insert new sections or replace the default email sections.
4214 if ( is_array( $sections ) && ! empty( $sections ) ) {
4215 foreach ( $sections as $section_id => $custom_section ) {
4216 if ( ! isset( $default_sections[ $section_id ] ) ) {
4217 // If the section does not exist, add it.
4218 $default_sections[ $section_id ] = $custom_section;
4219 } else {
4220 // If the section already exists, override it.
4221 $current_section = $default_sections[ $section_id ];
4222
4223 // Replace the current section's title if a custom section title exists.
4224 if ( isset( $custom_section['title'] ) ) {
4225 $current_section['title'] = $custom_section['title'];
4226 }
4227
4228 // Insert new rows under the current section or replace the default rows.
4229 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4230 foreach ( $custom_section['rows'] as $row_id => $row ) {
4231 $current_section['rows'][ $row_id ] = $row;
4232 }
4233 }
4234
4235 $default_sections[ $section_id ] = $current_section;
4236 }
4237 }
4238 }
4239
4240 $vars = array( 'sections' => $default_sections );
4241 $message = fs_get_template( 'email.php', $vars );
4242
4243 // Set the type of email to HTML.
4244 $headers[] = 'Content-type: text/html; charset=UTF-8';
4245
4246 $header_string = implode( "\r\n", $headers );
4247
4248 return wp_mail(
4249 $to_address,
4250 $subject,
4251 $message,
4252 $header_string
4253 );
4254 }
4255
4256 /**
4257 * Generates the data for the sections of the email content.
4258 *
4259 * @author Leo Fajardo (@leorw)
4260 * @since 1.1.2
4261 *
4262 * @return array
4263 */
4264 private function get_email_sections() {
4265 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4266 $current_user = self::_get_current_wp_user();
4267
4268 // Retrieve the cURL version information so that we can get the version number below.
4269 $curl_version_information = curl_version();
4270
4271 $active_plugin = self::get_active_plugins();
4272
4273 // Generate the list of active plugins separated by new line.
4274 $active_plugin_string = '';
4275 foreach ( $active_plugin as $plugin ) {
4276 $active_plugin_string .= sprintf(
4277 '<a href="%s">%s</a> [v%s]<br>',
4278 $plugin['PluginURI'],
4279 $plugin['Name'],
4280 $plugin['Version']
4281 );
4282 }
4283
4284 $server_ip = WP_FS__REMOTE_ADDR;
4285
4286 // Add PHP info for deeper investigation.
4287 ob_start();
4288 phpinfo();
4289 $php_info = ob_get_clean();
4290
4291 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4292
4293 // Generate the default email sections.
4294 $sections = array(
4295 'sdk' => array(
4296 'title' => 'SDK',
4297 'rows' => array(
4298 'fs_version' => array( 'FS Version', $this->version ),
4299 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4300 )
4301 ),
4302 'plugin' => array(
4303 'title' => ucfirst( $this->get_module_type() ),
4304 'rows' => array(
4305 'name' => array( 'Name', $this->get_plugin_name() ),
4306 'version' => array( 'Version', $this->get_plugin_version() )
4307 )
4308 ),
4309 'api' => array(
4310 'title' => 'API Subdomain',
4311 'rows' => array(
4312 'dns' => array(
4313 'DNS_CNAME',
4314 function_exists( 'dns_get_record' ) ?
4315 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4316 'dns_get_record() disabled/blocked'
4317 ),
4318 'ip' => array(
4319 'IP',
4320 function_exists( 'gethostbyname' ) ?
4321 gethostbyname( $api_domain ) :
4322 'gethostbyname() disabled/blocked'
4323 ),
4324 ),
4325 ),
4326 'site' => array(
4327 'title' => 'Site',
4328 'rows' => array(
4329 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4330 'address' => array( 'Address', site_url() ),
4331 'host' => array(
4332 'HTTP_HOST',
4333 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4334 ),
4335 'hosting' => array(
4336 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4337 fs_request_get( 'hosting_company' ) :
4338 'Unknown',
4339 ),
4340 'server_addr' => array(
4341 'SERVER_ADDR',
4342 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4343 )
4344 )
4345 ),
4346 'user' => array(
4347 'title' => 'User',
4348 'rows' => array(
4349 'email' => array( 'Email', $current_user->user_email ),
4350 'first' => array( 'First', $current_user->user_firstname ),
4351 'last' => array( 'Last', $current_user->user_lastname )
4352 )
4353 ),
4354 'plugins' => array(
4355 'title' => 'Plugins',
4356 'rows' => array(
4357 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4358 )
4359 ),
4360 'php_info' => array(
4361 'title' => 'PHP Info',
4362 'rows' => array(
4363 'info' => array( $php_info )
4364 ),
4365 )
4366 );
4367
4368 // Allow the sections to be modified by other code.
4369 $sections = $this->apply_filters( 'email_template_sections', $sections );
4370
4371 return $sections;
4372 }
4373
4374 #endregion
4375
4376 #----------------------------------------------------------------------------------
4377 #region Initialization
4378 #----------------------------------------------------------------------------------
4379
4380 /**
4381 * Init plugin's Freemius instance.
4382 *
4383 * @author Vova Feldman (@svovaf)
4384 * @since 1.0.1
4385 *
4386 * @param number $id
4387 * @param string $public_key
4388 * @param bool $is_live
4389 * @param bool $is_premium
4390 */
4391 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4392 $this->_logger->entrance();
4393
4394 $this->dynamic_init( array(
4395 'id' => $id,
4396 'public_key' => $public_key,
4397 'is_live' => $is_live,
4398 'is_premium' => $is_premium,
4399 ) );
4400 }
4401
4402 /**
4403 * Dynamic initiator, originally created to support initiation
4404 * with parent_id for add-ons.
4405 *
4406 * @author Vova Feldman (@svovaf)
4407 * @since 1.0.6
4408 *
4409 * @param array $plugin_info
4410 *
4411 * @throws Freemius_Exception
4412 */
4413 function dynamic_init( array $plugin_info ) {
4414 $this->_logger->entrance();
4415
4416 $this->parse_settings( $plugin_info );
4417
4418 $this->register_after_settings_parse_hooks();
4419
4420 /**
4421 * 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.
4422 *
4423 * @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.
4424 */
4425 if ( $this->is_anonymous() ) {
4426 $is_network_level = ( $this->_is_network_active && fs_is_network_admin() );
4427
4428 if (
4429 ! $is_network_level ||
4430 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
4431 ) {
4432 if ( $this->is_paying_or_trial() ) {
4433 $this->reset_anonymous_mode( $is_network_level );
4434 }
4435 } else {
4436 $network = get_network();
4437
4438 if ( is_object( $network ) ) {
4439 $main_blog_id = $network->site_id;
4440 $first_install = $this->get_install_by_blog_id( $main_blog_id );
4441
4442 if ( is_object( $first_install ) ) {
4443 $this->_storage->network_install_blog_id = $main_blog_id;
4444 $this->_storage->network_user_id = $first_install->user_id;
4445 }
4446 }
4447 }
4448 }
4449
4450 if ( $this->should_stop_execution() ) {
4451 return;
4452 }
4453
4454 if ( ! $this->is_registered() ) {
4455 if ( $this->is_anonymous() ) {
4456 // If user skipped, no need to test connectivity.
4457 $this->_has_api_connection = true;
4458 $this->_is_on = true;
4459 } else {
4460 if ( false === $this->has_api_connectivity() ) {
4461 return;
4462 } else {
4463 if ( $this->_anonymous_mode ) {
4464 // Simulate anonymous mode.
4465 $this->_is_anonymous = true;
4466 }
4467 }
4468 }
4469 }
4470
4471 /**
4472 * This should be executed even if Freemius is off for the core module,
4473 * otherwise, the add-ons dialog box won't work properly. This is especially
4474 * relevant when the developer decided to turn FS off for existing users.
4475 *
4476 * @author Vova Feldman (@svovaf)
4477 */
4478 if ( $this->is_user_in_admin() &&
4479 'plugin-information' === fs_request_get( 'tab', false ) &&
4480 $this->should_use_freemius_updater_and_dialog() &&
4481 (
4482 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4483 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4484 )
4485 ) {
4486 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4487
4488 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4489 }
4490
4491 // Check if Freemius is on for the current plugin.
4492 // This MUST be executed after all the plugin variables has been loaded.
4493 if ( ! $this->is_registered() && ! $this->is_on() ) {
4494 return;
4495 }
4496
4497 if ( $this->has_api_connectivity() ) {
4498 if ( self::is_cron() ) {
4499 $this->hook_callback_to_sync_cron();
4500 } else if ( $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
4527 if ( $this->is_registered() ) {
4528 FS_Clone_Manager::instance()->maybe_resolve_new_subsite_install_automatically( $this );
4529
4530 $this->hook_callback_to_install_sync();
4531 }
4532
4533 if ( $this->is_addon() ) {
4534 if ( $this->is_parent_plugin_installed() ) {
4535 // Link to parent FS.
4536 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4537
4538 // Get parent plugin reference.
4539 $this->_parent_plugin = $this->_parent->get_plugin();
4540 }
4541 }
4542
4543 if ( $this->is_user_in_admin() ) {
4544 if ( $this->is_registered() && fs_request_has( 'purchase_completed' ) ) {
4545 $this->_admin_notices->add_sticky(
4546 sprintf(
4547 /* translators: %s: License type (e.g. you have a professional license) */
4548 $this->get_text_inline( 'You have purchased a %s license.', 'you-have-x-license' ),
4549 fs_request_get( 'purchased_plan' )
4550 ) .
4551 sprintf(
4552 $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' ),
4553 $this->get_module_label( true ),
4554 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? "products' " : '' ),
4555 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? 's' : '' ),
4556 sprintf(
4557 '<strong>%s</strong>',
4558 fs_request_get( 'purchase_email' )
4559 )
4560 ),
4561 'plan_purchased',
4562 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
4563 );
4564 }
4565
4566 if ( $this->is_addon() ) {
4567 if ( ! $this->is_parent_plugin_installed() ) {
4568 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4569
4570 if ( isset( $plugin_info['parent'] ) ) {
4571 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4572 }
4573
4574 $this->_admin_notices->add(
4575 ( ! empty( $parent_name ) ?
4576 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 ) :
4577 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() )
4578 ),
4579 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4580 'error'
4581 );
4582
4583 return;
4584 } else {
4585 $is_network_admin = fs_is_network_admin();
4586
4587 if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4588 // If add-on activated and parent not, automatically install parent for the user.
4589 $this->activate_parent_account( $this->_parent );
4590 } else if (
4591 $this->_parent->is_registered() &&
4592 ! $this->is_registered() &&
4593 /**
4594 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
4595 * * Network active and in network admin - network activate add-on account.
4596 * * Network active and not in network admin - activate add-on account for the current blog.
4597 * * Not network active and not in network admin - activate add-on account for the current blog.
4598 *
4599 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
4600 *
4601 * @author Leo Fajardo (@leorw)
4602 * @since 2.3.0
4603 */
4604 ( $this->is_network_active() || ! $is_network_admin )
4605 ) {
4606 $premium_license = null;
4607
4608 if (
4609 ! $this->has_free_plan() &&
4610 $this->is_bundle_license_auto_activation_enabled() &&
4611 $this->_parent->is_activated_with_bundle_license()
4612 ) {
4613 /**
4614 * If the add-on has no free plan, try to activate the account only when there's a bundle license.
4615 *
4616 * @author Leo Fajardo (@leorw)
4617 * @since 2.4.0
4618 */
4619 $bundle_license = $this->get_active_parent_license( $this->_parent->_get_license()->secret_key, false );
4620
4621 if (
4622 is_object( $bundle_license ) &&
4623 ! empty( $bundle_license->products ) &&
4624 in_array( $this->get_id(), $bundle_license->products )
4625 ) {
4626 $premium_license = $bundle_license;
4627 }
4628 }
4629
4630 if ( $this->has_free_plan() || is_object( $premium_license) ) {
4631 // If parent plugin activated, automatically install add-on for the user.
4632 $this->_activate_addon_account(
4633 $this->_parent,
4634 ( $this->is_network_active() && $is_network_admin ) ?
4635 true :
4636 get_current_blog_id(),
4637 $premium_license
4638 );
4639 }
4640 }
4641
4642 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4643 if ( $this->is_premium() ) {
4644 // Remove add-on download admin-notice.
4645 $this->_parent->_admin_notices->remove_sticky( array(
4646 'addon_plan_upgraded_' . $this->_slug,
4647 'no_addon_license_' . $this->_slug,
4648 ) );
4649 }
4650
4651 // $this->deactivate_premium_only_addon_without_license();
4652 }
4653 }
4654
4655 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
4656
4657 // if ( $this->is_registered() ||
4658 // $this->is_anonymous() ||
4659 // $this->is_pending_activation()
4660 // ) {
4661 // $this->_init_admin();
4662 // }
4663 }
4664
4665 /**
4666 * Should be called outside `$this->is_user_in_admin()` scope
4667 * because the updater has some logic that needs to be executed
4668 * during AJAX calls.
4669 *
4670 * Currently, we need to hook to the `http_request_host_is_external` filter.
4671 * In the future, there might be additional logic added.
4672 *
4673 * @author Vova Feldman
4674 * @since 1.2.1.6
4675 */
4676 if (
4677 $this->should_use_freemius_updater_and_dialog() &&
4678 (
4679 $this->is_premium() ||
4680 /**
4681 * If not premium but the premium version is installed, also instantiate the updater so that the
4682 * plugin information dialog of the premium version will have the information from the server.
4683 *
4684 * @author Leo Fajardo (@leorw)
4685 * @since 2.2.3
4686 */
4687 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
4688 ) &&
4689 $this->has_release_on_freemius() &&
4690 ( ! $this->is_unresolved_clone( true ) )
4691 ) {
4692 FS_Plugin_Updater::instance( $this );
4693 }
4694
4695 $this->do_action( 'initiated' );
4696
4697 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
4698 if ( isset( $this->_storage->prev_is_premium ) ) {
4699 $this->apply_filters(
4700 'after_code_type_change',
4701 // New code type.
4702 $this->_plugin->is_premium
4703 );
4704 } else {
4705 // Set for code type for the first time.
4706 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
4707 }
4708 }
4709
4710 if ( ! $this->is_addon() ) {
4711 if ( $this->is_registered() ) {
4712 // Fix for upgrade from versions < 1.0.9.
4713 if ( ! isset( $this->_storage->activation_timestamp ) ) {
4714 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
4715 }
4716
4717 $this->do_action( 'after_init_plugin_registered' );
4718 } else if ( $this->is_anonymous() ) {
4719 $this->do_action( 'after_init_plugin_anonymous' );
4720 } else if ( $this->is_pending_activation() ) {
4721 $this->do_action( 'after_init_plugin_pending_activations' );
4722 }
4723 } else {
4724 if ( $this->is_registered() ) {
4725 $this->do_action( 'after_init_addon_registered' );
4726 } else if ( $this->is_anonymous() ) {
4727 $this->do_action( 'after_init_addon_anonymous' );
4728 } else if ( $this->is_pending_activation() ) {
4729 $this->do_action( 'after_init_addon_pending_activations' );
4730 }
4731 }
4732 }
4733
4734 /**
4735 * @author Leo Fajardo (@leorw)
4736 * @since 2.2.3
4737 *
4738 * @return bool
4739 */
4740 private function should_use_freemius_updater_and_dialog() {
4741 return (
4742 /**
4743 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
4744 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
4745 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
4746 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
4747 * plugin details from .org).
4748 */
4749 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
4750 (
4751 ! self::is_plugin_install_page() &&
4752 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
4753 ( 'install-plugin' !== fs_request_get( 'action' ) )
4754 )
4755 );
4756 }
4757
4758 /**
4759 * @param string[] $permissions
4760 * @param bool $is_enabled
4761 * @param int|null $blog_id
4762 *
4763 * @return true|object `true` on success, API error object on failure.
4764 */
4765 private function update_site_permissions( array $permissions, $is_enabled, $blog_id = null ) {
4766 $this->_logger->entrance();
4767
4768 $params = array(
4769 'permissions' => implode( ',', $permissions ),
4770 'is_enabled' => $is_enabled,
4771 );
4772
4773 $current_blog_id = get_current_blog_id();
4774 $is_blog_switched = false;
4775 if ( is_numeric( $blog_id ) && $current_blog_id != $blog_id ) {
4776 $is_blog_switched = $this->switch_to_blog( $blog_id );
4777 }
4778
4779 $result = $this->api_site_call( '/permissions.json', 'put', $params );
4780
4781 if ( $is_blog_switched ) {
4782 $this->switch_to_blog( $current_blog_id );
4783 }
4784
4785 if (
4786 ! $this->is_api_result_object( $result ) ||
4787 ! isset( $result->install_id )
4788 ) {
4789 $this->_logger->api_error( $result );
4790
4791 return $result;
4792 }
4793
4794 return true;
4795 }
4796
4797 /**
4798 * @param string[] $permissions
4799 * @param bool $is_enabled
4800 * @param bool $has_site_delegated_connection
4801 *
4802 * @return true|object `true` on success, API error object on failure.
4803 */
4804 private function update_network_permissions(
4805 array $permissions,
4806 $is_enabled,
4807 &$has_site_delegated_connection
4808 ) {
4809 $this->_logger->entrance();
4810
4811 $install_id_2_blog_id = array();
4812 $install_by_blog_id = $this->get_blog_install_map();
4813
4814 $has_site_delegated_connection = false;
4815
4816 foreach ( $install_by_blog_id as $blog_id => $install ) {
4817 if ( $this->is_site_delegated_connection( $blog_id ) ) {
4818 // Only update permissions of non-delegated installs.
4819 $has_site_delegated_connection = true;
4820 continue;
4821 }
4822
4823 $install_id_2_blog_id[ $install->id ] = $blog_id;
4824 }
4825
4826 if ( empty( $install_id_2_blog_id ) ) {
4827 return true;
4828 }
4829
4830 $params = array(
4831 'permissions' => implode( ',', $permissions ),
4832 'is_enabled' => $is_enabled,
4833 'install_ids' => implode( ',', array_keys( $install_id_2_blog_id ) ),
4834 );
4835
4836 // Send update to FS.
4837 $result = $this->get_current_or_network_user_api_scope()->call(
4838 "/plugins/{$this->_module_id}/installs/permissions.json",
4839 'put',
4840 $params
4841 );
4842
4843 if ( ! $this->is_api_result_object( $result, 'installs_metadata' ) ) {
4844 $this->_logger->api_error( $result );
4845
4846 return $result;
4847 }
4848
4849 return true;
4850 }
4851
4852 /**
4853 * @param mixed $result
4854 *
4855 * @return string
4856 */
4857 private function get_api_error_message( $result ) {
4858 $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:',
4859 'unexpected-api-error' ), $this->_module_type ) . ' ';
4860
4861 if (
4862 $this->is_api_error( $result ) &&
4863 isset( $result->error )
4864 ) {
4865 $code = empty( $result->error->code ) ? '' : " Code: {$result->error->code}";
4866
4867 $error_message .= "<b>{$result->error->message}{$code}</b>";
4868 } else {
4869 $error_message .= var_export( $result, true );
4870 }
4871
4872 return $error_message;
4873 }
4874
4875 /**
4876 * @author Vova Feldman (@svovaf)
4877 * @since 2.5.1
4878 */
4879 function _toggle_permission_tracking_callback() {
4880 $this->_logger->entrance();
4881
4882 $this->check_ajax_referer( 'toggle_permission_tracking' );
4883
4884 if ( ! $this->is_registered( true ) ) {
4885 self::shoot_ajax_failure( 'User never opted-in.' );
4886 }
4887
4888 $is_enabled = fs_request_get_bool( 'is_enabled' );
4889 $permissions = fs_request_get( 'permissions' );
4890
4891 if ( ! is_string( $permissions ) ) {
4892 self::shoot_ajax_failure( 'The permissions param must be a string.' );
4893 }
4894
4895 $permissions = explode( ',', $permissions );
4896
4897 $result = $this->toggle_permission_tracking( $permissions, $is_enabled );
4898
4899 if ( true !== $result ) {
4900 self::shoot_ajax_failure( $this->get_api_error_message( $result ) );
4901 }
4902
4903 self::shoot_ajax_success();
4904 }
4905
4906 /**
4907 * @param string[] $permissions
4908 * @param bool $is_enabled
4909 * @param int|null $blog_id
4910 *
4911 * @return bool|mixed `true` if updated successfully or no update is needed.
4912 */
4913 private function toggle_permission_tracking( $permissions, $is_enabled, $blog_id = null ) {
4914 if ( ! $this->is_registered( true ) ) {
4915 // User never opted-in.
4916 return false;
4917 }
4918
4919 // Check if permissions are already set as needed.
4920 if ( FS_Permission_Manager::instance( $this )->are_permissions( $permissions, $is_enabled, $blog_id ) ) {
4921 /**
4922 * Note:
4923 * 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.
4924 */
4925 return true;
4926 }
4927
4928 $api_managed_permissions = array_intersect(
4929 $permissions,
4930 FS_Permission_Manager::get_api_managed_permission_ids()
4931 );
4932
4933 if (
4934 in_array( FS_Permission_Manager::PERMISSION_ESSENTIALS, $permissions ) &&
4935 ! in_array( FS_Permission_Manager::PERMISSION_SITE, $permissions )
4936 ) {
4937 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_SITE;
4938 }
4939
4940 if ( ! empty( $api_managed_permissions ) ) {
4941 $has_site_delegated_connection = false;
4942
4943 if (
4944 ! $is_enabled &&
4945 ! in_array( FS_Permission_Manager::PERMISSION_EXTENSIONS, $api_managed_permissions ) &&
4946 false === FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed( $blog_id )
4947 ) {
4948 /**
4949 * 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.
4950 *
4951 * @todo Remove this entire `if` after implementing granular opt-in that also sends the permissions to the API when opting in.
4952 */
4953 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_EXTENSIONS;
4954 }
4955
4956 if ( is_null( $blog_id ) && fs_is_network_admin() ) {
4957 $result = $this->update_network_permissions(
4958 $api_managed_permissions,
4959 $is_enabled,
4960 $has_site_delegated_connection
4961 );
4962 } else {
4963 $result = $this->update_site_permissions(
4964 $api_managed_permissions,
4965 $is_enabled,
4966 $blog_id
4967 );
4968 }
4969
4970 if ( true !== $result ) {
4971 return $result;
4972 }
4973
4974 if ( in_array( FS_Permission_Manager::PERMISSION_SITE, $api_managed_permissions ) ) {
4975 if ( $is_enabled ) {
4976 $this->schedule_sync_cron();
4977 } else {
4978 $this->clear_sync_cron( ! $has_site_delegated_connection );
4979 }
4980 }
4981
4982 if ( in_array( FS_Permission_Manager::PERMISSION_USER, $api_managed_permissions ) ) {
4983 $this->toggle_user_permission( $is_enabled, $blog_id );
4984 }
4985 }
4986
4987 $this->update_tracking_permissions(
4988 $permissions,
4989 $is_enabled,
4990 $blog_id
4991 );
4992
4993 return true;
4994 }
4995
4996 /**
4997 * @param bool $is_enabled
4998 * @param int|null $blog_id
4999 */
5000 private function toggle_user_permission( $is_enabled, $blog_id = null ) {
5001 $network_or_blog_ids = is_numeric( $blog_id ) ?
5002 $blog_id :
5003 fs_is_network_admin();
5004
5005 if ( $is_enabled ) {
5006 $this->reset_anonymous_mode( $network_or_blog_ids );
5007 } else {
5008 $this->skip_connection( $network_or_blog_ids );
5009 }
5010 }
5011
5012 /**
5013 * Opt-in back into usage tracking.
5014 *
5015 * Note: This will only work if the user opted-in previously.
5016 *
5017 * Returns:
5018 * 1. FALSE - If the user never opted-in.
5019 * 2. TRUE - If successfully opted-in back to usage tracking.
5020 * 3. object - API result on failure.
5021 *
5022 * @author Leo Fajardo (@leorw)
5023 * @since 1.2.1.5
5024 *
5025 * @bool $is_enabled
5026 *
5027 * @return bool|object
5028 */
5029 private function toggle_site_tracking( $is_enabled, $blog_id = null ) {
5030 $this->_logger->entrance();
5031
5032 return $this->toggle_permission_tracking(
5033 FS_Permission_Manager::instance( $this )->get_site_tracking_permission_names(),
5034 $is_enabled,
5035 $blog_id
5036 );
5037 }
5038
5039 /**
5040 * If user opted-in and later disabled usage-tracking,
5041 * re-allow tracking for licensing and updates.
5042 *
5043 * @author Leo Fajardo (@leorw)
5044 * @since 1.2.1.5
5045 *
5046 * @param bool $is_context_single_site
5047 */
5048 private function reconnect_locally( $is_context_single_site = false ) {
5049 $this->_logger->entrance();
5050
5051 if ( ! $this->is_registered() ) {
5052 return;
5053 }
5054
5055 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5056 if ( $this->is_tracking_prohibited() ) {
5057 FS_Permission_Manager::instance( $this )->update_site_tracking( true );
5058 }
5059 } else {
5060 $installs_map = $this->get_blog_install_map();
5061 foreach ( $installs_map as $blog_id => $install ) {
5062 /**
5063 * @var FS_Site $install
5064 */
5065 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
5066 FS_Permission_Manager::instance( $this )->update_site_tracking( true, $blog_id );
5067 }
5068 }
5069 }
5070 }
5071
5072 /**
5073 * 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.
5074 *
5075 * @param string[] $permissions
5076 * @param bool $is_enabled
5077 * @param int|null $blog_id
5078 *
5079 * @return array
5080 */
5081 private function update_tracking_permissions( $permissions, $is_enabled, $blog_id = null ) {
5082 // Alias.
5083 $permission_manager = FS_Permission_Manager::instance( $this );
5084
5085 $network_or_blog_ids = is_numeric( $blog_id ) ?
5086 $blog_id :
5087 fs_is_network_admin();
5088
5089 if ( true === $network_or_blog_ids ) {
5090 // Update the permission for all non-delegated sub-sites.
5091 $blog_ids = $this->get_non_delegated_blog_ids();
5092
5093 // Add the network-level to the array, to update the permission on the network-level storage.
5094 array_unshift( $blog_ids, null );
5095 }
5096 else
5097 {
5098 if ( false === $network_or_blog_ids ) {
5099 $network_or_blog_ids = null;
5100 }
5101
5102 $blog_ids = is_array( $network_or_blog_ids ) ?
5103 $network_or_blog_ids :
5104 array( $network_or_blog_ids );
5105 }
5106
5107 $result = array();
5108 foreach ( $permissions as $permission ) {
5109 $permission = trim( $permission );
5110 $is_permission_supported = true;
5111
5112 foreach ( $blog_ids as $id ) {
5113 $is_permission_supported = $permission_manager->update_permission_tracking_flag(
5114 $permission,
5115 $is_enabled,
5116 $id
5117 );
5118 }
5119
5120 if ( ! $is_permission_supported ) {
5121 $permission = 'no_match';
5122 }
5123
5124 $result[ $permission ] = $is_enabled;
5125 }
5126
5127 return $result;
5128 }
5129
5130 /**
5131 * Parse plugin's settings (as defined by the plugin dev).
5132 *
5133 * @author Vova Feldman (@svovaf)
5134 * @since 1.1.7.3
5135 *
5136 * @param array $plugin_info
5137 *
5138 * @throws \Freemius_Exception
5139 */
5140 private function parse_settings( &$plugin_info ) {
5141 $this->_logger->entrance();
5142
5143 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5144 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5145 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5146 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5147 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5148
5149 /**
5150 * @author Vova Feldman (@svovaf)
5151 * @since 1.1.9 Try to pull secret key from external config.
5152 */
5153 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5154 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5155 }
5156
5157 if ( isset( $plugin_info['parent'] ) ) {
5158 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5159 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5160 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5161 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5162 }
5163
5164 if ( false === $id ) {
5165 throw new Freemius_Exception( array(
5166 'error' => array(
5167 'type' => 'ParameterNotSet',
5168 'message' => 'Plugin id parameter is not set.',
5169 'code' => 'plugin_id_not_set',
5170 'http' => 500,
5171 )
5172 ) );
5173 }
5174 if ( false === $public_key ) {
5175 throw new Freemius_Exception( array(
5176 'error' => array(
5177 'type' => 'ParameterNotSet',
5178 'message' => 'Plugin public_key parameter is not set.',
5179 'code' => 'plugin_public_key_not_set',
5180 'http' => 500,
5181 )
5182 ) );
5183 }
5184
5185 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5186 $this->_plugin :
5187 new FS_Plugin();
5188
5189 $is_premium = $this->get_bool_option( $plugin_info, 'is_premium', true );
5190 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5191
5192 $module_type = $this->get_option( $plugin_info, 'type', $this->_module_type );
5193
5194 $parallel_activation = $this->get_option( $plugin_info, 'parallel_activation' );
5195
5196 if (
5197 ! $is_premium &&
5198 is_array( $parallel_activation ) &&
5199 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type ) &&
5200 $this->get_bool_option( $parallel_activation, 'enabled' )
5201 ) {
5202 $premium_basename = $this->get_option( $parallel_activation, 'premium_version_basename' );
5203
5204 if ( empty( $premium_basename ) ) {
5205 throw new Exception('You need to specify the premium version basename to enable parallel version activation.');
5206 }
5207
5208 $this->_premium_plugin_basename_from_parallel_activation = $premium_basename;
5209
5210 if ( is_plugin_active( $premium_basename ) ) {
5211 $is_premium = true;
5212 }
5213 }
5214
5215 $plugin->update( array(
5216 'id' => $id,
5217 'type' => $module_type,
5218 'public_key' => $public_key,
5219 'slug' => $this->_slug,
5220 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5221 'parent_plugin_id' => $parent_id,
5222 'version' => $this->get_plugin_version(),
5223 'title' => $this->get_plugin_name( $premium_suffix ),
5224 'file' => $this->_plugin_basename,
5225 'is_premium' => $is_premium,
5226 'premium_suffix' => $premium_suffix,
5227 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5228 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5229 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5230 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ),
5231 'opt_in_moderation' => $this->get_option(
5232 $plugin_info,
5233 'opt_in',
5234 // For backward compatibility, we support both parameter names: opt_in and opt_in_moderation.
5235 $this->get_option( $plugin_info, 'opt_in_moderation', null )
5236 ),
5237 ) );
5238
5239 if ( $plugin->is_updated() ) {
5240 // Update plugin details.
5241 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5242 }
5243 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5244 $this->_plugin->secret_key = $secret_key;
5245
5246 /**
5247 * 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).
5248 *
5249 * @author Vova Feldman
5250 * @since 2.4.5
5251 */
5252 if ( $this->is_network_active() && fs_is_network_admin() ) {
5253 if ( isset( $plugin_info['menu_network'] ) &&
5254 is_array( $plugin_info['menu_network'] ) &&
5255 ! empty( $plugin_info['menu_network'] )
5256 ) {
5257 $plugin_info['menu'] = $plugin_info['menu_network'];
5258 }
5259 }
5260
5261 if ( ! isset( $plugin_info['menu'] ) ) {
5262 $plugin_info['menu'] = array();
5263
5264 if ( ! empty( $this->_storage->sdk_last_version ) &&
5265 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5266 ) {
5267 // Backward compatibility to 1.1.2
5268 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5269 $plugin_info['menu_slug'] :
5270 $this->_slug;
5271 }
5272 }
5273
5274 $this->_menu = FS_Admin_Menu_Manager::instance(
5275 $this->_module_id,
5276 $this->_module_type,
5277 $this->get_unique_affix()
5278 );
5279
5280 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5281
5282 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5283 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5284 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5285 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5286 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5287 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5288 if ( $this->_is_premium_only ) {
5289 // If premium only plugin, disable anonymous mode.
5290 $this->_enable_anonymous = false;
5291 $this->_anonymous_mode = false;
5292 } else {
5293 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5294 $this->_anonymous_mode = (
5295 $this->get_bool_option( $plugin_info, 'anonymous_mode', false ) ||
5296 (
5297 $this->apply_filters( 'playground_anonymous_mode', true ) &&
5298 ! empty( $_SERVER['HTTP_HOST'] ) &&
5299 FS_Site::is_playground_wp_environment_by_host( $_SERVER['HTTP_HOST'] )
5300 )
5301 );
5302 }
5303 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5304 $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
5305
5306 if ( ! empty( $plugin_info['trial'] ) ) {
5307 $this->_trial_days = $this->get_numeric_option(
5308 $plugin_info['trial'],
5309 'days',
5310 // Default to 0 - trial without days specification.
5311 0
5312 );
5313
5314 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5315 }
5316
5317 $this->_navigation = $this->get_option(
5318 $plugin_info,
5319 'navigation',
5320 $this->is_free_wp_org_theme() ?
5321 self::NAVIGATION_TABS :
5322 self::NAVIGATION_MENU
5323 );
5324 }
5325
5326 /**
5327 * @param string[] $options
5328 * @param string $key
5329 * @param mixed $default
5330 *
5331 * @return bool
5332 */
5333 private function get_option( &$options, $key, $default = false ) {
5334 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5335 }
5336
5337 private function get_bool_option( &$options, $key, $default = false ) {
5338 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5339 }
5340
5341 private function get_numeric_option( &$options, $key, $default = false ) {
5342 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5343 }
5344
5345 /**
5346 * Gate keeper.
5347 *
5348 * @author Vova Feldman (@svovaf)
5349 * @since 1.1.7.3
5350 *
5351 * @return bool
5352 */
5353 private function should_stop_execution() {
5354 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5355 /**
5356 * Don't execute Freemius until plugin was fully loaded at least once,
5357 * to give the opportunity for the activation hook to run before pinging
5358 * the API for connectivity test. This logic is relevant for the
5359 * identification of new plugin install vs. plugin update.
5360 *
5361 * @author Vova Feldman (@svovaf)
5362 * @since 1.1.9
5363 */
5364 return true;
5365 }
5366
5367 if ( $this->is_activation_mode() ) {
5368 if ( ! is_admin() ) {
5369 /**
5370 * If in activation mode, don't execute Freemius outside the admin dashboard.
5371 *
5372 * @author Vova Feldman (@svovaf)
5373 * @since 1.1.7.3
5374 */
5375 return true;
5376 }
5377
5378 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5379 /**
5380 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5381 * then don't start Freemius.
5382 *
5383 * @author Vova Feldman (@svovaf)
5384 * @since 1.1.6.3
5385 *
5386 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5387 */
5388 return true;
5389 }
5390
5391 if ( self::is_cron() ) {
5392 /**
5393 * If in activation mode, don't execute Freemius during wp crons
5394 * (wp crons have HTTP context - called as HTTP request).
5395 *
5396 * @author Vova Feldman (@svovaf)
5397 * @since 1.1.7.3
5398 */
5399 return true;
5400 }
5401
5402 if ( self::is_ajax() ) {
5403 /**
5404 * During activation, if running in AJAX mode, unless there's a sticky
5405 * connectivity issue notice, don't run Freemius.
5406 *
5407 * @author Vova Feldman (@svovaf)
5408 * @since 1.1.7.3
5409 */
5410 return true;
5411 }
5412 }
5413
5414 return false;
5415 }
5416
5417 /**
5418 * Triggered after code type has changed.
5419 *
5420 * @author Vova Feldman (@svovaf)
5421 * @since 1.1.9.1
5422 */
5423 function _after_code_type_change() {
5424 $this->_logger->entrance();
5425
5426 if ( $this->is_theme() ) {
5427 // Expire the cache of the previous tabs since the theme may
5428 // have setting updates after code type has changed.
5429 $this->_cache->expire( 'tabs' );
5430 $this->_cache->expire( 'tabs_stylesheets' );
5431 }
5432
5433 if ( ! $this->is_addon() ) {
5434 add_action(
5435 is_admin() ? 'admin_init' : 'init',
5436 array( &$this, '_plugin_code_type_changed' )
5437 );
5438 }
5439
5440 if ( $this->is_registered() && $this->is_premium() ) {
5441 // Purge cached payments after switching to the premium version.
5442 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5443 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5444 }
5445 }
5446
5447 /**
5448 * Handles plugin's code type change (free <--> premium).
5449 *
5450 * @author Vova Feldman (@svovaf)
5451 * @since 1.0.9
5452 */
5453 function _plugin_code_type_changed() {
5454 $this->_logger->entrance();
5455
5456 if ( $this->is_premium() ) {
5457 $this->reconnect_locally();
5458
5459 // Activated premium code.
5460 $this->do_action( 'after_premium_version_activation' );
5461
5462 // Remove all sticky messages related to download of the premium version.
5463 $this->_admin_notices->remove_sticky( array(
5464 'trial_started',
5465 'plan_upgraded',
5466 'plan_changed',
5467 'license_activated',
5468 ) );
5469
5470 $notice = '';
5471 if ( ! $this->is_only_premium() ) {
5472 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5473 }
5474
5475 $license_notice = $this->get_license_network_activation_notice();
5476 if ( ! empty( $license_notice ) ) {
5477 $notice .= ' ' . $license_notice;
5478 }
5479
5480 if ( ! empty( $notice ) ) {
5481 $this->_admin_notices->add_sticky(
5482 trim( $notice ),
5483 'premium_activated',
5484 $this->get_text_x_inline( 'W00t',
5485 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5486 );
5487 }
5488 } else {
5489 // Remove sticky message related to premium code activation.
5490 $this->_admin_notices->remove_sticky( 'premium_activated' );
5491
5492 // Activated free code (after had the premium before).
5493 $this->do_action( 'after_free_version_reactivation' );
5494
5495 if ( $this->is_paying() && ! $this->is_premium() ) {
5496 $this->add_complete_upgrade_instructions_notice(
5497 sprintf(
5498 /* translators: %s: License type (e.g. you have a professional license) */
5499 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5500 $this->get_plan_title()
5501 ),
5502 'plan_upgraded'
5503 );
5504 }
5505 }
5506
5507 if ( $this->is_registered() ) {
5508 // Schedule code type changes event.
5509 $this->maybe_schedule_install_sync_cron();
5510 }
5511
5512 /**
5513 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5514 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5515 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5516 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5517 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5518 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5519 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5520 */
5521 $this->unregister_uninstall_hook();
5522
5523 $this->clear_module_main_file_cache();
5524
5525 // Update is_premium of latest version.
5526 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5527 }
5528
5529 #endregion
5530
5531 #----------------------------------------------------------------------------------
5532 #region Add-ons
5533 #----------------------------------------------------------------------------------
5534
5535 /**
5536 * Check if add-on installed and activated on site.
5537 *
5538 * @author Vova Feldman (@svovaf)
5539 * @since 1.0.6
5540 *
5541 * @param string|number $id_or_slug
5542 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5543 *
5544 * @return bool
5545 */
5546 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5547 $this->_logger->entrance();
5548
5549 $addon_id = self::get_module_id( $id_or_slug );
5550 $is_activated = self::has_instance( $addon_id );
5551
5552 if ( ! $is_activated ) {
5553 return false;
5554 }
5555
5556 if ( is_bool( $is_premium ) ) {
5557 // Check if the specified code version is activate.
5558 $addon = $this->get_addon_instance( $addon_id );
5559 $is_activated = ( $is_premium === $addon->is_premium() );
5560 }
5561
5562 return $is_activated;
5563 }
5564
5565 /**
5566 * Check if add-on was connected to install
5567 *
5568 * @author Vova Feldman (@svovaf)
5569 * @since 1.1.7
5570 *
5571 * @param string|number $id_or_slug
5572 *
5573 * @return bool
5574 */
5575 function is_addon_connected( $id_or_slug ) {
5576 $this->_logger->entrance();
5577
5578 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5579
5580 $addon_id = self::get_module_id( $id_or_slug );
5581 $addon = $this->get_addon( $addon_id );
5582 $slug = $addon->slug;
5583 if ( ! isset( $sites[ $slug ] ) ) {
5584 return false;
5585 }
5586
5587 $site = $sites[ $slug ];
5588
5589 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5590
5591 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5592 // The given slug do NOT belong to any of the plugin's add-ons.
5593 return false;
5594 }
5595
5596 return ( is_object( $site ) &&
5597 is_numeric( $site->id ) &&
5598 is_numeric( $site->user_id ) &&
5599 FS_Plugin_Plan::is_valid_id( $site->plan_id )
5600 );
5601 }
5602
5603 /**
5604 * Determines if add-on installed.
5605 *
5606 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
5607 *
5608 * @author Vova Feldman (@svovaf)
5609 * @since 1.0.6
5610 *
5611 * @param string|number $id_or_slug
5612 *
5613 * @return bool
5614 */
5615 function is_addon_installed( $id_or_slug ) {
5616 $this->_logger->entrance();
5617
5618 $addon_id = self::get_module_id( $id_or_slug );
5619
5620 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
5621 }
5622
5623 /**
5624 * Get add-on basename.
5625 *
5626 * @author Vova Feldman (@svovaf)
5627 * @since 1.0.6
5628 *
5629 * @param string|number $id_or_slug
5630 *
5631 * @return string
5632 */
5633 function get_addon_basename( $id_or_slug ) {
5634 $addon_id = self::get_module_id( $id_or_slug );
5635
5636 if ( $this->is_addon_activated( $addon_id ) ) {
5637 return self::instance( $addon_id )->get_plugin_basename();
5638 }
5639
5640 $addon = $this->get_addon( $addon_id );
5641 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
5642
5643 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
5644 return $premium_basename;
5645 }
5646
5647 $all_plugins = $this->get_all_plugins();
5648
5649 foreach ( $all_plugins as $basename => $data ) {
5650 if ( $addon->slug === $data['slug'] ||
5651 $addon->premium_slug === $data['slug']
5652 ) {
5653 return $basename;
5654 }
5655 }
5656
5657 $free_basename = "{$addon->slug}/{$addon->slug}.php";
5658
5659 return $free_basename;
5660 }
5661
5662 /**
5663 * Get installed add-ons instances.
5664 *
5665 * @author Vova Feldman (@svovaf)
5666 * @since 1.0.6
5667 *
5668 * @return Freemius[]
5669 */
5670 function get_installed_addons() {
5671 if ( $this->is_addon() ) {
5672 // Add-on cannot have add-ons.
5673 return array();
5674 }
5675
5676 $installed_addons = array();
5677
5678 foreach ( self::$_instances as $instance ) {
5679 if ( $instance->is_addon_of( $this->_plugin->id ) ) {
5680 $installed_addons[] = $instance;
5681 }
5682 }
5683
5684 return $installed_addons;
5685 }
5686
5687 /**
5688 * Check if any add-ons of the plugin are installed.
5689 *
5690 * @author Leo Fajardo (@leorw)
5691 * @since 1.1.1
5692 *
5693 * @return bool
5694 */
5695 function has_installed_addons() {
5696 if ( ! $this->has_addons() ) {
5697 return false;
5698 }
5699
5700 foreach ( self::$_instances as $instance ) {
5701 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
5702 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
5703 return true;
5704 }
5705 }
5706 }
5707
5708 return false;
5709 }
5710
5711 /**
5712 * Tell Freemius that the current plugin is an add-on.
5713 *
5714 * @author Vova Feldman (@svovaf)
5715 * @since 1.0.6
5716 *
5717 * @param number $parent_plugin_id The parent plugin ID
5718 */
5719 function init_addon( $parent_plugin_id ) {
5720 $this->_plugin->parent_plugin_id = $parent_plugin_id;
5721 }
5722
5723 /**
5724 * @author Vova Feldman (@svovaf)
5725 * @since 1.0.6
5726 *
5727 * @return bool
5728 */
5729 function is_addon() {
5730 return (
5731 isset( $this->_plugin->parent_plugin_id ) &&
5732 is_numeric( $this->_plugin->parent_plugin_id )
5733 );
5734 }
5735
5736 /**
5737 * @author Vova Feldman (@svovaf)
5738 * @since 2.3.2
5739 *
5740 * @param number $parent_product_id
5741 *
5742 * @return bool
5743 */
5744 function is_addon_of( $parent_product_id ) {
5745 return (
5746 $this->is_addon() &&
5747 $parent_product_id == $this->_plugin->parent_plugin_id
5748 );
5749 }
5750
5751 /**
5752 * Deactivate add-on if it's premium only and the user does't have a valid license.
5753 *
5754 * @param bool $is_after_trial_cancel
5755 *
5756 * @return bool If add-on was deactivated.
5757 */
5758 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
5759 if ( ! $this->has_free_plan() &&
5760 ! $this->has_features_enabled_license() &&
5761 ! $this->_has_premium_license()
5762 ) {
5763 if ( $this->is_registered() ) {
5764 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
5765 // if (empty($this->_storage->activation_timestamp) ||
5766 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
5767 // ) {
5768 /**
5769 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
5770 *
5771 * Retry syncing the user add-on licenses.
5772 */
5773 // Sync licenses.
5774 $this->_sync_licenses();
5775 // }
5776
5777 // Try to activate premium license.
5778 $this->_activate_license( true );
5779 }
5780
5781 if ( ! $this->has_free_plan() &&
5782 ! $this->has_features_enabled_license() &&
5783 ! $this->_has_premium_license()
5784 ) {
5785 // @todo Check if deactivate plugins also call the deactivation hook.
5786
5787 $this->_parent->_admin_notices->add_sticky(
5788 sprintf(
5789 ( $is_after_trial_cancel ?
5790 $this->_parent->get_text_inline(
5791 '%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.',
5792 'addon-trial-cancelled-message'
5793 ) :
5794 $this->_parent->get_text_inline(
5795 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
5796 'addon-no-license-message'
5797 )
5798 ),
5799 '<b>' . $this->_plugin->title . '</b>'
5800 ) . ' ' . sprintf(
5801 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
5802 $this->_parent->addon_url( $this->_slug ),
5803 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
5804 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
5805 ),
5806 'no_addon_license_' . $this->_slug,
5807 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
5808 ( $is_after_trial_cancel ? 'success' : 'error' )
5809 );
5810
5811 deactivate_plugins( array( $this->_plugin_basename ), true );
5812
5813 return true;
5814 }
5815 }
5816
5817 return false;
5818 }
5819
5820 #endregion
5821
5822 #----------------------------------------------------------------------------------
5823 #region Sandbox
5824 #----------------------------------------------------------------------------------
5825
5826 /**
5827 * Set Freemius into sandbox mode for debugging.
5828 *
5829 * @author Vova Feldman (@svovaf)
5830 * @since 1.0.4
5831 *
5832 * @param string $secret_key
5833 */
5834 function init_sandbox( $secret_key ) {
5835 $this->_plugin->secret_key = $secret_key;
5836
5837 // Update plugin details.
5838 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
5839 }
5840
5841 /**
5842 * Check if running payments in sandbox mode.
5843 *
5844 * @author Vova Feldman (@svovaf)
5845 * @since 1.0.4
5846 *
5847 * @return bool
5848 */
5849 function is_payments_sandbox() {
5850 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
5851 }
5852
5853 #endregion
5854
5855 /**
5856 * Check if running test vs. live plugin.
5857 *
5858 * @author Vova Feldman (@svovaf)
5859 * @since 1.0.5
5860 *
5861 * @return bool
5862 */
5863 function is_live() {
5864 return $this->_plugin->is_live;
5865 }
5866
5867 /**
5868 * Check if super-admin skipped connection for all sites in the network.
5869 *
5870 * @author Vova Feldman (@svovaf)
5871 * @since 2.0.0
5872 */
5873 function is_network_anonymous() {
5874 if ( ! $this->_is_network_active ) {
5875 return false;
5876 }
5877
5878 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
5879
5880 if ( empty( $is_anonymous_ms ) ) {
5881 return false;
5882 }
5883
5884 return $is_anonymous_ms['is'];
5885 }
5886
5887 /**
5888 * Check if super-admin opted-in for all sites in the network.
5889 *
5890 * @author Vova Feldman (@svovaf)
5891 * @since 2.0.0
5892 */
5893 function is_network_connected() {
5894 if ( ! $this->_is_network_active ) {
5895 return false;
5896 }
5897
5898 return $this->_storage->get( 'is_network_connected' );
5899 }
5900
5901 /**
5902 * Check if the user skipped connecting the account with Freemius.
5903 *
5904 * @author Vova Feldman (@svovaf)
5905 * @since 1.0.7
5906 *
5907 * @return bool
5908 */
5909 function is_anonymous() {
5910 if ( ! isset( $this->_is_anonymous ) ) {
5911 if ( $this->is_network_anonymous() ) {
5912 $this->_is_anonymous = true;
5913 } else if ( fs_is_network_admin() ) {
5914 /**
5915 * When not-network-anonymous, yet, running in the network admin, consider as anonymous only when ALL non-delegated sites are set to anonymous.
5916 */
5917 $non_delegated_sites = $this->get_non_delegated_blog_ids();
5918
5919 foreach ( $non_delegated_sites as $blog_id ) {
5920 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
5921
5922 if ( empty( $is_anonymous ) || false === $is_anonymous[ 'is' ] ) {
5923 $this->_is_anonymous = false;
5924 break;
5925 }
5926 }
5927
5928 if ( false !== $this->_is_anonymous ) {
5929 $this->_is_anonymous = true;
5930 }
5931 } else {
5932 if ( ! isset( $this->_storage->is_anonymous ) ) {
5933 // Not skipped.
5934 $this->_is_anonymous = false;
5935 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
5936 // For back compatibility, since the variable was boolean before.
5937 $this->_is_anonymous = $this->_storage->is_anonymous;
5938
5939 // Upgrade stored data format to 1.1.3 format.
5940 $this->set_anonymous_mode( $this->_storage->is_anonymous );
5941 } else {
5942 // Version 1.1.3 and later.
5943 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
5944 }
5945 }
5946 }
5947
5948 return $this->_is_anonymous;
5949 }
5950
5951 /**
5952 * Check if the user skipped the connection of a specified site.
5953 *
5954 * @author Vova Feldman (@svovaf)
5955 * @since 2.0.0
5956 *
5957 * @param int $blog_id
5958 *
5959 * @return bool
5960 */
5961 function is_anonymous_site( $blog_id = 0 ) {
5962 if ( $this->is_network_anonymous() ) {
5963 return true;
5964 }
5965
5966 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
5967
5968 if ( empty( $is_anonymous ) ) {
5969 return false;
5970 }
5971
5972 return $is_anonymous['is'];
5973 }
5974
5975 /**
5976 * Check if user connected his account and install pending email activation.
5977 *
5978 * @author Vova Feldman (@svovaf)
5979 * @since 1.0.7
5980 *
5981 * @return bool
5982 */
5983 function is_pending_activation() {
5984 return $this->_storage->get( 'is_pending_activation', false );
5985 }
5986
5987 /**
5988 * @author Leo Fajardo (@leorw)
5989 * @since 2.5.0
5990 */
5991 private function clear_pending_activation_mode() {
5992 // Remove the pending activation sticky notice (if it still exists).
5993 $this->_admin_notices->remove_sticky( 'activation_pending' );
5994
5995 // Clear the plugin's pending activation mode.
5996 unset( $this->_storage->is_pending_activation );
5997 }
5998
5999 /**
6000 * Check if plugin must be WordPress.org compliant.
6001 *
6002 * @since 1.0.7
6003 *
6004 * @return bool
6005 */
6006 function is_org_repo_compliant() {
6007 return $this->_is_org_compliant;
6008 }
6009
6010 #--------------------------------------------------------------------------------
6011 #region WP Cron Common
6012 #--------------------------------------------------------------------------------
6013
6014 /**
6015 * @author Vova Feldman (@svovaf)
6016 * @since 2.0.0
6017 *
6018 * @param string $name Cron name.
6019 *
6020 * @return object
6021 */
6022 private function get_cron_data( $name ) {
6023 $this->_logger->entrance( $name );
6024
6025 /**
6026 * @var object $cron_data
6027 */
6028 return $this->_storage->get( "{$name}_cron", null );
6029 }
6030
6031 /**
6032 * @author Vova Feldman (@svovaf)
6033 * @since 2.0.0
6034 *
6035 * @param string $name Cron name.
6036 */
6037 private function clear_cron_data( $name ) {
6038 $this->_logger->entrance( $name );
6039
6040 $this->_storage->remove( "{$name}_cron" );
6041 }
6042
6043 /**
6044 * @author Vova Feldman (@svovaf)
6045 * @since 2.0.0
6046 *
6047 * @param string $name Cron name.
6048 * @param int $cron_blog_id The cron executing blog ID.
6049 */
6050 private function set_cron_data( $name, $cron_blog_id = 0 ) {
6051 $this->_logger->entrance( $name );
6052
6053 $this->_storage->store( "{$name}_cron", (object) array(
6054 'version' => $this->get_plugin_version(),
6055 'blog_id' => $cron_blog_id,
6056 'sdk_version' => $this->version,
6057 'timestamp' => WP_FS__SCRIPT_START_TIME,
6058 'on' => true,
6059 ) );
6060 }
6061
6062 /**
6063 * Get the cron's executing blog ID.
6064 *
6065 * @author Vova Feldman (@svovaf)
6066 * @since 2.0.0
6067 *
6068 * @param string $name Cron name.
6069 *
6070 * @return int
6071 */
6072 private function get_cron_blog_id( $name ) {
6073 $this->_logger->entrance( $name );
6074
6075 if ( ! is_multisite() ) {
6076 // Not a multisite.
6077 return 0;
6078 }
6079
6080 $cron_data = $this->get_cron_data( $name );
6081
6082 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6083 $cron_data->blog_id :
6084 0;
6085 }
6086
6087 /**
6088 * @author Vova Feldman (@svovaf)
6089 * @since 2.0.0
6090 *
6091 * @param string $name Cron name.
6092 *
6093 * @return bool
6094 */
6095 private function is_cron_on( $name ) {
6096 $this->_logger->entrance( $name );
6097
6098 /**
6099 * @var object $cron_data
6100 */
6101 $cron_data = $this->get_cron_data( $name );
6102
6103 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6104 }
6105
6106 /**
6107 * Unix timestamp for previous cron execution or false if never executed.
6108 *
6109 * @author Vova Feldman (@svovaf)
6110 * @since 2.0.0
6111 *
6112 * @param string $name Cron name.
6113 *
6114 * @return int|false
6115 */
6116 private function cron_last_execution( $name ) {
6117 $this->_logger->entrance( $name );
6118
6119 return $this->_storage->get( "{$name}_timestamp" );
6120 }
6121
6122 /**
6123 * Set cron execution time to now.
6124 *
6125 * @author Vova Feldman (@svovaf)
6126 * @since 2.0.0
6127 *
6128 * @param string $name Cron name.
6129 */
6130 private function set_cron_execution_timestamp( $name ) {
6131 $this->_logger->entrance( $name );
6132
6133 $this->_storage->store( "{$name}_timestamp", time() );
6134 }
6135
6136 /**
6137 * Sets the keepalive time to now.
6138 *
6139 * @author Leo Fajardo (@leorw)
6140 * @since 2.2.3
6141 *
6142 * @param bool|null $use_network_level_storage
6143 */
6144 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6145 $this->_logger->entrance();
6146
6147 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6148 }
6149
6150 /**
6151 * Check if cron was executed in the last $period of seconds.
6152 *
6153 * @author Vova Feldman (@svovaf)
6154 * @since 2.0.0
6155 *
6156 * @param string $name Cron name.
6157 * @param int $period In seconds
6158 *
6159 * @return bool
6160 */
6161 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6162 $this->_logger->entrance( $name );
6163
6164 $last_execution = $this->cron_last_execution( $name );
6165
6166 if ( ! is_numeric( $last_execution ) ) {
6167 return false;
6168 }
6169
6170 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6171 }
6172
6173 /**
6174 * WP Cron is executed on a site level. When running in a multisite network environment
6175 * with the network integration activated, for optimization reasons, we are consolidating
6176 * the installs data sync cron to be executed only from a single site.
6177 *
6178 * @author Vova Feldman (@svovaf)
6179 * @since 2.0.0
6180 *
6181 * @param int $except_blog_id Target any except the excluded blog ID.
6182 *
6183 * @return int
6184 */
6185 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6186 if ( ! is_multisite() ) {
6187 return 0;
6188 }
6189
6190 if ( $this->_is_network_active ) {
6191 $network_install_blog_id = $this->_storage->network_install_blog_id;
6192
6193 if (
6194 is_numeric( $network_install_blog_id ) &&
6195 $except_blog_id != $network_install_blog_id &&
6196 self::is_site_active( $network_install_blog_id )
6197 ) {
6198 // Try to run cron from the main network blog.
6199 $install = $this->get_install_by_blog_id( $network_install_blog_id );
6200
6201 if (
6202 is_object( $install ) &&
6203 $this->is_tracking_allowed( $network_install_blog_id, $install )
6204 ) {
6205 return $network_install_blog_id;
6206 }
6207 }
6208 }
6209
6210 // Get first opted-in blog ID with active tracking.
6211 $installs = $this->get_blog_install_map();
6212 foreach ( $installs as $blog_id => $install ) {
6213 if ( $except_blog_id != $blog_id &&
6214 self::is_site_active( $blog_id ) &&
6215 $this->is_tracking_allowed( $blog_id, $install )
6216 ) {
6217 return $blog_id;
6218 }
6219 }
6220
6221 return 0;
6222 }
6223
6224 /**
6225 * @author Vova Feldman (@svovaf)
6226 * @since 2.0.0
6227 *
6228 * @param string $name Cron name.
6229 * @param string $action_tag Callback action tag.
6230 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6231 */
6232 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6233 $this->_logger->entrance( $name );
6234
6235 if ( ! $this->is_cron_on( $name ) ) {
6236 return;
6237 }
6238
6239 $clear_cron = true;
6240 if ( ! $is_network_clear && $this->_is_network_active ) {
6241 $installs = $this->get_blog_install_map();
6242
6243 foreach ( $installs as $blog_id => $install ) {
6244 /**
6245 * @var FS_Site $install
6246 */
6247 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6248 $clear_cron = false;
6249 break;
6250 }
6251 }
6252 }
6253
6254 if ( ! $clear_cron ) {
6255 return;
6256 }
6257
6258 $cron_blog_id = $this->get_cron_blog_id( $name );
6259
6260 $this->clear_cron_data( $name );
6261
6262 if ( 0 < $cron_blog_id ) {
6263 switch_to_blog( $cron_blog_id );
6264 }
6265
6266 if ( empty( $action_tag ) ) {
6267 $action_tag = $name;
6268 }
6269
6270 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6271
6272 if ( 0 < $cron_blog_id ) {
6273 restore_current_blog();
6274 }
6275 }
6276
6277 /**
6278 * Unix timestamp for next cron execution or false if not scheduled.
6279 *
6280 * @author Vova Feldman (@svovaf)
6281 * @since 2.0.0
6282 *
6283 * @param string $name Cron name.
6284 * @param string $action_tag Callback action tag.
6285 *
6286 * @return int|false
6287 */
6288 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6289 $this->_logger->entrance( $name );
6290
6291 if ( ! $this->is_cron_on( $name ) ) {
6292 return false;
6293 }
6294
6295 $cron_blog_id = $this->get_cron_blog_id( $name );
6296
6297 if ( 0 < $cron_blog_id ) {
6298 switch_to_blog( $cron_blog_id );
6299 }
6300
6301 if ( empty( $action_tag ) ) {
6302 $action_tag = $name;
6303 }
6304
6305 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6306
6307 if ( 0 < $cron_blog_id ) {
6308 restore_current_blog();
6309 }
6310
6311 return $next_scheduled;
6312 }
6313
6314 /**
6315 * @author Vova Feldman (@svovaf)
6316 * @since 2.0.0
6317 *
6318 * @param string $name Cron name.
6319 * @param string $action_tag Callback action tag.
6320 * @param string $recurrence 'single' or 'daily'.
6321 * @param int $start_at Defaults to now.
6322 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6323 * @param int $except_blog_id Target any except the excluded blog ID.
6324 */
6325 private function schedule_cron(
6326 $name,
6327 $action_tag = '',
6328 $recurrence = 'single',
6329 $start_at = WP_FS__SCRIPT_START_TIME,
6330 $randomize_start = true,
6331 $except_blog_id = 0
6332 ) {
6333 $this->_logger->entrance( $name );
6334
6335 $this->clear_cron( $name, $action_tag, true );
6336
6337 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6338
6339 if ( is_multisite() && 0 == $cron_blog_id ) {
6340 // Don't schedule cron since couldn't find a target blog.
6341 return;
6342 }
6343
6344 if ( 0 < $cron_blog_id ) {
6345 switch_to_blog( $cron_blog_id );
6346 }
6347
6348 if ( 'daily' === $recurrence ) {
6349 if ( $randomize_start ) {
6350 // Schedule first sync with a random 12 hour time range from now.
6351 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6352 }
6353
6354 // Schedule daily WP cron.
6355 wp_schedule_event(
6356 $start_at,
6357 'daily',
6358 $this->get_action_tag( $action_tag )
6359 );
6360 } else if ( 'single' === $recurrence ) {
6361 // Schedule single cron.
6362 wp_schedule_single_event(
6363 $start_at,
6364 $this->get_action_tag( $action_tag )
6365 );
6366 }
6367
6368 $this->set_cron_data( $name, $cron_blog_id );
6369
6370 if ( 0 < $cron_blog_id ) {
6371 restore_current_blog();
6372 }
6373 }
6374
6375 /**
6376 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6377 * that doesn't halt page loading.
6378 *
6379 * @author Vova Feldman (@svovaf)
6380 * @since 2.0.0
6381 *
6382 * @param string $name Cron name.
6383 * @param callable $callable The function that should be executed.
6384 */
6385 private function execute_cron( $name, $callable ) {
6386 $this->_logger->entrance( $name );
6387
6388 // Store the last time data sync was executed.
6389 $this->set_cron_execution_timestamp( $name );
6390
6391 // Check if API is temporary down.
6392 if ( FS_Api::is_temporary_down() ) {
6393 return;
6394 }
6395
6396 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6397
6398 $users_2_blog_ids = array();
6399
6400 if ( ! is_multisite() ) {
6401 // Add dummy blog.
6402 $users_2_blog_ids[0] = array( 0 );
6403 } else {
6404 $installs = $this->get_blog_install_map();
6405 foreach ( $installs as $blog_id => $install ) {
6406 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6407 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6408 $users_2_blog_ids[ $install->user_id ] = array();
6409 }
6410
6411 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6412 }
6413 }
6414 }
6415
6416 $current_blog_id = get_current_blog_id();
6417
6418 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6419 if ( 0 < $blog_ids[0] ) {
6420 $this->switch_to_blog( $blog_ids[0] );
6421 }
6422
6423 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6424
6425 foreach ( $blog_ids as $blog_id ) {
6426 $this->do_action( "after_{$name}_cron", $blog_id );
6427 }
6428 }
6429
6430 if ( is_multisite() ) {
6431 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6432
6433 $this->do_action( "after_{$name}_cron_multisite" );
6434 }
6435 }
6436
6437 #endregion
6438
6439 #----------------------------------------------------------------------------------
6440 #region Daily Sync Cron
6441 #----------------------------------------------------------------------------------
6442
6443
6444 /**
6445 * @author Vova Feldman (@svovaf)
6446 * @since 2.0.0
6447 *
6448 * @return bool
6449 */
6450 private function is_sync_cron_scheduled() {
6451 return $this->is_cron_on( 'sync' );
6452 }
6453
6454 /**
6455 * Get the sync cron's executing blog ID.
6456 *
6457 * @author Vova Feldman (@svovaf)
6458 * @since 2.0.0
6459 *
6460 * @return int
6461 */
6462 private function get_sync_cron_blog_id() {
6463 return $this->get_cron_blog_id( 'sync' );
6464 }
6465
6466 /**
6467 * @author Vova Feldman (@svovaf)
6468 * @since 1.1.7.3
6469 */
6470 private function run_manual_sync() {
6471 if ( ! $this->is_user_admin() ) {
6472 return;
6473 }
6474
6475 // Run manual sync.
6476 $this->_sync_cron();
6477
6478 // Reschedule next cron to run 24 hours from now (performance optimization).
6479 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6480 }
6481
6482 /**
6483 * Data sync cron job. Replaces the background sync non blocking HTTP request
6484 * that doesn't halt page loading.
6485 *
6486 * @author Vova Feldman (@svovaf)
6487 * @since 1.1.7.3
6488 * @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.
6489 */
6490 function _sync_cron() {
6491 $this->_logger->entrance();
6492
6493 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6494 }
6495
6496 /**
6497 * The actual data sync cron logic.
6498 *
6499 * @author Vova Feldman (@svovaf)
6500 * @since 2.0.0
6501 *
6502 * @param int[] $blog_ids
6503 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6504 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6505 * updates for a single site in case `execute_cron` has switched to a different blog.
6506 */
6507 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6508 if ( $this->is_registered() ) {
6509 if ( $this->has_paid_plan() ) {
6510 // Initiate background plan sync.
6511 $this->_sync_license( true, false, $current_blog_id );
6512
6513 if ( $this->is_paying() ) {
6514 // Check for premium plugin updates.
6515 $this->check_updates( true );
6516 }
6517 } else {
6518 // Sync install(s) (only if something changed locally).
6519 if ( 1 < count( $blog_ids ) ) {
6520 $this->sync_installs();
6521 } else {
6522 $this->sync_install();
6523 }
6524
6525 $this->maybe_sync_install_user();
6526 }
6527 }
6528 }
6529
6530 /**
6531 * Check if sync was executed in the last $period of seconds.
6532 *
6533 * @author Vova Feldman (@svovaf)
6534 * @since 1.1.7.3
6535 *
6536 * @param int $period In seconds
6537 *
6538 * @return bool
6539 */
6540 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6541 return $this->is_cron_executed( 'sync', $period );
6542 }
6543
6544 /**
6545 * @author Vova Feldman (@svovaf)
6546 * @since 1.1.7.3
6547 *
6548 * @return bool
6549 */
6550 private function is_sync_cron_on() {
6551 return $this->is_cron_on( 'sync' );
6552 }
6553
6554 /**
6555 * @author Leo Fajardo (@leorw)
6556 * @since 2.5.0
6557 */
6558 private function maybe_schedule_sync_cron() {
6559 $next_schedule = $this->next_sync_cron();
6560
6561 // The event is properly scheduled, so no need to reschedule it.
6562 if (
6563 is_numeric( $next_schedule ) &&
6564 $next_schedule > time()
6565 ) {
6566 return;
6567 }
6568
6569 $this->schedule_sync_cron();
6570 }
6571
6572 /**
6573 * Instead of running blocking install sync event, execute non blocking scheduled cron job.
6574 *
6575 * @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.
6576 *
6577 * @author Leo Fajardo (@leorw)
6578 * @since 2.9.1
6579 */
6580 private function maybe_schedule_install_sync_cron( $except_blog_id = 0 ) {
6581 if ( ! $this->is_user_in_admin() ) {
6582 return;
6583 }
6584
6585 if ( $this->is_clone() ) {
6586 return;
6587 }
6588
6589 if (
6590 // The event has been properly scheduled, so no need to reschedule it.
6591 is_numeric( $this->next_install_sync() )
6592 ) {
6593 return;
6594 }
6595
6596 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
6597 }
6598
6599 /**
6600 * @author Vova Feldman (@svovaf)
6601 * @since 1.1.7.3
6602 *
6603 * @param int $start_at Defaults to now.
6604 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6605 * @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.
6606 */
6607 private function schedule_sync_cron(
6608 $start_at = WP_FS__SCRIPT_START_TIME,
6609 $randomize_start = true,
6610 $except_blog_id = 0
6611 ) {
6612 $this->schedule_cron(
6613 'sync',
6614 'data_sync',
6615 'daily',
6616 $start_at,
6617 $randomize_start,
6618 $except_blog_id
6619 );
6620 }
6621
6622 /**
6623 * Add the actual sync function to the cron job hook.
6624 *
6625 * @author Vova Feldman (@svovaf)
6626 * @since 1.1.7.3
6627 */
6628 private function hook_callback_to_sync_cron() {
6629 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6630 }
6631
6632 /**
6633 * @author Vova Feldman (@svovaf)
6634 * @since 1.1.7.3
6635 *
6636 * @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.
6637 */
6638 private function clear_sync_cron( $is_network_clear = false ) {
6639 $this->_logger->entrance();
6640
6641 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6642 }
6643
6644 /**
6645 * Unix timestamp for next sync cron execution or false if not scheduled.
6646 *
6647 * @author Vova Feldman (@svovaf)
6648 * @since 1.1.7.3
6649 *
6650 * @return int|false
6651 */
6652 function next_sync_cron() {
6653 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6654 }
6655
6656 /**
6657 * Unix timestamp for previous sync cron execution or false if never executed.
6658 *
6659 * @author Vova Feldman (@svovaf)
6660 * @since 1.1.7.3
6661 *
6662 * @return int|false
6663 */
6664 function last_sync_cron() {
6665 return $this->cron_last_execution( 'sync' );
6666 }
6667
6668 #endregion Daily Sync Cron ------------------------------------------------------------------
6669
6670 #----------------------------------------------------------------------------------
6671 #region Async Install Sync
6672 #----------------------------------------------------------------------------------
6673
6674 /**
6675 * @author Vova Feldman (@svovaf)
6676 * @since 1.1.7.3
6677 *
6678 * @return bool
6679 */
6680 private function is_install_sync_scheduled() {
6681 return $this->is_cron_on( 'install_sync' );
6682 }
6683
6684 /**
6685 * Get the sync cron's executing blog ID.
6686 *
6687 * @author Vova Feldman (@svovaf)
6688 * @since 2.0.0
6689 *
6690 * @return int
6691 */
6692 private function get_install_sync_cron_blog_id() {
6693 return $this->get_cron_blog_id( 'install_sync' );
6694 }
6695
6696 /**
6697 * Unix timestamp for previous install sync cron execution or false if never executed.
6698 *
6699 * @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.
6700 *
6701 * @author Vova Feldman (@svovaf)
6702 * @since 1.1.7.3
6703 *
6704 * @return int|false
6705 */
6706 function last_install_sync() {
6707 return $this->cron_last_execution( 'install_sync' );
6708 }
6709
6710 /**
6711 * Unix timestamp for next install sync cron execution or false if not scheduled.
6712 *
6713 * @author Vova Feldman (@svovaf)
6714 * @since 1.1.7.3
6715 *
6716 * @return int|false
6717 */
6718 function next_install_sync() {
6719 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
6720 }
6721
6722 /**
6723 * Add the actual install sync function to the cron job hook.
6724 *
6725 * @author Vova Feldman (@svovaf)
6726 * @since 1.1.7.3
6727 */
6728 private function hook_callback_to_install_sync() {
6729 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
6730 }
6731
6732 /**
6733 * @author Vova Feldman (@svovaf)
6734 * @since 1.1.7.3
6735 *
6736 * @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.
6737 */
6738 private function clear_install_sync_cron( $is_network_clear = false ) {
6739 $this->_logger->entrance();
6740
6741 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
6742 }
6743
6744 /**
6745 * @author Vova Feldman (@svovaf)
6746 * @since 1.1.7.3
6747 * @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.
6748 */
6749 public function _run_sync_install() {
6750 $this->_logger->entrance();
6751
6752 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
6753 }
6754
6755 /**
6756 * The actual install(s) sync cron logic.
6757 *
6758 * @author Vova Feldman (@svovaf)
6759 * @since 2.0.0
6760 *
6761 * @param int[] $blog_ids
6762 * @param int|null $current_blog_id
6763 */
6764 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
6765 if ( $this->is_registered() ) {
6766 if ( 1 < count( $blog_ids ) ) {
6767 $this->sync_installs( array(), true );
6768 } else {
6769 $this->sync_install( array(), true );
6770 }
6771
6772 $this->maybe_sync_install_user();
6773 }
6774 }
6775
6776 #endregion Async Install Sync ------------------------------------------------------------------
6777
6778 /**
6779 * Show a notice that activation is currently pending.
6780 *
6781 * @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.
6782 *
6783 * @author Vova Feldman (@svovaf)
6784 * @since 1.0.7
6785 *
6786 * @param bool|string $email_address
6787 * @param bool $is_pending_trial Since 1.2.1.5
6788 * @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.
6789 * @param bool $has_upgrade_context Since 2.5.3
6790 * @param bool $support_email_address Since 2.5.3
6791 */
6792 function _add_pending_activation_notice(
6793 $email_address = false,
6794 $is_pending_trial = false,
6795 $is_suspicious_email = false,
6796 $has_upgrade_context = false,
6797 $support_email_address = false
6798 ) {
6799 if ( ! is_string( $email_address ) ) {
6800 $current_user = self::_get_current_wp_user();
6801 $email_address = $current_user->user_email;
6802 }
6803
6804 $formatted_message_args = array(
6805 "<b>{$this->get_plugin_name()}</b>",
6806 "<b>{$email_address}</b>",
6807 );
6808
6809 if ( ! $has_upgrade_context || ! fs_is_network_admin() ) {
6810 /* translators: %3$s: action (e.g.: "start the trial" or "complete the opt-in") */
6811 $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' );
6812
6813 $formatted_message_args[] = $is_pending_trial ?
6814 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
6815 $this->get_text_inline( 'complete the opt-in', 'complete-the-opt-in' );
6816
6817 $notice_title = $this->get_text_inline( 'Thanks!', 'thanks' );
6818 } else {
6819 /* translators: %3$s: What the user is expected to receive via email (e.g.: "the installation instructions" or "a license key") */
6820 $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.' );
6821
6822 if ( $this->has_release_on_freemius() ) {
6823 $formatted_message_args[] = $this->get_text_x_inline(
6824 'the installation instructions',
6825 'Part of the message telling the user what they should receive via email.',
6826 'the-installation-instructions-phrase'
6827 );
6828 } else {
6829 $formatted_message_args[] = $this->get_text_x_inline(
6830 'a license key',
6831 'Part of the message telling the user what they should receive via email.',
6832 'a-license-key-phrase'
6833 );
6834
6835 $formatted_message .= ( ' ' . sprintf(
6836 /* translators: %s: activation link (e.g.: <a>Click here</a>) */
6837 $this->get_text_inline( '%s to activate the license once you get it.', 'license-activation-link-message' ),
6838 sprintf(
6839 '<b><a href="%s">%s</a></b>',
6840 $this->get_activation_url( array(
6841 'fs_action' => 'reset_pending_activation_mode',
6842 'require_license' => 'true',
6843 'fs_unique_affix' => $this->get_unique_affix(),
6844 ) ),
6845 $this->get_text_x_inline( 'Click here', 'Part of an activation link message.', 'click-here' )
6846 )
6847 ) );
6848 }
6849
6850 $formatted_message_args[] = ( ! empty( $support_email_address ) ) ?
6851 ( "<b>{$support_email_address}</b>" ) :
6852 $this->get_text_x_inline(
6853 "the product's support email address",
6854 'Part of the message that tells the user to check their spam folder for a specific email.',
6855 'product-support-email-address-phrase'
6856 );
6857
6858 $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' ) );
6859
6860 $notice_title = $this->get_text_inline( 'Thanks for upgrading.', 'after-upgrade-thank-you-message' );
6861 }
6862
6863 $this->_admin_notices->add_sticky(
6864 vsprintf( $formatted_message, $formatted_message_args ),
6865 'activation_pending',
6866 $notice_title
6867 );
6868 }
6869
6870 /**
6871 * Check if currently in plugin activation.
6872 *
6873 * @author Vova Feldman (@svovaf)
6874 * @since 1.1.4
6875 *
6876 * @return bool
6877 */
6878 function is_plugin_activation() {
6879 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
6880
6881 return !empty($result);
6882 }
6883
6884 /**
6885 *
6886 * NOTE: admin_menu action executed before admin_init.
6887 *
6888 * @author Vova Feldman (@svovaf)
6889 * @since 1.0.7
6890 */
6891 function _admin_init_action() {
6892 $is_migration = $this->is_migration();
6893
6894 /**
6895 * Automatically redirect to connect/activation page after plugin activation.
6896 *
6897 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
6898 */
6899 if ( $this->is_plugin_activation() ) {
6900 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
6901
6902 if ( isset( $_GET['activate-multi'] ) ) {
6903 /**
6904 * Don't redirect if activating multiple plugins at once (bulk activation).
6905 */
6906 } else if (
6907 self::is_deactivation_snoozed() &&
6908 (
6909 // Either running the free code base.
6910 ! $this->is_premium() ||
6911 // Or if has a free version.
6912 ! $this->is_only_premium() ||
6913 // If premium only, don't redirect if license is activated.
6914 ( $this->is_registered() && ! $this->can_use_premium_code() )
6915 )
6916 ) {
6917 /**
6918 * 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.
6919 */
6920 } else if ( ! $is_migration ) {
6921 $this->_redirect_on_activation_hook();
6922 return;
6923 }
6924 }
6925
6926 if ( $is_migration ) {
6927 return;
6928 }
6929
6930 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
6931 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
6932
6933 $this->skip_connection( fs_is_network_admin() );
6934
6935 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
6936 }
6937
6938 if ( $this->is_network_activation_mode() &&
6939 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
6940 ) {
6941 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
6942
6943 $this->delegate_connection();
6944
6945 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
6946 }
6947
6948 $this->_add_upgrade_action_link();
6949
6950 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
6951 (
6952 ( true === $this->_storage->require_license_activation ) ||
6953 // Not registered nor anonymous.
6954 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
6955 // OR, network level and in network upgrade mode.
6956 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
6957 )
6958 ) {
6959 if ( ! $this->is_pending_activation() ) {
6960 if ( ! $this->is_activation_page() ) {
6961 /**
6962 * If a user visits any other admin page before activating the premium-only theme with a valid
6963 * license, reactivate the previous theme.
6964 *
6965 * @author Leo Fajardo (@leorw)
6966 * @since 1.2.2
6967 */
6968 if ( $this->is_theme() &&
6969 ! $this->has_settings_menu() &&
6970 ! isset( $_REQUEST['fs_action'] ) &&
6971 $this->can_activate_previous_theme()
6972 ) {
6973 if ( $this->is_only_premium() ) {
6974 $this->activate_previous_theme();
6975 return;
6976 }
6977
6978 if ( true === $this->_storage->require_license_activation ) {
6979 $this->_storage->require_license_activation = false;
6980 }
6981 }
6982
6983 if ( ! fs_is_network_admin() &&
6984 $this->is_network_activation_mode() &&
6985 ! $this->is_delegated_connection()
6986 ) {
6987 return;
6988 }
6989
6990 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
6991 if ( ! $this->_anonymous_mode &&
6992 ( ! $this->is_addon() || ! $this->_parent->is_anonymous() ) ) {
6993 // Show notice for new plugin installations.
6994 $this->_admin_notices->add(
6995 sprintf(
6996 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
6997 sprintf( '<b><a href="%s">%s</a></b>',
6998 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
6999 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
7000 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
7001 )
7002 ),
7003 '',
7004 'update-nag'
7005 );
7006 }
7007 } else {
7008 if ( $this->should_add_sticky_optin_notice() ) {
7009 $this->add_sticky_optin_admin_notice();
7010 }
7011
7012 if ( $this->has_filter( 'optin_pointer_element' ) ) {
7013 // Don't show admin nag if plugin update.
7014 wp_enqueue_script( 'wp-pointer' );
7015 wp_enqueue_style( 'wp-pointer' );
7016
7017 $this->_enqueue_connect_essentials();
7018
7019 add_action( 'admin_print_footer_scripts', array(
7020 $this,
7021 '_add_connect_pointer_script'
7022 ) );
7023 }
7024 }
7025 }
7026 }
7027
7028 if ( $this->show_opt_in_on_themes_page() &&
7029 $this->is_activation_page()
7030 ) {
7031 $this->_show_theme_activation_optin_dialog();
7032 }
7033 }
7034 }
7035
7036 /**
7037 * @author Vova Feldman (@svovaf)
7038 * @since 2.0.0
7039 *
7040 * @return bool
7041 */
7042 private function should_add_sticky_optin_notice() {
7043 if ( $this->is_addon() && $this->_parent->is_anonymous() ) {
7044 return false;
7045 }
7046
7047 if ( fs_is_network_admin() ) {
7048 if ( ! $this->_is_network_active ) {
7049 return false;
7050 }
7051
7052 if ( ! $this->is_network_activation_mode() ) {
7053 return false;
7054 }
7055
7056 return ! isset( $this->_storage->sticky_optin_added_ms );
7057 }
7058
7059 if ( ! $this->is_activation_mode() ) {
7060 return false;
7061 }
7062
7063 // If running from a blog admin and delegated the connection.
7064 return ! isset( $this->_storage->sticky_optin_added );
7065 }
7066
7067 /**
7068 * @author Leo Fajardo (@leorw)
7069 * @since 2.0.0
7070 */
7071 private function add_sticky_optin_admin_notice() {
7072 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7073 $this->_storage->sticky_optin_added = true;
7074 } else {
7075 $this->_storage->sticky_optin_added_ms = true;
7076 }
7077
7078 // Show notice for new plugin installations.
7079 $this->_admin_notices->add_sticky(
7080 sprintf(
7081 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7082 $this->_module_type,
7083 sprintf( '<b><a href="%s">%s</a></b>',
7084 $this->get_activation_url(),
7085 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7086 )
7087 ),
7088 'connect_account',
7089 '',
7090 'update-nag'
7091 );
7092 }
7093
7094 /**
7095 * Enqueue connect requires scripts and styles.
7096 *
7097 * @author Vova Feldman (@svovaf)
7098 * @since 1.1.4
7099 */
7100 function _enqueue_connect_essentials() {
7101 wp_enqueue_script( 'jquery' );
7102 wp_enqueue_script( 'json2' );
7103
7104 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.js' );
7105 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7106 }
7107
7108 /**
7109 * Add connect / opt-in pointer.
7110 *
7111 * @author Vova Feldman (@svovaf)
7112 * @since 1.1.4
7113 */
7114 function _add_connect_pointer_script() {
7115 $vars = array( 'id' => $this->_module_id );
7116 $pointer_content = fs_get_template( 'connect.php', $vars );
7117 ?>
7118 <script type="text/javascript">// <![CDATA[
7119 jQuery(document).ready(function ($) {
7120 if ('undefined' !== typeof(jQuery().pointer)) {
7121
7122 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7123
7124 if (element.length > 0) {
7125 var optin = $(element).pointer($.extend(true, {}, {
7126 content : <?php echo json_encode( $pointer_content ) ?>,
7127 position : {
7128 edge : 'left',
7129 align: 'center'
7130 },
7131 buttons : function () {
7132 // Don't show pointer buttons.
7133 return '';
7134 },
7135 pointerWidth: 482
7136 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7137
7138 <?php
7139 echo $this->apply_filters( 'optin_pointer_execute', "
7140
7141 optin.pointer('open');
7142
7143 // Tag the opt-in pointer with custom class.
7144 $('.wp-pointer #fs_connect')
7145 .parents('.wp-pointer.wp-pointer-top')
7146 .addClass('fs-opt-in-pointer');
7147
7148 ", 'element', 'optin' ) ?>
7149 }
7150 }
7151 });
7152 // ]]></script>
7153 <?php
7154 }
7155
7156 /**
7157 * Return current page's URL.
7158 *
7159 * @author Vova Feldman (@svovaf)
7160 * @since 1.0.7
7161 *
7162 * @return string
7163 */
7164 static function current_page_url() {
7165 $url = 'http';
7166
7167 if ( isset( $_SERVER["HTTPS"] ) ) {
7168 if ( $_SERVER["HTTPS"] == "on" ) {
7169 $url .= "s";
7170 }
7171 }
7172 $url .= "://";
7173 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7174 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7175 } else {
7176 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7177 }
7178
7179 return esc_url( $url );
7180 }
7181
7182 /**
7183 * Check if the current page is the plugin's main admin settings page.
7184 *
7185 * @author Vova Feldman (@svovaf)
7186 * @since 1.0.7
7187 *
7188 * @return bool
7189 */
7190 function _is_plugin_page() {
7191 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7192 fs_is_plugin_page( $this->_slug );
7193 }
7194
7195 /* Events
7196 ------------------------------------------------------------------------------------------------------------------*/
7197 /**
7198 * Delete site install from Database.
7199 *
7200 * @author Vova Feldman (@svovaf)
7201 * @since 1.0.1
7202 *
7203 * @param bool $store
7204 * @param int|null $blog_id Since 2.0.0
7205 *
7206 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7207 */
7208 function _delete_site( $store = true, $blog_id = null ) {
7209 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7210 }
7211
7212 /**
7213 * Delete site install from Database.
7214 *
7215 * @author Vova Feldman (@svovaf)
7216 * @since 1.2.2.7
7217 *
7218 * @param string $slug
7219 * @param string $module_type
7220 * @param bool $store
7221 * @param int|null $blog_id Since 2.0.0
7222 *
7223 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7224 */
7225 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7226 $sites = self::get_all_sites( $module_type, $blog_id );
7227
7228 $install_id = false;
7229
7230 if ( isset( $sites[ $slug ] ) ) {
7231 if ( is_object( $sites[ $slug ] ) ) {
7232 $install_id = $sites[ $slug ]->id;
7233 }
7234
7235 unset( $sites[ $slug ] );
7236
7237 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7238 }
7239
7240 return $install_id;
7241 }
7242
7243 /**
7244 * Delete plugin's plans information.
7245 *
7246 * @param bool $store Flush to Database if true.
7247 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7248 *
7249 * @author Vova Feldman (@svovaf)
7250 * @since 1.0.9
7251 */
7252 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7253 $this->_logger->entrance();
7254
7255 $plans = self::get_all_plans( $this->_module_type );
7256
7257 $plans_to_keep = array();
7258
7259 if ( $keep_associated_plans ) {
7260 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7261 foreach ( $plans_ids_to_keep as $plan_id ) {
7262 $plan = self::_get_plan_by_id( $plan_id );
7263 if ( is_object( $plan ) ) {
7264 $plans_to_keep[] = self::_encrypt_entity( $plan );
7265 }
7266 }
7267 }
7268
7269 if ( ! empty( $plans_to_keep ) ) {
7270 $plans[ $this->_slug ] = $plans_to_keep;
7271 } else {
7272 unset( $plans[ $this->_slug ] );
7273 }
7274
7275 $this->set_account_option( 'plans', $plans, $store );
7276 }
7277
7278 /**
7279 * Delete all plugin licenses.
7280 *
7281 * @author Vova Feldman (@svovaf)
7282 * @since 1.0.9
7283 *
7284 * @param bool $store
7285 */
7286 private function _delete_licenses( $store = true ) {
7287 $this->_logger->entrance();
7288
7289 $all_licenses = self::get_all_licenses();
7290
7291 unset( $all_licenses[ $this->_module_id ] );
7292
7293 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7294 }
7295
7296 /**
7297 * Check if Freemius was added on new plugin installation.
7298 *
7299 * @author Vova Feldman (@svovaf)
7300 * @since 1.1.5
7301 *
7302 * @return bool
7303 */
7304 function is_plugin_new_install() {
7305 return isset( $this->_storage->is_plugin_new_install ) &&
7306 $this->_storage->is_plugin_new_install;
7307 }
7308
7309 /**
7310 * Check if it's the first plugin release that is running Freemius.
7311 *
7312 * @author Vova Feldman (@svovaf)
7313 * @since 1.2.1.5
7314 *
7315 * @return bool
7316 */
7317 function is_first_freemius_powered_version() {
7318 return empty( $this->_storage->plugin_last_version );
7319 }
7320
7321 /**
7322 * @author Leo Fajardo (@leorw)
7323 * @since 1.2.2
7324 *
7325 * @return bool|string
7326 */
7327 private function get_previous_theme_slug() {
7328 return isset( $this->_storage->previous_theme ) ?
7329 $this->_storage->previous_theme :
7330 false;
7331 }
7332
7333 /**
7334 * @author Leo Fajardo (@leorw)
7335 * @since 1.2.2
7336 *
7337 * @return bool
7338 */
7339 private function can_activate_previous_theme() {
7340 return $this->can_activate_theme( $this->get_previous_theme_slug() );
7341 }
7342
7343 /**
7344 * @author Leo Fajardo (@leorw)
7345 * @since 2.5.0
7346 *
7347 * @return bool
7348 */
7349 private function can_activate_theme( $slug ) {
7350 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7351 $theme_instance = wp_get_theme( $slug );
7352
7353 return $theme_instance->exists();
7354 }
7355
7356 return false;
7357 }
7358
7359 /**
7360 * @author Leo Fajardo (@leorw)
7361 * @since 1.2.2
7362 */
7363 private function activate_previous_theme() {
7364 switch_theme( $this->get_previous_theme_slug() );
7365 unset( $this->_storage->previous_theme );
7366
7367 global $pagenow;
7368 if ( 'themes.php' === $pagenow ) {
7369 /**
7370 * Refresh the active theme information.
7371 *
7372 * @author Leo Fajardo (@leorw)
7373 * @since 1.2.2
7374 */
7375 fs_redirect( $this->admin_url( $pagenow ) );
7376 }
7377 }
7378
7379 /**
7380 * @author Leo Fajardo (@leorw)
7381 * @since 1.2.2
7382 *
7383 * @return string
7384 */
7385 function get_previous_theme_activation_url() {
7386 if ( ! $this->can_activate_previous_theme() ) {
7387 return '';
7388 }
7389
7390 /**
7391 * Activation URL
7392 *
7393 * @author Leo Fajardo (@leorw)
7394 * @since 1.2.2
7395 */
7396 return wp_nonce_url(
7397 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7398 'switch-theme_' . $this->get_previous_theme_slug()
7399 );
7400 }
7401
7402 /**
7403 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7404 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7405 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7406 * theme doesn't exist, then there will be no close button.
7407 *
7408 * @author Leo Fajardo (@leorw)
7409 * @since 1.2.2
7410 *
7411 * @param string $slug_or_name Old theme's slug or name.
7412 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7413 */
7414 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7415 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7416 $old_theme->get_stylesheet() :
7417 $slug_or_name;
7418
7419 $this->_activate_plugin_event_hook();
7420 }
7421
7422 /**
7423 * Plugin activated hook.
7424 *
7425 * @author Vova Feldman (@svovaf)
7426 * @since 1.0.1
7427 *
7428 * @uses FS_Api
7429 */
7430 function _activate_plugin_event_hook() {
7431 $this->_logger->entrance( 'slug = ' . $this->_slug );
7432
7433 if ( ! $this->is_user_admin() ) {
7434 return;
7435 }
7436
7437 $this->unregister_uninstall_hook();
7438
7439 // Clear API cache on activation.
7440 FS_Api::clear_cache();
7441
7442 $is_premium_version_activation = $this->is_plugin() ?
7443 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7444 $this->is_premium();
7445
7446 if ( $is_premium_version_activation && $this->is_pending_activation() ) {
7447 $this->clear_pending_activation_mode();
7448 }
7449
7450 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7451
7452 if ( $this->is_plugin() ) {
7453 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7454 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7455 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7456 $other_version_basename = $is_premium_version_activation ?
7457 $this->_free_plugin_basename :
7458 $this->premium_plugin_basename();
7459
7460 if ( ! $this->_is_network_active ) {
7461 /**
7462 * Themes are always network activated, but the ACTUAL activation is per site.
7463 *
7464 * During the activation, the plugin isn't yet active, therefore,
7465 * _is_network_active will be set to false even if it's a network level
7466 * activation. So we need to fix that by looking at the is_network_admin() value.
7467 *
7468 * @author Vova Feldman
7469 */
7470 $this->_is_network_active = (
7471 $this->_is_multisite_integrated &&
7472 fs_is_network_admin()
7473 );
7474 }
7475
7476 /**
7477 * If the other module version is active, deactivate it.
7478 *
7479 * is_plugin_active() checks if the plugin is active on the site or the network level and
7480 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7481 *
7482 * @author Leo Fajardo (@leorw)
7483 * @since 1.2.2
7484 */
7485 if (
7486 is_plugin_active( $other_version_basename ) &&
7487 $this->apply_filters( 'deactivate_on_activation', ! $this->is_parallel_activation() )
7488 ) {
7489 deactivate_plugins( $other_version_basename );
7490 }
7491 }
7492
7493 if ( $this->is_registered() ) {
7494 if ( $is_premium_version_activation ) {
7495 $this->reconnect_locally();
7496 }
7497
7498
7499 // Schedule re-activation event and sync.
7500 // $this->sync_install( array(), true );
7501 $this->maybe_schedule_install_sync_cron();
7502
7503 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7504 if ( $is_premium_version_activation ) {
7505 $this->_admin_notices->add(
7506 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7507 $this->get_text_x_inline( 'W00t',
7508 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7509 );
7510 }
7511 } else if ( $this->is_anonymous() ) {
7512 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7513 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7514 $network = true;
7515 } else {
7516 $plugin_version = isset( $this->_storage->is_anonymous ) ?
7517 $this->_storage->is_anonymous['version'] :
7518 null;
7519 $network = false;
7520 }
7521
7522 /**
7523 * Reset "skipped" click cache on the following:
7524 * 1. Freemius DEV mode.
7525 * 2. WordPress DEBUG mode.
7526 * 3. If a plugin and the user skipped the exact same version before.
7527 *
7528 * @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).
7529 *
7530 * @todo 4. If explicitly asked to retry after every activation.
7531 */
7532 if ( WP_FS__DEV_MODE ||
7533 (
7534 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7535 $this->get_plugin_version() == $plugin_version
7536 )
7537 ) {
7538 $this->reset_anonymous_mode( $network );
7539 }
7540 }
7541
7542 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7543
7544 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7545 /**
7546 * When activating an add-on, try to also activate a license.
7547 *
7548 * @author Leo Fajardo (@leorw)
7549 * @since 2.3.0
7550 */
7551 if ( ! $this->_is_network_active ) {
7552 $this->maybe_activate_addon_license();
7553 } else {
7554 $this->maybe_network_activate_addon_license();
7555 }
7556
7557 /**
7558 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7559 *
7560 * @author Leo Fajardo (@leorw)
7561 * @since 2.3.0
7562 */
7563 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7564
7565 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7566 $this->_storage->require_license_activation = false;
7567 }
7568 }
7569
7570 if (
7571 $is_premium_version_activation &&
7572 (
7573 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7574 (
7575 $this->is_registered() &&
7576 ! $is_trial_or_has_features_enabled_license
7577 )
7578 )
7579 ) {
7580 $this->_storage->require_license_activation = true;
7581 }
7582
7583 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7584 /**
7585 * If no previous version of plugin's version exist, it means that it's either
7586 * the first time that the plugin installed on the site, or the plugin was installed
7587 * before but didn't have Freemius integrated.
7588 *
7589 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7590 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7591 * only after immediate activation.
7592 *
7593 * @since 1.1.4
7594 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7595 */
7596 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7597 }
7598
7599 /**
7600 * Also flush when activating the premium version so that even if Freemius was off before, the API
7601 * connectivity test can be run again.
7602 *
7603 * @author Leo Fajardo (@leorw)
7604 * @since 2.2.3.1
7605 */
7606 $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation );
7607
7608 if ( ! $this->_anonymous_mode &&
7609 ( false !== $has_api_connectivity ) &&
7610 ! $this->_isAutoInstall
7611 ) {
7612 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7613 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7614 }
7615
7616 /**
7617 * Activation hook is executed after the plugin's main file is loaded, therefore,
7618 * after the plugin was loaded. The logic is located at activate_plugin()
7619 * ./wp-admin/includes/plugin.php.
7620 *
7621 * @author Vova Feldman (@svovaf)
7622 * @since 1.1.9
7623 */
7624 $this->_storage->was_plugin_loaded = true;
7625 }
7626
7627 /**
7628 * @author Leo Fajardo (@leorw)
7629 * @since 2.3.0
7630 */
7631 private function maybe_activate_addon_license() {
7632 $parent_fs = $this->get_parent_instance();
7633
7634 if (
7635 ! is_object( $parent_fs ) ||
7636 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7637 ) {
7638 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7639 return;
7640 }
7641
7642 $license = $this->get_active_parent_license();
7643 if ( ! is_object( $license ) ) {
7644 return;
7645 }
7646
7647 if (
7648 $this->is_bundle_license_auto_activation_enabled() &&
7649 ! empty( $license->products )
7650 ) {
7651 $this->activate_bundle_license( $license );
7652
7653 return;
7654 }
7655
7656 if ( ! $this->is_registered() ) {
7657 // Opt in with a license key.
7658 $this->opt_in(
7659 $parent_fs->get_current_or_network_user()->email,
7660 false,
7661 false,
7662 $license->secret_key
7663 );
7664 } else {
7665 // Activate the license.
7666 $install = $this->api_site_call(
7667 '/',
7668 'put',
7669 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7670 );
7671
7672 if ( ! FS_Api::is_api_error( $install ) ) {
7673 $this->_sync_addon_license( $this->get_id(), true );
7674 }
7675 }
7676 }
7677
7678 /**
7679 * @author Leo Fajardo (@leorw)
7680 * @since 2.3.0
7681 *
7682 * @param FS_Plugin_License $license
7683 */
7684 private function maybe_network_activate_addon_license( $license = null ) {
7685 $parent_fs = $this->get_parent_instance();
7686 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7687 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7688 return;
7689 }
7690
7691 $license = ( ! is_null( $license ) ) ?
7692 $license :
7693 $this->get_active_parent_license();
7694
7695 if ( ! is_object( $license ) ) {
7696 return;
7697 }
7698
7699 if (
7700 $this->is_bundle_license_auto_activation_enabled() &&
7701 ! empty( $license->products )
7702 ) {
7703 $this->activate_bundle_license( $license );
7704
7705 return;
7706 }
7707
7708 if ( ! $this->is_network_registered() ) {
7709 $sites = $this->get_sites_for_network_level_optin();
7710
7711 if ( count( $sites ) > $license->left() ) {
7712 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
7713 return;
7714 }
7715
7716 // Opt in with a license key.
7717 $this->opt_in(
7718 $parent_fs->get_user()->email,
7719 false,
7720 false,
7721 $license->secret_key,
7722 false,
7723 false,
7724 false,
7725 null,
7726 $sites
7727 );
7728 } else {
7729 $blog_2_install_map = array();
7730 $site_ids = array();
7731
7732 $all_sites = Freemius::get_sites();
7733
7734 foreach ( $all_sites as $site ) {
7735 $blog_id = Freemius::get_site_blog_id( $site );
7736 $install = $this->get_install_by_blog_id( $blog_id );
7737
7738 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
7739 // Skip license activation for installs that are already associated with a license.
7740 continue;
7741 }
7742
7743 if ( is_object( $install ) ) {
7744 $blog_2_install_map[ $blog_id ] = $install;
7745 } else {
7746 $site_ids[] = $blog_id;
7747 }
7748 }
7749
7750 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
7751 return;
7752 }
7753
7754 $user = $this->get_current_or_network_user();
7755
7756 if ( ! empty( $blog_2_install_map ) ) {
7757 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
7758
7759 if ( true !== $result ) {
7760 return;
7761 }
7762 }
7763
7764 if ( ! empty( $site_ids ) ) {
7765 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
7766 }
7767 }
7768 }
7769
7770 /**
7771 * 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.
7772 *
7773 * @author Leo Fajardo (@leorw)
7774 * @since 2.4.0
7775 *
7776 * @param FS_Plugin_License $license
7777 * @param array $sites
7778 * @param int $blog_id
7779 */
7780 private function maybe_activate_bundle_license( FS_Plugin_License $license = null, $sites = array(), $blog_id = 0 ) {
7781 if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
7782 $license = $this->_license;
7783 }
7784
7785 if ( ! is_object( $license ) ) {
7786 return;
7787 }
7788
7789 $parent_license = ( ! empty( $license->products ) ) ?
7790 $license :
7791 $this->get_active_parent_license( $license->secret_key );
7792
7793 if ( is_object( $parent_license ) ) {
7794 $this->activate_bundle_license( $parent_license, $sites, $blog_id );
7795 }
7796 }
7797
7798 /**
7799 * Try to activate a bundle license for all the bundle products installed on the site.
7800 * (1) If a child product install already has a license, the bundle license won't be activated.
7801 * (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.
7802 * (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.
7803 *
7804 * @author Leo Fajardo (@leorw)
7805 * @since 2.4.0
7806 *
7807 * @param FS_Plugin_License $license
7808 * @param array $sites
7809 * @param int $current_blog_id
7810 */
7811 private function activate_bundle_license( $license, $sites = array(), $current_blog_id = 0 ) {
7812 $is_network_admin = fs_is_network_admin();
7813
7814 $installs_by_blog_map = array();
7815 $site_info_by_blog_map = array();
7816
7817 /**
7818 * Try to activate the license for all supported products.
7819 *
7820 * @author Leo Fajardo
7821 */
7822 foreach ( $license->products as $product_id ) {
7823 $fs = self::get_instance_by_id( $product_id );
7824
7825 if ( ! is_object( $fs ) ) {
7826 continue;
7827 }
7828
7829 if ( ! $fs->has_paid_plan() ) {
7830 continue;
7831 }
7832
7833 if (
7834 ! $fs->is_addon() &&
7835 ! FS_Plan_Manager::instance()->has_paid_plan( $fs->_plans )
7836 ) {
7837 /**
7838 * The parent product can be free-only but can have its `has_paid_plan` flag set to `true` when
7839 * there is a context bundle.
7840 */
7841 continue;
7842 }
7843
7844 if ( $current_blog_id > 0 ) {
7845 $fs->switch_to_blog( $current_blog_id );
7846 }
7847
7848 if ( $fs->has_active_valid_license() ) {
7849 continue;
7850 }
7851
7852 if ( ! $is_network_admin || $current_blog_id > 0 ) {
7853 if ( $fs->is_network_active() && ! $fs->is_delegated_connection( $current_blog_id ) ) {
7854 // Do not try to activate the license in the site level if the product is network active and the connection was not delegated.
7855 continue;
7856 }
7857 } else {
7858 if ( ! $fs->is_network_active() ) {
7859 // Do not try to activate the license in the network level if the product is not network active.
7860 continue;
7861 }
7862
7863 if ( $fs->is_network_delegated_connection() ) {
7864 // Do not try to activate the license in the network level if the activation has been delegated to site admins.
7865 continue;
7866 }
7867
7868 $has_install_with_license = false;
7869
7870 // 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.
7871 $filtered_sites = array();
7872
7873 if ( empty( $sites ) ) {
7874 $all_sites = self::get_sites();
7875
7876 foreach ( $all_sites as $site ) {
7877 $sites[] = array( 'blog_id' => self::get_site_blog_id( $site ) );
7878 }
7879 } else {
7880 // Populate the map here to avoid calling `$fs->get_site_info( $site );` in the other `for` loop below.
7881 foreach ( $sites as $site ) {
7882 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
7883 continue;
7884 }
7885
7886 $site_info_by_blog_map[ $site['blog_id'] ] = $site;
7887 }
7888 }
7889
7890 foreach ( $sites as $site ) {
7891 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
7892 continue;
7893 }
7894
7895 $blog_id = $site['blog_id'];
7896
7897 if ( ! isset( $installs_by_blog_map[ $blog_id ] ) ) {
7898 $installs_by_blog_map[ $blog_id ] = self::get_all_sites( $fs->get_module_type(), $blog_id );
7899 }
7900
7901 $installs = $installs_by_blog_map[ $blog_id ];
7902 $install = null;
7903
7904 if ( isset( $installs[ $fs->get_slug() ] ) ) {
7905 $install = $installs[ $fs->get_slug() ];
7906
7907 if (
7908 is_object( $install ) &&
7909 (
7910 ! FS_Site::is_valid_id( $install->id ) ||
7911 ! FS_User::is_valid_id( $install->user_id ) ||
7912 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
7913 )
7914 ) {
7915 $install = null;
7916 }
7917 }
7918
7919 if (
7920 is_object( $install ) &&
7921 FS_Plugin_License::is_valid_id( $install->license_id )
7922 ) {
7923 $has_install_with_license = true;
7924 break;
7925 }
7926
7927 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
7928 // Site activation delegated, don't activate bundle license on the site in the network admin.
7929 continue;
7930 }
7931
7932 if ( ! isset( $site_info_by_blog_map[ $blog_id ] ) ) {
7933 $site_info_by_blog_map[ $blog_id ] = $fs->get_site_info( $site );
7934 }
7935
7936 $filtered_sites[] = $site_info_by_blog_map[ $blog_id ];
7937 }
7938
7939 if ( $has_install_with_license || empty( $filtered_sites ) ) {
7940 // 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.
7941 continue;
7942 }
7943
7944 $sites = $filtered_sites;
7945 }
7946
7947 $fs->activate_migrated_license(
7948 $license->secret_key,
7949 null,
7950 null,
7951 $sites,
7952 ( $current_blog_id > 0 ? $current_blog_id : null )
7953 );
7954 }
7955 }
7956
7957 /**
7958 * Returns a parent license that can be activated for the context product.
7959 *
7960 * @author Leo Fajardo (@leorw)
7961 * @since 2.3.0
7962 *
7963 * @param string|null $license_key
7964 * @param bool $flush
7965 *
7966 * @return FS_Plugin_License
7967 */
7968 function get_active_parent_license( $license_key = null, $flush = true ) {
7969 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
7970
7971 $fs = $this;
7972
7973 if ( $this->is_addon() ) {
7974 $parent_instance = $this->get_parent_instance();
7975
7976 if ( is_object( $parent_instance ) && $parent_instance->is_registered() ) {
7977 $fs = $parent_instance;
7978 }
7979 }
7980
7981 $foreign_licenses = $fs->get_foreign_licenses_info(
7982 self::get_all_licenses( $this->get_parent_id() )
7983 );
7984
7985 if ( ! empty ( $foreign_licenses ) ) {
7986 $foreign_licenses = array(
7987 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
7988 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
7989 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
7990 );
7991
7992 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
7993 }
7994
7995 $result = $fs->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, $flush );
7996
7997 if (
7998 ! $this->is_api_result_object( $result, 'licenses' ) ||
7999 ! is_array( $result->licenses ) ||
8000 empty( $result->licenses )
8001 ) {
8002 return null;
8003 }
8004
8005 $parent_license = null;
8006
8007 if ( empty( $license_key ) ) {
8008 $parent_license = $result->licenses[0];
8009 } else {
8010 foreach ( $result->licenses as $license ) {
8011 if ( $license_key === $license->secret_key ) {
8012 $parent_license = $license;
8013 break;
8014 }
8015 }
8016 }
8017
8018 if ( ! is_null( $parent_license ) ) {
8019 $parent_license = new FS_Plugin_License( $parent_license );
8020 }
8021
8022 return $parent_license;
8023 }
8024
8025 /**
8026 * @author Leo Fajardo (@leorw)
8027 * @since 2.3.0
8028 *
8029 * @return array
8030 */
8031 function get_sites_for_network_level_optin() {
8032 $sites = array();
8033 $all_sites = self::get_sites();
8034
8035 foreach ( $all_sites as $site ) {
8036 $blog_id = self::get_site_blog_id( $site );
8037
8038 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
8039 ! $this->is_installed_on_site( $blog_id )
8040 ) {
8041 $sites[] = $this->get_site_info( $site );
8042 }
8043 }
8044
8045 return $sites;
8046 }
8047
8048 /**
8049 * Delete account.
8050 *
8051 * @author Vova Feldman (@svovaf)
8052 * @since 1.0.3
8053 *
8054 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8055 */
8056 function delete_account_event( $check_user = true ) {
8057 $this->_logger->entrance( 'slug = ' . $this->_slug );
8058
8059 if ( $check_user && ! $this->is_user_admin() ) {
8060 return;
8061 }
8062
8063 $this->do_action( 'before_account_delete' );
8064
8065 // Clear all admin notices.
8066 $this->_admin_notices->clear_all_sticky( false );
8067
8068 $this->_delete_site( false );
8069
8070 $delete_network_common_data = true;
8071
8072 if ( $this->_is_network_active ) {
8073 $installs = $this->get_blog_install_map();
8074
8075 // Don't delete common network data unless no other installs left.
8076 $delete_network_common_data = empty( $installs );
8077 }
8078
8079 if ( $delete_network_common_data ) {
8080 $this->_delete_plans( false );
8081
8082 $this->_delete_licenses( false );
8083
8084 // Delete add-ons related to plugin's account.
8085 $this->_delete_account_addons( false );
8086 }
8087
8088 // @todo Delete plans and licenses of add-ons.
8089
8090 self::$_accounts->store();
8091
8092 /**
8093 * IMPORTANT:
8094 * Clear crons must be executed before clearing all storage.
8095 * Otherwise, the cron will not be cleared.
8096 */
8097 if ( $delete_network_common_data ) {
8098 $this->clear_sync_cron();
8099 }
8100
8101 $this->clear_install_sync_cron();
8102
8103 // Clear all storage data.
8104 $this->_storage->clear_all( true, array(
8105 'is_delegated_connection',
8106 'connectivity_test',
8107 'is_on',
8108 ), false );
8109
8110 // Send delete event.
8111 $this->get_api_site_scope()->call( '/', 'delete' );
8112
8113 $this->do_action( 'after_account_delete' );
8114 }
8115
8116 /**
8117 * Delete network level account.
8118 *
8119 * @author Vova Feldman (@svovaf)
8120 * @since 2.0.0
8121 *
8122 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8123 */
8124 function delete_network_account_event( $check_user = true ) {
8125 $this->_logger->entrance( 'slug = ' . $this->_slug );
8126
8127 if ( $check_user && ! $this->is_user_admin() ) {
8128 return;
8129 }
8130
8131 $this->do_action( 'before_network_account_delete' );
8132
8133 // Clear all admin notices.
8134 $this->_admin_notices->clear_all_sticky();
8135
8136 $this->_delete_plans( false, false );
8137
8138 $this->_delete_licenses( false );
8139
8140 // Delete add-ons related to plugin's account.
8141 $this->_delete_account_addons( false );
8142
8143 // @todo Delete plans and licenses of add-ons.
8144
8145 self::$_accounts->store( true );
8146
8147 /**
8148 * IMPORTANT:
8149 * Clear crons must be executed before clearing all storage.
8150 * Otherwise, the cron will not be cleared.
8151 */
8152 $this->clear_sync_cron( true );
8153 $this->clear_install_sync_cron( true );
8154
8155 $sites = self::get_sites();
8156
8157 $install_ids = array();
8158 foreach ( $sites as $site ) {
8159 $blog_id = self::get_site_blog_id( $site );
8160
8161 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8162 continue;
8163 }
8164
8165 $install_id = $this->_delete_site( true, $blog_id );
8166
8167 // Clear all storage data.
8168 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8169
8170 if ( FS_Site::is_valid_id( $install_id ) ) {
8171 $install_ids[] = $install_id;
8172 }
8173
8174 switch_to_blog( $blog_id );
8175
8176 $this->do_action( 'after_account_delete' );
8177
8178 restore_current_blog();
8179 }
8180
8181 $this->_storage->clear_all( true, array(
8182 'connectivity_test',
8183 'is_on',
8184 ), true );
8185
8186 // Send delete event.
8187 if ( ! empty( $install_ids ) ) {
8188 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8189 }
8190
8191 $this->do_action( 'after_network_account_delete' );
8192 }
8193
8194 /**
8195 * Plugin deactivation hook.
8196 *
8197 * @author Vova Feldman (@svovaf)
8198 * @since 1.0.1
8199 */
8200 function _deactivate_plugin_hook() {
8201 $this->_logger->entrance( 'slug = ' . $this->_slug );
8202
8203 if ( ! $this->is_user_admin() ) {
8204 return;
8205 }
8206
8207 $is_network_deactivation = fs_is_network_admin();
8208 $storage_keys_for_removal = array();
8209
8210 $this->_admin_notices->clear_all_sticky();
8211
8212 $storage_keys_for_removal[] = 'sticky_optin_added';
8213 if ( isset( $this->_storage->sticky_optin_added ) ) {
8214 unset( $this->_storage->sticky_optin_added );
8215 }
8216
8217 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8218 // Remember that plugin was already installed.
8219 $this->_storage->is_plugin_new_install = false;
8220 }
8221
8222 // Hook to plugin uninstall.
8223 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8224
8225 $this->clear_module_main_file_cache();
8226 $this->clear_sync_cron( $this->_is_network_active );
8227 $this->clear_install_sync_cron();
8228
8229 if ( $this->is_registered() ) {
8230 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8231 FS_Plugin_Updater::instance( $this )->delete_update_data();
8232 }
8233
8234 if ( $is_network_deactivation ) {
8235 // Send deactivation event.
8236 $this->sync_installs( array(
8237 'is_active' => false,
8238 ) );
8239 } else {
8240 // Send deactivation event.
8241 $this->sync_install( array(
8242 'is_active' => false,
8243 ) );
8244 }
8245 } else {
8246 if ( false === $this->has_api_connectivity() && ! $this->is_premium() ) {
8247 // Reset connectivity test cache.
8248 $this->clear_connectivity_info();
8249
8250 $storage_keys_for_removal[] = 'connectivity_test';
8251 }
8252 }
8253
8254 if ( $is_network_deactivation ) {
8255 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8256 unset( $this->_storage->sticky_optin_added_ms );
8257 }
8258
8259 if ( ! empty( $storage_keys_for_removal ) ) {
8260 $sites = self::get_sites();
8261
8262 foreach ( $sites as $site ) {
8263 $blog_id = self::get_site_blog_id( $site );
8264
8265 foreach ( $storage_keys_for_removal as $key ) {
8266 $this->_storage->remove( $key, false, $blog_id );
8267 }
8268
8269 $this->_storage->save( $blog_id );
8270 }
8271 }
8272 }
8273
8274 // Clear API cache on deactivation.
8275 FS_Api::clear_cache();
8276
8277 $this->remove_sdk_reference();
8278 }
8279
8280 /**
8281 * @author Vova Feldman (@svovaf)
8282 * @since 1.1.6
8283 */
8284 private function remove_sdk_reference() {
8285 global $fs_active_plugins;
8286
8287 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8288 if ( $this->_plugin_basename == $data->plugin_path ) {
8289 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8290 break;
8291 }
8292 }
8293
8294 fs_fallback_to_newest_active_sdk();
8295 }
8296
8297 /**
8298 * @author Vova Feldman (@svovaf)
8299 * @since 1.1.3
8300 *
8301 * @param bool $is_anonymous
8302 * @param bool|int $network_or_blog_id Since 2.0.0
8303 */
8304 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8305 // Store information regarding skip to try and opt-in the user
8306 // again in the future.
8307 $skip_info = array(
8308 'is' => $is_anonymous,
8309 'timestamp' => WP_FS__SCRIPT_START_TIME,
8310 'version' => $this->get_plugin_version(),
8311 );
8312
8313 if ( true === $network_or_blog_id ) {
8314 $this->_storage->is_anonymous_ms = $skip_info;
8315 } else {
8316 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8317 }
8318
8319 $this->network_upgrade_mode_completed();
8320
8321 // Update anonymous mode cache.
8322 $this->_is_anonymous = $is_anonymous;
8323 }
8324
8325 /**
8326 * @author Vova Feldman (@svovaf)
8327 * @since 2.5.1
8328 *
8329 * @param bool|int $network_or_blog_id
8330 */
8331 private function unset_anonymous_mode( $network_or_blog_id = 0 ) {
8332 if ( true === $network_or_blog_id ) {
8333 unset( $this->_storage->is_anonymous_ms );
8334 } else {
8335 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8336 }
8337 }
8338
8339 /**
8340 * @author Vova Feldman (@svovaf)
8341 * @since 2.0.0
8342 *
8343 * @param int $blog_id Site ID.
8344 * @param int $user_id User ID.
8345 * @param string $domain Site domain.
8346 * @param string $path Site path.
8347 * @param int $network_id Network ID. Only relevant on multi-network installations.
8348 * @param array $meta Metadata. Used to set initial site options.
8349 *
8350 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8351 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8352 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8353 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8354 */
8355 public function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8356 $this->_logger->entrance();
8357
8358 if ( ! $this->_is_network_active ) {
8359 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
8360 return;
8361 }
8362
8363 $site = null;
8364 $new_blog_id = $blog_id;
8365
8366 if ( $this->is_premium() &&
8367 $this->is_network_connected() &&
8368 is_object( $this->_license ) &&
8369 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8370 $this->is_license_network_active( $blog_id )
8371 ) {
8372 /**
8373 * 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.
8374 */
8375 $current_blog_id = get_current_blog_id();
8376 $license = clone $this->_license;
8377
8378 $this->switch_to_blog( $blog_id );
8379
8380 // Opt-in with network user.
8381 $this->install_with_user(
8382 $this->get_network_user(),
8383 $license->secret_key,
8384 false,
8385 false,
8386 false
8387 );
8388
8389 if ( is_object( $this->_site ) ) {
8390 if ( $this->_site->license_id == $license->id ) {
8391 /**
8392 * If the license was activated successfully, sync the license data from the remote server.
8393 */
8394 $this->_license = $license;
8395 $this->sync_site_license();
8396 }
8397 }
8398
8399 $site = $this->_site;
8400
8401 $this->switch_to_blog( $current_blog_id );
8402
8403 if ( is_object( $site ) ) {
8404 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id, $site );
8405
8406 // Already connected (with or without a license), so no need to continue.
8407 return;
8408 }
8409 }
8410
8411 if ( $this->is_network_anonymous() ) {
8412 /**
8413 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8414 */
8415 $this->skip_site_connection( $blog_id );
8416 } else if ( $this->is_network_delegated_connection() ) {
8417 /**
8418 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8419 */
8420 $this->delegate_site_connection( $blog_id );
8421 } else if ( $this->is_network_connected() ) {
8422 /**
8423 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8424 */
8425 $current_blog_id = get_current_blog_id();
8426
8427 $this->switch_to_blog( $blog_id );
8428
8429 // Opt-in with network user.
8430 $this->install_with_user(
8431 $this->get_network_user(),
8432 false,
8433 false,
8434 false,
8435 false
8436 );
8437
8438 $site = $this->_site;
8439
8440 $this->switch_to_blog( $current_blog_id );
8441 } else {
8442 /**
8443 * If the super-admin mixed different options (connect, skip, delegated):
8444 * a) If at least one site connection was delegated, then automatically delegate connection.
8445 * 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.
8446 */
8447 $has_delegated_site = false;
8448
8449 $sites = self::get_sites();
8450 foreach ( $sites as $wp_site ) {
8451 $blog_id = self::get_site_blog_id( $wp_site );
8452
8453 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8454 $has_delegated_site = true;
8455 break;
8456 }
8457 }
8458
8459 if ( $has_delegated_site ) {
8460 $this->delegate_site_connection( $blog_id );
8461 } else {
8462 $this->skip_site_connection( $blog_id );
8463 }
8464 }
8465
8466 /**
8467 * 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.
8468 *
8469 * @author Leo Fajardo (@leorw)
8470 * @since 2.5.0
8471 */
8472 FS_Clone_Manager::instance()->store_blog_install_info( $new_blog_id, $site );
8473 }
8474
8475 /**
8476 * @author Vova Feldman (@svovaf)
8477 * @since 2.5.0
8478 *
8479 * @param \WP_Site $new_site
8480 * @param array $args
8481 */
8482 public function _after_wp_initialize_site_callback( WP_Site $new_site, $args ) {
8483 $this->_logger->entrance();
8484
8485 $this->_after_new_blog_callback(
8486 $new_site->id,
8487 // Dummy user ID (not in use).
8488 0,
8489 $new_site->domain,
8490 $new_site->path,
8491 $new_site->network_id,
8492 // Dummy meta, not in use.
8493 array()
8494 );
8495 }
8496
8497 /**
8498 * @author Vova Feldman (@svovaf)
8499 * @since 1.1.3
8500 *
8501 * @param bool|int|int[] $network_or_blog_ids Since 2.0.0.
8502 */
8503 private function reset_anonymous_mode( $network_or_blog_ids = false ) {
8504 if ( true === $network_or_blog_ids ) {
8505 $this->unset_anonymous_mode( true );
8506
8507 if ( fs_is_network_admin() ) {
8508 $this->_is_anonymous = null;
8509 }
8510
8511 // Rest anonymous mode for all non-delegated sub-sites.
8512 $blog_ids = $this->get_non_delegated_blog_ids();
8513 }
8514 else
8515 {
8516 if ( false === $network_or_blog_ids ) {
8517 $network_or_blog_ids = 0;
8518 }
8519
8520 $blog_ids = is_array( $network_or_blog_ids ) ?
8521 $network_or_blog_ids :
8522 array( $network_or_blog_ids );
8523
8524 foreach ( $blog_ids as $blog_id ) {
8525 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8526 $this->_is_anonymous = null;
8527 }
8528 }
8529 }
8530
8531 foreach ( $blog_ids as $blog_id ) {
8532 $this->unset_anonymous_mode( $blog_id );
8533 }
8534
8535 /**
8536 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8537 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8538 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8539 *
8540 * @author Leo Fajardo (@leorw)
8541 * @since 1.2.2
8542 */
8543 if ( ! $this->_is_network_active ) {
8544 $this->_is_anonymous = null;
8545 }
8546 }
8547
8548 /**
8549 * @author Leo Fajardo (@leorw)
8550 * @since 2.5.3
8551 */
8552 private function update_license_required_permissions_if_anonymous() {
8553 if ( ! $this->is_anonymous() ) {
8554 return;
8555 }
8556
8557 $this->reset_anonymous_mode( fs_is_network_admin() );
8558
8559 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
8560 'essentials' => true,
8561 'events' => true,
8562 'diagnostic' => false,
8563 'extensions' => false,
8564 'site' => false,
8565 ) );
8566 }
8567
8568 /**
8569 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8570 * deleting the account in the network level, the URL of the page to redirect to is correct.
8571 *
8572 * @author Leo Fajardo (@leorw)
8573 *
8574 * @since 2.1.3
8575 */
8576 private function maybe_set_slug_and_network_menu_exists_flag() {
8577 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8578 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8579 $this->_menu->get_slug() :
8580 $this->_slug
8581 );
8582 }
8583 }
8584
8585 /**
8586 * Clears the anonymous mode and redirects to the opt-in screen.
8587 *
8588 * @author Vova Feldman (@svovaf)
8589 * @since 1.1.7
8590 */
8591 function connect_again() {
8592 if ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) {
8593 return;
8594 }
8595
8596 if ( $this->is_anonymous() ) {
8597 $this->reset_anonymous_mode( fs_is_network_admin() );
8598 }
8599
8600 $activation_url_params = array();
8601
8602 if ( $this->is_pending_activation() ) {
8603 $this->clear_pending_activation_mode();
8604
8605 if ( fs_request_get_bool( 'require_license' ) ) {
8606 $activation_url_params['require_license'] = true;
8607 }
8608 }
8609
8610 $this->maybe_set_slug_and_network_menu_exists_flag();
8611
8612 fs_redirect( $this->get_activation_url( $activation_url_params ) );
8613 }
8614
8615 /**
8616 * Skip account connect, and set anonymous mode.
8617 *
8618 * @author Vova Feldman (@svovaf)
8619 * @since 1.1.1
8620 *
8621 * @param bool|int|int[] $network_or_blog_ids Since 2.5.1
8622 */
8623 function skip_connection( $network_or_blog_ids = false ) {
8624 $this->_logger->entrance();
8625
8626 $this->_admin_notices->remove_sticky( 'connect_account' );
8627
8628 if ( true === $network_or_blog_ids ) {
8629 $this->set_anonymous_mode( true, true );
8630
8631 if ( fs_is_network_admin() ) {
8632 $this->_is_anonymous = null;
8633 }
8634
8635 // Rest anonymous mode for all non-delegated sub-sites.
8636 $blog_ids = $this->get_non_delegated_blog_ids();
8637 }
8638 else
8639 {
8640 if ( false === $network_or_blog_ids ) {
8641 $network_or_blog_ids = 0;
8642 }
8643
8644 $blog_ids = is_array( $network_or_blog_ids ) ?
8645 $network_or_blog_ids :
8646 array( $network_or_blog_ids );
8647
8648 foreach ( $blog_ids as $blog_id ) {
8649 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8650 $this->_is_anonymous = null;
8651 }
8652 }
8653 }
8654
8655 foreach ( $blog_ids as $blog_id ) {
8656 $this->skip_site_connection( $blog_id );
8657 }
8658
8659 $this->network_upgrade_mode_completed();
8660 }
8661
8662 /**
8663 * Skip connection for specific site in the network.
8664 *
8665 * @author Vova Feldman (@svovaf)
8666 * @since 2.0.0
8667 *
8668 * @param int|null $blog_id
8669 * @param bool $send_skip
8670 */
8671 private function skip_site_connection( $blog_id = null ) {
8672 $this->_logger->entrance();
8673
8674 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8675
8676 $this->set_anonymous_mode( true, $blog_id );
8677 }
8678
8679 /**
8680 * Plugin version update hook.
8681 *
8682 * @author Vova Feldman (@svovaf)
8683 * @since 1.0.4
8684 */
8685 private function update_plugin_version_event() {
8686 $this->_logger->entrance();
8687
8688 if ( ! $this->is_registered() ) {
8689 return;
8690 }
8691
8692 $this->maybe_schedule_install_sync_cron();
8693 // $this->sync_install( array(), true );
8694 }
8695
8696 /**
8697 * Generate an MD5 signature of a plugins collection.
8698 * This helper methods used to identify changes in a plugins collection.
8699 *
8700 * @author Vova Feldman (@svovaf)
8701 * @since 2.0.0
8702 *
8703 * @param array [string]array $plugins
8704 *
8705 * @return string
8706 */
8707 private function get_plugins_thumbprint( $plugins ) {
8708 ksort( $plugins );
8709
8710 $thumbprint = '';
8711 foreach ( $plugins as $basename => $data ) {
8712 $thumbprint .= $data['slug'] . ',' .
8713 $data['Version'] . ',' .
8714 ( $data['is_active'] ? '1' : '0' ) . ';';
8715 }
8716
8717 return md5( $thumbprint );
8718 }
8719
8720 /**
8721 * Return a list of modified plugins since the last sync.
8722 *
8723 * Note:
8724 * There's no point to store a plugins counter since even if the number of
8725 * plugins didn't change, we still need to check if the versions are all the
8726 * same and the activity state is similar.
8727 *
8728 * @author Vova Feldman (@svovaf)
8729 * @since 1.1.8
8730 *
8731 * @return array|false
8732 */
8733 private function get_plugins_data_for_api() {
8734 // Alias.
8735 $site_active_plugins_option_name = 'active_plugins';
8736 $network_plugins_option_name = 'all_plugins';
8737
8738 /**
8739 * Collection of all site level active plugins.
8740 */
8741 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8742
8743 if ( ! is_object( $site_active_plugins_cache ) ) {
8744 $site_active_plugins_cache = (object) array(
8745 'timestamp' => '',
8746 'md5' => '',
8747 'plugins' => array(),
8748 );
8749 }
8750
8751 $time = time();
8752
8753 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
8754 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8755 ) {
8756 // Don't send plugin updates if last update was in the past 5 min.
8757 return false;
8758 }
8759
8760 // Write timestamp to lock the logic.
8761 $site_active_plugins_cache->timestamp = $time;
8762 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8763
8764 // Reload options from DB.
8765 self::$_accounts->load( true );
8766 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8767
8768 if ( $time != $site_active_plugins_cache->timestamp ) {
8769 // If timestamp is different, then another thread captured the lock.
8770 return false;
8771 }
8772
8773 /**
8774 * Collection of all plugins (network level).
8775 */
8776 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
8777
8778 if ( ! is_object( $network_plugins_cache ) ) {
8779 $network_plugins_cache = (object) array(
8780 'timestamp' => '',
8781 'md5' => '',
8782 'plugins' => array(),
8783 );
8784 }
8785
8786 // Check if there's a change in plugins.
8787 $network_plugins = self::get_network_plugins();
8788 $site_active_plugins = self::get_site_active_plugins();
8789
8790 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
8791 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
8792
8793 // Check if plugins status changed (version or active/inactive).
8794 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
8795 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
8796
8797 if ( ! $network_plugins_changed &&
8798 ! $site_active_plugins_changed
8799 ) {
8800 // No changes.
8801 return array();
8802 }
8803
8804 $plugins_update_data = array();
8805
8806 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
8807 if ( ! isset( $network_plugins[ $basename ] ) ) {
8808 // Plugin uninstalled.
8809 $uninstalled_plugin_data = $data;
8810 $uninstalled_plugin_data['is_active'] = false;
8811 $uninstalled_plugin_data['is_uninstalled'] = true;
8812 $plugins_update_data[] = $uninstalled_plugin_data;
8813
8814 unset( $network_plugins[ $basename ] );
8815
8816 unset( $network_plugins_cache->plugins[ $basename ] );
8817 unset( $site_active_plugins_cache->plugins[ $basename ] );
8818
8819 continue;
8820 }
8821
8822 $was_active = $data['is_active'] ||
8823 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8824 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
8825 $is_active = $network_plugins[ $basename ]['is_active'] ||
8826 ( isset( $site_active_plugins[ $basename ] ) &&
8827 $site_active_plugins[ $basename ]['is_active'] );
8828
8829 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8830 isset( $site_active_plugins[ $basename ] )
8831 ) {
8832 // Plugin was site level activated.
8833 $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
8834 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
8835 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8836 ! isset( $site_active_plugins[ $basename ] )
8837 ) {
8838 // Plugin was site level deactivated.
8839 unset( $site_active_plugins_cache->plugins[ $basename ] );
8840 }
8841
8842 $prev_version = $data['version'];
8843 $current_version = $network_plugins[ $basename ]['Version'];
8844
8845 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
8846 // Plugin activated or deactivated, or version changed.
8847
8848 if ( $was_active !== $is_active ) {
8849 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
8850 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
8851 }
8852 }
8853
8854 if ( $prev_version !== $current_version ) {
8855 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
8856 }
8857
8858 $updated_plugin_data = $data;
8859 $updated_plugin_data['is_active'] = $is_active;
8860 $updated_plugin_data['version'] = $current_version;
8861 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
8862 $plugins_update_data[] = $updated_plugin_data;
8863 }
8864 }
8865
8866 // Find new plugins that weren't yet seen before.
8867 foreach ( $network_plugins as $basename => $data ) {
8868 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
8869 // New plugin.
8870 $new_plugin = array(
8871 'slug' => $data['slug'],
8872 'version' => $data['Version'],
8873 'title' => $data['Name'],
8874 'is_active' => $data['is_active'],
8875 'is_uninstalled' => false,
8876 );
8877
8878 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
8879
8880 $is_site_level_active = (
8881 isset( $site_active_plugins[ $basename ] ) &&
8882 $site_active_plugins[ $basename ]['is_active']
8883 );
8884
8885 /**
8886 * If not network active, set the activity status based on the site-level plugin status.
8887 */
8888 if ( ! $new_plugin['is_active'] ) {
8889 $new_plugin['is_active'] = $is_site_level_active;
8890 }
8891
8892 $plugins_update_data[] = $new_plugin;
8893
8894 if ( isset( $site_active_plugins[ $basename ] ) ) {
8895 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
8896 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
8897 }
8898 }
8899 }
8900
8901 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
8902 $site_active_plugins_cache->timestamp = $time;
8903 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8904
8905 $network_plugins_cache->md5 = $network_plugins_thumbprint;
8906 $network_plugins_cache->timestamp = $time;
8907 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
8908
8909 return $plugins_update_data;
8910 }
8911
8912 /**
8913 * Return a list of modified themes since the last sync.
8914 *
8915 * Note:
8916 * There's no point to store a themes counter since even if the number of
8917 * themes didn't change, we still need to check if the versions are all the
8918 * same and the activity state is similar.
8919 *
8920 * @author Vova Feldman (@svovaf)
8921 * @since 1.1.8
8922 *
8923 * @return array|false
8924 */
8925 private function get_themes_data_for_api() {
8926 // Alias.
8927 $option_name = 'all_themes';
8928
8929 $all_cached_themes = self::$_accounts->get_option( $option_name );
8930
8931 if ( ! is_object( $all_cached_themes ) ) {
8932 $all_cached_themes = (object) array(
8933 'timestamp' => '',
8934 'md5' => '',
8935 'themes' => array(),
8936 );
8937 }
8938
8939 $time = time();
8940
8941 if ( ! empty( $all_cached_themes->timestamp ) &&
8942 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8943 ) {
8944 // Don't send theme updates if last update was in the past 5 min.
8945 return false;
8946 }
8947
8948 // Write timestamp to lock the logic.
8949 $all_cached_themes->timestamp = $time;
8950 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
8951
8952 // Reload options from DB.
8953 self::$_accounts->load( true );
8954 $all_cached_themes = self::$_accounts->get_option( $option_name );
8955
8956 if ( $time != $all_cached_themes->timestamp ) {
8957 // If timestamp is different, then another thread captured the lock.
8958 return false;
8959 }
8960
8961 // Get active theme.
8962 $active_theme = wp_get_theme();
8963 $active_theme_stylesheet = $active_theme->get_stylesheet();
8964
8965 // Check if there's a change in themes.
8966 $all_themes = wp_get_themes();
8967
8968 // Check if themes changed.
8969 ksort( $all_themes );
8970
8971 $themes_signature = '';
8972 foreach ( $all_themes as $slug => $data ) {
8973 $is_active = ( $slug === $active_theme_stylesheet );
8974 $themes_signature .= $slug . ',' .
8975 $data->version . ',' .
8976 ( $is_active ? '1' : '0' ) . ';';
8977 }
8978
8979 // Check if themes status changed (version or active/inactive).
8980 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
8981
8982 $themes_update_data = array();
8983
8984 if ( $themes_changed ) {
8985 // Change in themes, report changes.
8986
8987 // Update existing themes info.
8988 foreach ( $all_cached_themes->themes as $slug => $data ) {
8989 $is_active = ( $slug === $active_theme_stylesheet );
8990
8991 if ( ! isset( $all_themes[ $slug ] ) ) {
8992 // Plugin uninstalled.
8993 $uninstalled_theme_data = $data;
8994 $uninstalled_theme_data['is_active'] = false;
8995 $uninstalled_theme_data['is_uninstalled'] = true;
8996 $themes_update_data[] = $uninstalled_theme_data;
8997
8998 unset( $all_themes[ $slug ] );
8999 unset( $all_cached_themes->themes[ $slug ] );
9000 } else if ( $data['is_active'] !== $is_active ||
9001 $data['version'] !== $all_themes[ $slug ]->version
9002 ) {
9003 // Plugin activated or deactivated, or version changed.
9004
9005 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
9006 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
9007
9008 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
9009 }
9010 }
9011
9012 // Find new themes that weren't yet seen before.
9013 foreach ( $all_themes as $slug => $data ) {
9014 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
9015 $is_active = ( $slug === $active_theme_stylesheet );
9016
9017 // New plugin.
9018 $new_plugin = array(
9019 'slug' => $slug,
9020 'version' => $data->version,
9021 'title' => $data->name,
9022 'is_active' => $is_active,
9023 'is_uninstalled' => false,
9024 );
9025
9026 $themes_update_data[] = $new_plugin;
9027 $all_cached_themes->themes[ $slug ] = $new_plugin;
9028 }
9029 }
9030
9031 $all_cached_themes->md5 = md5( $themes_signature );
9032 $all_cached_themes->timestamp = time();
9033 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9034 }
9035
9036 return $themes_update_data;
9037 }
9038
9039 /**
9040 * Get site data for API install request.
9041 *
9042 * @author Vova Feldman (@svovaf)
9043 * @since 1.1.2
9044 *
9045 * @param string[] $override
9046 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9047 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9048 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, title, and URL).
9049 *
9050 * @return array
9051 */
9052 private function get_install_data_for_api(
9053 array $override,
9054 $include_plugins = true,
9055 $include_themes = true,
9056 $include_blog_data = true
9057 ) {
9058 // Alias.
9059 $permissions = FS_Permission_Manager::instance( $this );
9060
9061 if ( $permissions->is_extensions_tracking_allowed() ) {
9062 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
9063 /**
9064 * @since 1.1.8 Also send plugin updates.
9065 */
9066 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9067 $plugins = $this->get_plugins_data_for_api();
9068 if ( ! empty( $plugins ) ) {
9069 $override['plugins'] = $plugins;
9070 }
9071 }
9072 }
9073
9074 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
9075 /**
9076 * @since 1.1.8 Also send themes updates.
9077 */
9078 if ( $include_themes && ! isset( $override['themes'] ) ) {
9079 $themes = $this->get_themes_data_for_api();
9080 if ( ! empty( $themes ) ) {
9081 $override['themes'] = $themes;
9082 }
9083 }
9084 }
9085 }
9086
9087 $versions = $this->get_versions();
9088
9089 $blog_data = array();
9090 if ( $include_blog_data ) {
9091 $blog_data['url'] = self::get_unfiltered_site_url();
9092
9093 if ( $permissions->is_diagnostic_tracking_allowed() ) {
9094 $blog_data = array_merge( $blog_data, array(
9095 'language' => self::get_sanitized_language(),
9096 'title' => get_bloginfo( 'name' ),
9097 ) );
9098 }
9099 }
9100
9101 return array_merge( $versions, $blog_data, array(
9102 'version' => $this->get_plugin_version(),
9103 'is_premium' => $this->is_premium(),
9104 // Special params.
9105 'is_active' => true,
9106 'is_uninstalled' => false,
9107 ), $override );
9108 }
9109
9110 /**
9111 * Update installs details.
9112 *
9113 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
9114 *
9115 * @author Vova Feldman (@svovaf)
9116 * @since 2.0.0
9117 *
9118 * @param string[] string $override
9119 * @param bool $only_diff
9120 * @param bool $is_keepalive
9121 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9122 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9123 *
9124 * @return array
9125 */
9126 private function get_installs_data_for_api(
9127 array $override,
9128 $only_diff = false,
9129 $is_keepalive = false,
9130 $include_plugins = true,
9131 $include_themes = true
9132 ) {
9133 /**
9134 * @since 1.1.8 Also send plugin updates.
9135 */
9136 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9137 // $plugins = $this->get_plugins_data_for_api();
9138 // if ( ! empty( $plugins ) ) {
9139 // $override['plugins'] = $plugins;
9140 // }
9141 // }
9142 /**
9143 * @since 1.1.8 Also send themes updates.
9144 */
9145 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9146 // $themes = $this->get_themes_data_for_api();
9147 // if ( ! empty( $themes ) ) {
9148 // $override['themes'] = $themes;
9149 // }
9150 // }
9151
9152 // Common properties.
9153 $versions = $this->get_versions();
9154 $common = array_merge( $versions, array(
9155 'version' => $this->get_plugin_version(),
9156 'is_premium' => $this->is_premium(),
9157 ), $override );
9158
9159
9160 $is_common_diff_for_any_site = false;
9161 $common_diff_union = array();
9162
9163 $installs_data = array();
9164
9165 $sites = self::get_sites();
9166
9167 $subsite_data_for_api_by_install_id = array();
9168 $install_url_by_install_id = array();
9169 $subsite_registration_date_by_install_id = array();
9170
9171 foreach ( $sites as $site ) {
9172 $blog_id = self::get_site_blog_id( $site );
9173
9174 $install = $this->get_install_by_blog_id( $blog_id );
9175
9176 if ( is_object( $install ) ) {
9177 if ( $install->user_id != $this->_user->id ) {
9178 // Install belongs to a different owner.
9179 continue;
9180 }
9181
9182 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
9183 // Don't send updates regarding opted-out installs.
9184 continue;
9185 }
9186
9187 $install_data = $this->get_site_info( $site, true );
9188
9189 if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $install_data['blog_id'] ) ) {
9190 continue;
9191 }
9192
9193 $uid = $install_data['uid'];
9194 $url = $install_data['url'];
9195 $registration_date = $install_data['registration_date'];
9196
9197 if ( isset( $subsite_data_for_api_by_install_id[ $install->id ] ) ) {
9198 $clone_subsite_data = $subsite_data_for_api_by_install_id[ $install->id ];
9199 $clone_install_url = $install_url_by_install_id[ $install->id ];
9200 $clone_subsite_registration_date = $subsite_registration_date_by_install_id[ $install->id ];
9201
9202 $skip = false;
9203
9204 if (
9205 ! empty( $install_data['registration_date'] ) &&
9206 ! empty( $clone_subsite_registration_date )
9207 ) {
9208 /**
9209 * 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.
9210 *
9211 * @author Leo Fajardo (@leorw)
9212 * @since 2.5.1
9213 */
9214 $skip = ( strtotime( $install_data['registration_date'] ) > strtotime( $clone_subsite_registration_date ) );
9215 } else if (
9216 /**
9217 * 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.
9218 *
9219 * @author Leo Fajardo (@leorw)
9220 * @since 2.5.0
9221 */
9222 fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) ||
9223 fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $url ) )
9224 ) {
9225 $skip = true;
9226 }
9227
9228 if ( $skip ) {
9229 // 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.
9230 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
9231 continue;
9232 }
9233 }
9234
9235 unset( $install_data['blog_id'] );
9236 unset( $install_data['uid'] );
9237 unset( $install_data['url'] );
9238 unset( $install_data['registration_date'] );
9239
9240 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9241 $install_data['is_uninstalled'] = $install->is_uninstalled;
9242
9243 $common_diff = null;
9244 $is_common_diff = false;
9245 if ( $only_diff ) {
9246 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9247 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9248
9249 $is_common_diff = ! empty( $common_diff );
9250
9251 if ( $is_common_diff ) {
9252 foreach ( $common_diff as $k => $v ) {
9253 if ( ! isset( $common_diff_union[ $k ] ) ) {
9254 $common_diff_union[ $k ] = $v;
9255 }
9256 }
9257 }
9258
9259 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9260 }
9261
9262 if ( ! empty( $install_data ) || $is_common_diff || $is_keepalive ) {
9263 // Add install ID and site unique ID.
9264 $install_data['id'] = $install->id;
9265 $install_data['uid'] = $uid;
9266 $install_data['url'] = $url;
9267
9268 $subsite_data_for_api_by_install_id[ $install->id ] = $install_data;
9269 $install_url_by_install_id[ $install->id ] = $install->url;
9270 $subsite_registration_date_by_install_id[ $install->id ] = $registration_date;
9271 }
9272 }
9273 }
9274
9275 restore_current_blog();
9276
9277 $installs_data = array_merge(
9278 $installs_data,
9279 array_values( $subsite_data_for_api_by_install_id )
9280 );
9281
9282 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9283 if ( ! $only_diff ) {
9284 $installs_data[] = $common;
9285 } else if ( ! empty( $common_diff_union ) ) {
9286 $installs_data[] = $common_diff_union;
9287 }
9288 }
9289
9290 foreach ( $installs_data as &$data ) {
9291 $data = (object) $data;
9292 }
9293
9294 return $installs_data;
9295 }
9296
9297 /**
9298 * Compare site actual data to the stored install data and return the differences for an API data sync.
9299 *
9300 * @author Vova Feldman (@svovaf)
9301 * @since 2.0.0
9302 *
9303 * @param array $site
9304 * @param FS_Site $install
9305 * @param string[] string $override
9306 *
9307 * @return array
9308 */
9309 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9310 $diff = array();
9311 $special = array();
9312 $special_override = false;
9313
9314 foreach ( $site as $p => $v ) {
9315 if ( property_exists( $install, $p ) ) {
9316 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9317 $install->{$p} != $v
9318 ) {
9319 $val = self::get_api_sanitized_property( $p, $v );
9320
9321 if ( $install->{$p} != $val ) {
9322 $install->{$p} = $val;
9323 $diff[ $p ] = $val;
9324 }
9325 }
9326 } else {
9327 $special[ $p ] = $v;
9328
9329 if ( isset( $override[ $p ] ) ||
9330 'plugins' === $p ||
9331 'themes' === $p
9332 ) {
9333 $special_override = true;
9334 }
9335 }
9336 }
9337
9338 if ( $special_override || 0 < count( $diff ) ) {
9339 // Add special params only if has at least one
9340 // standard param, or if explicitly requested to
9341 // override a special param or a param which is not exist
9342 // in the install object.
9343 $diff = array_merge( $diff, $special );
9344 }
9345
9346 return $diff;
9347 }
9348
9349 /**
9350 * @author Leo Fajardo (@leorw)
9351 * @since 2.5.1
9352 */
9353 private function send_pending_clone_update_once() {
9354 $this->_logger->entrance();
9355
9356 if ( ! empty( $this->_storage->clone_id ) ) {
9357 return;
9358 }
9359
9360 $install_clone = $this->get_api_site_scope()->call(
9361 '/clones',
9362 'post',
9363 array( 'site_url' => self::get_unfiltered_site_url() )
9364 );
9365
9366 if ( $this->is_api_result_entity( $install_clone ) ) {
9367 $this->_storage->clone_id = $install_clone->id;
9368 }
9369 }
9370
9371 /**
9372 * @author Leo Fajardo (@leorw)
9373 * @since 2.5.1
9374 *
9375 * @param string $resolution_type
9376 * @param FS_Site $clone_context_install
9377 */
9378 function send_clone_resolution_update( $resolution_type, $clone_context_install ) {
9379 $this->_logger->entrance();
9380
9381 if ( empty( $this->_storage->clone_id ) ) {
9382 return;
9383 }
9384
9385 $new_install_id = null;
9386 $current_site = null;
9387
9388 $flush = false;
9389
9390 /**
9391 * 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.
9392 */
9393 if ( $clone_context_install->id != $this->_site->id ) {
9394 $new_install_id = $this->_site->id;
9395 $current_site = $this->_site;
9396 $this->_site = $clone_context_install;
9397
9398 $flush = true;
9399 }
9400
9401 $this->get_api_site_scope( $flush )->call(
9402 "/clones/{$this->_storage->clone_id}",
9403 'put',
9404 array(
9405 'resolution' => $resolution_type,
9406 'new_install_id' => $new_install_id,
9407 )
9408 );
9409
9410 if ( is_object( $current_site ) ) {
9411 /**
9412 * Ensure that the install scope entity is updated back to the previous install entity.
9413 */
9414 $this->_site = $current_site;
9415
9416 // Restore the previous install scope entity of the API.
9417 $this->get_api_site_scope( true );
9418 }
9419 }
9420
9421 /**
9422 * Update install only if changed.
9423 *
9424 * @author Vova Feldman (@svovaf)
9425 * @since 1.0.9
9426 *
9427 * @param string[] string $override
9428 * @param bool $flush
9429 * @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.
9430 *
9431 * @return false|object|string
9432 */
9433 private function send_install_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9434 $this->_logger->entrance();
9435
9436 $check_properties = $this->get_install_data_for_api( $override );
9437
9438 if ( $flush ) {
9439 $params = $check_properties;
9440 } else {
9441 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9442 }
9443
9444 if ( empty( $params ) ) {
9445 $keepalive_only_update = $this->should_send_keepalive_update();
9446
9447 if ( ! $keepalive_only_update ) {
9448 /**
9449 * There are no updates to send including keepalive.
9450 *
9451 * @author Leo Fajardo (@leorw)
9452 * @since 2.2.3
9453 */
9454 return false;
9455 }
9456 }
9457
9458 if ( $is_two_way_sync ) {
9459 /**
9460 * Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9461 *
9462 * @author Leo Fajardo (@leorw)
9463 * @since 2.2.3
9464 */
9465 if ( ! is_multisite() ) {
9466 // Update last install sync timestamp.
9467 $this->set_cron_execution_timestamp( 'install_sync' );
9468 }
9469
9470 $params['uid'] = $this->get_anonymous_id();
9471 }
9472
9473 $this->set_keepalive_timestamp();
9474
9475 // Send updated values to FS.
9476 $site = $this->api_site_call( '/', 'put', $params, true );
9477
9478 if ( $is_two_way_sync && $this->is_api_result_entity( $site ) ) {
9479 /**
9480 * Clear scheduled install sync after a two-way sync call.
9481 *
9482 * @author Leo Fajardo (@leorw)
9483 * @since 2.2.3
9484 */
9485 if ( ! is_multisite() ) {
9486 // I successfully sent install update, clear scheduled sync if exist.
9487 $this->clear_install_sync_cron();
9488 }
9489 }
9490
9491 return $site;
9492 }
9493
9494 /**
9495 * Update installs only if changed.
9496 *
9497 * @author Vova Feldman (@svovaf)
9498 * @since 2.0.0
9499 *
9500 * @param string[] string $override
9501 * @param bool $flush
9502 * @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.
9503 *
9504 * @return false|object|string
9505 */
9506 private function send_installs_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9507 $this->_logger->entrance();
9508
9509 /**
9510 * Pass `true` to use the network level storage since the update is for many installs.
9511 *
9512 * @author Leo Fajardo (@leorw)
9513 * @since 2.2.3
9514 */
9515 $should_send_keepalive = $this->should_send_keepalive_update( true );
9516
9517 $installs_data = $this->get_installs_data_for_api( $override, ! $flush, $should_send_keepalive );
9518
9519 if ( empty( $installs_data ) ) {
9520 return false;
9521 }
9522
9523 if ( $is_two_way_sync ) {
9524 // Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9525 $this->set_cron_execution_timestamp( 'install_sync' );
9526 }
9527
9528 /**
9529 * Pass `true` to use the network level storage since the update is for many installs.
9530 *
9531 * @author Leo Fajardo (@leorw)
9532 * @since 2.2.3
9533 */
9534 $this->set_keepalive_timestamp( true );
9535
9536 // Send updated values to FS.
9537 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9538
9539 if ( $is_two_way_sync && $this->is_api_result_object( $result, 'installs' ) ) {
9540 // I successfully sent a two-way installs update, clear the scheduled install sync if it exists.
9541 $this->clear_install_sync_cron();
9542 }
9543
9544 return $result;
9545 }
9546
9547 /**
9548 * @author Leo Fajardo (@leorw)
9549 *
9550 * @param bool|null $use_network_level_storage
9551 *
9552 * @return bool
9553 */
9554 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9555 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9556
9557 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9558 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9559 return true;
9560 } else {
9561 // 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.
9562 return ( 7 == rand( 1, 7 ) );
9563 }
9564 }
9565
9566 /**
9567 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9568 *
9569 * @author Leo Fajardo (@leorw)
9570 * @since 2.3.2
9571 */
9572 private function maybe_sync_install_user() {
9573 if ( $this->_user->id == $this->_site->user_id ) {
9574 return;
9575 }
9576
9577 // Fetch user data and store if found.
9578 $this->sync_user_by_current_install();
9579 }
9580
9581 /**
9582 * Update install only if changed.
9583 *
9584 * @author Vova Feldman (@svovaf)
9585 * @since 1.0.9
9586 *
9587 * @param string[] string $override
9588 * @param bool $flush
9589 */
9590 function sync_install( $override = array(), $flush = false ) {
9591 $this->_logger->entrance();
9592
9593 $site = $this->send_install_update( $override, $flush, true );
9594
9595 if ( false === $site ) {
9596 // No sync required.
9597 return;
9598 }
9599
9600 if ( ! $this->is_api_result_entity( $site ) ) {
9601 // Failed to sync, don't update locally.
9602 return;
9603 }
9604
9605 $this->_site = new FS_Site( $site );
9606
9607 $this->_store_site( true );
9608 }
9609
9610 /**
9611 * Update install only if changed.
9612 *
9613 * @author Vova Feldman (@svovaf)
9614 * @since 1.0.9
9615 *
9616 * @param string[] string $override
9617 * @param bool $flush
9618 */
9619 private function sync_installs( $override = array(), $flush = false ) {
9620 $this->_logger->entrance();
9621
9622 $result = $this->send_installs_update( $override, $flush, true );
9623
9624 if ( false === $result ) {
9625 // No sync required.
9626 return;
9627 }
9628
9629 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9630 // Failed to sync, don't update locally.
9631 return;
9632 }
9633
9634 $address_to_blog_map = $this->get_address_to_blog_map();
9635
9636 foreach ( $result->installs as $install ) {
9637 $this->_site = new FS_Site( $install );
9638
9639 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9640 $blog_id = $address_to_blog_map[ $address ];
9641
9642 $this->_store_site( true, $blog_id );
9643 }
9644 }
9645
9646 /**
9647 * Track install's custom event.
9648 *
9649 * IMPORTANT:
9650 * Custom event tracking is currently only supported for specific clients.
9651 * If you are not one of them, please don't use this method. If you will,
9652 * the API will simply ignore your request based on the plugin ID.
9653 *
9654 * Need custom tracking for your plugin or theme?
9655 * If you are interested in custom event tracking please contact yo@freemius.com
9656 * for further details.
9657 *
9658 * @author Vova Feldman (@svovaf)
9659 * @since 1.2.1
9660 *
9661 * @param string $name Event name.
9662 * @param array $properties Associative key/value array with primitive values only
9663 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9664 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9665 *
9666 * @return object|false Event data or FALSE on failure.
9667 *
9668 * @throws \Freemius_InvalidArgumentException
9669 */
9670 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9671 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9672
9673 if ( ! $this->is_registered() ) {
9674 return false;
9675 }
9676
9677 $event = array( 'type' => $name );
9678
9679 if ( is_numeric( $process_at ) && $process_at > time() ) {
9680 $event['process_at'] = $process_at;
9681 }
9682
9683 if ( $once ) {
9684 $event['once'] = true;
9685 }
9686
9687 if ( ! empty( $properties ) ) {
9688 // Verify associative array values are primitive.
9689 foreach ( $properties as $k => $v ) {
9690 if ( ! is_scalar( $v ) ) {
9691 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9692 }
9693 }
9694
9695 $event['properties'] = $properties;
9696 }
9697
9698 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9699
9700 return $this->is_api_error( $result ) ?
9701 false :
9702 $result;
9703 }
9704
9705 /**
9706 * Track install's custom event only once, but it still triggers the API call.
9707 *
9708 * IMPORTANT:
9709 * Custom event tracking is currently only supported for specific clients.
9710 * If you are not one of them, please don't use this method. If you will,
9711 * the API will simply ignore your request based on the plugin ID.
9712 *
9713 * Need custom tracking for your plugin or theme?
9714 * If you are interested in custom event tracking please contact yo@freemius.com
9715 * for further details.
9716 *
9717 * @author Vova Feldman (@svovaf)
9718 * @since 1.2.1
9719 *
9720 * @param string $name Event name.
9721 * @param array $properties Associative key/value array with primitive values only
9722 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9723 *
9724 * @return object|false Event data or FALSE on failure.
9725 *
9726 * @throws \Freemius_InvalidArgumentException
9727 *
9728 * @user Freemius::track_event()
9729 */
9730 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9731 return $this->track_event( $name, $properties, $process_at, true );
9732 }
9733
9734 /**
9735 * Plugin uninstall hook.
9736 *
9737 * @author Vova Feldman (@svovaf)
9738 * @since 1.0.1
9739 *
9740 * @param bool $check_user Enforce checking if user have plugins activation privileges.
9741 */
9742 function _uninstall_plugin_event( $check_user = true ) {
9743 $this->_logger->entrance( 'slug = ' . $this->_slug );
9744
9745 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
9746 return;
9747 }
9748
9749 $params = array();
9750 $uninstall_reason = null;
9751 if ( isset( $this->_storage->uninstall_reason ) ) {
9752 $uninstall_reason = $this->_storage->uninstall_reason;
9753 $params['reason_id'] = $uninstall_reason->id;
9754 $params['reason_info'] = $uninstall_reason->info;
9755 }
9756
9757 if ( ! $this->is_registered() ) {
9758 // Send anonymous uninstall event only if user submitted a feedback.
9759 if ( isset( $uninstall_reason ) ) {
9760 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
9761 $this->opt_in( false, false, false, false, true );
9762 } else {
9763 $params['uid'] = $this->get_anonymous_id();
9764 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
9765 }
9766 }
9767 } else {
9768 $params = array_merge( $params, array(
9769 'is_active' => false,
9770 'is_uninstalled' => true,
9771 ) );
9772
9773 if ( $this->_is_network_active ) {
9774 // Send uninstall event.
9775 $this->send_installs_update( $params );
9776 } else {
9777 // Send uninstall event and handle the result.
9778 $this->sync_install( $params );
9779 }
9780 }
9781
9782 // @todo Decide if we want to delete plugin information from db.
9783 }
9784
9785 /**
9786 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
9787 *
9788 * @author Vova Feldman (@svovaf)
9789 * @since 2.2.1
9790 *
9791 * @param string $is_premium
9792 * @param string $caller
9793 *
9794 * @return void
9795 */
9796 function set_basename( $is_premium, $caller ) {
9797 $basename = plugin_basename( $caller );
9798
9799 $current_basename = $is_premium ?
9800 $this->_premium_plugin_basename :
9801 $this->_free_plugin_basename;
9802
9803 if ( $current_basename == $basename ) {
9804 // Basename value set correctly.
9805 return;
9806 }
9807
9808 if ( $is_premium ) {
9809 $this->_premium_plugin_basename = $basename;
9810 } else {
9811 $this->_free_plugin_basename = $basename;
9812 }
9813
9814 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
9815
9816 register_activation_hook(
9817 $plugin_dir . $basename,
9818 array( &$this, '_activate_plugin_event_hook' )
9819 );
9820 }
9821
9822 /**
9823 * @author Vova Feldman (@svovaf)
9824 * @since 1.1.1
9825 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
9826 *
9827 * @return string
9828 */
9829 function premium_plugin_basename() {
9830 if ( ! isset( $this->_premium_plugin_basename ) ) {
9831 $this->_premium_plugin_basename = $this->is_premium() ?
9832 // The product is premium, so use the current basename.
9833 $this->_plugin_basename :
9834 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
9835 }
9836
9837 return $this->_premium_plugin_basename;
9838 }
9839
9840 /**
9841 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
9842 *
9843 * @author Vova Feldman (@svovaf)
9844 * @since 1.0.2
9845 */
9846 public static function _uninstall_plugin_hook() {
9847 self::_load_required_static();
9848
9849 self::$_static_logger->entrance();
9850
9851 if ( ! current_user_can( 'activate_plugins' ) ) {
9852 return;
9853 }
9854
9855 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
9856
9857 self::$_static_logger->info( 'plugin = ' . $plugin_file );
9858
9859 define( 'WP_FS__UNINSTALL_MODE', true );
9860
9861 $fs = self::get_instance_by_file( $plugin_file );
9862
9863 if ( is_object( $fs ) ) {
9864 $fs->remove_sdk_reference();
9865
9866 self::require_plugin_essentials();
9867
9868 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
9869 is_plugin_active( $fs->premium_plugin_basename() )
9870 ) {
9871 // Deleting Free or Premium plugin version while the other version still installed.
9872 return;
9873 }
9874
9875 if (
9876 ! $fs->is_clone() &&
9877 /**
9878 * 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).
9879 *
9880 * @author Leo Fajardo
9881 */
9882 ( ! is_object( $fs->_site ) || $fs->is_registered() )
9883 ) {
9884 $fs->_uninstall_plugin_event();
9885 }
9886
9887 $fs->do_action( 'after_uninstall' );
9888 }
9889 }
9890
9891 #----------------------------------------------------------------------------------
9892 #region Plugin Information
9893 #----------------------------------------------------------------------------------
9894
9895 /**
9896 * Load WordPress core plugin.php essential module.
9897 *
9898 * @author Vova Feldman (@svovaf)
9899 * @since 1.1.1
9900 */
9901 private static function require_plugin_essentials() {
9902 if ( ! function_exists( 'get_plugins' ) ) {
9903 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
9904
9905 require_once ABSPATH . 'wp-admin/includes/plugin.php';
9906 }
9907 }
9908
9909 /**
9910 * Load WordPress core pluggable.php module.
9911 *
9912 * @author Vova Feldman (@svovaf)
9913 * @since 1.1.2
9914 */
9915 private static function require_pluggable_essentials() {
9916 if ( ! function_exists( 'wp_get_current_user' ) ) {
9917 require_once ABSPATH . 'wp-includes/pluggable.php';
9918 }
9919 }
9920
9921 /**
9922 * Return plugin data.
9923 *
9924 * @author Vova Feldman (@svovaf)
9925 * @since 1.0.1
9926 *
9927 * @param bool $reparse_plugin_metadata
9928 *
9929 * @return array
9930 */
9931 function get_plugin_data( $reparse_plugin_metadata = false ) {
9932 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
9933 self::require_plugin_essentials();
9934
9935 if ( $this->is_plugin() ) {
9936 /**
9937 * @author Vova Feldman (@svovaf)
9938 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
9939 *
9940 * @link https://github.com/Freemius/wordpress-sdk/issues/77
9941 */
9942 $plugin_data = get_plugin_data(
9943 $this->_plugin_main_file_path,
9944 false,
9945 false
9946 );
9947 } else {
9948 $theme_data = wp_get_theme();
9949
9950 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
9951 $parent_theme = $theme_data->parent();
9952
9953 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
9954 $theme_data = $parent_theme;
9955 }
9956 }
9957
9958 $plugin_data = array(
9959 'Name' => $theme_data->get( 'Name' ),
9960 'Version' => $theme_data->get( 'Version' ),
9961 'Author' => $theme_data->get( 'Author' ),
9962 'Description' => $theme_data->get( 'Description' ),
9963 'PluginURI' => $theme_data->get( 'ThemeURI' ),
9964 );
9965 }
9966
9967 $this->_plugin_data = $plugin_data;
9968 }
9969
9970 return $this->_plugin_data;
9971 }
9972
9973 /**
9974 * @author Vova Feldman (@svovaf)
9975 * @since 1.0.1
9976 * @since 1.2.2.5 If slug not set load slug by module ID.
9977 *
9978 * @return string Plugin slug.
9979 */
9980 function get_slug() {
9981 if ( ! isset( $this->_slug ) ) {
9982 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
9983 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
9984 }
9985
9986 return $this->_slug;
9987 }
9988
9989 /**
9990 * @author Leo Fajardo (@leorw)
9991 * @since 2.2.1
9992 *
9993 * @return string
9994 */
9995 function get_premium_slug() {
9996 return ( is_object( $this->_plugin ) && ! empty( $this->_plugin->premium_slug ) ) ?
9997 $this->_plugin->premium_slug :
9998 "{$this->_slug}-premium";
9999 }
10000
10001 /**
10002 * Retrieve the desired folder name for the product.
10003 *
10004 * @author Vova Feldman (@svovaf)
10005 * @since 1.2.1.7
10006 *
10007 * @return string Plugin slug.
10008 */
10009 function get_target_folder_name() {
10010 return $this->can_use_premium_code() ?
10011 $this->_plugin->premium_slug :
10012 $this->_slug;
10013 }
10014
10015 /**
10016 * @author Vova Feldman (@svovaf)
10017 * @since 1.0.1
10018 *
10019 * @return number Plugin ID.
10020 */
10021 function get_id() {
10022 return $this->_plugin->id;
10023 }
10024
10025 /**
10026 * @author Leo Fajardo (@leorw)
10027 * @since 2.2.4
10028 *
10029 * @return number|null Bundle ID.
10030 */
10031 function get_bundle_id() {
10032 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
10033 $this->_plugin->bundle_id :
10034 null;
10035 }
10036
10037 /**
10038 * @author Vova Feldman (@svovaf)
10039 * @since 2.3.1
10040 *
10041 * @return string|null Bundle public key.
10042 */
10043 function get_bundle_public_key() {
10044 return isset( $this->_plugin->bundle_public_key ) ?
10045 $this->_plugin->bundle_public_key :
10046 null;
10047 }
10048
10049 /**
10050 * Get whether the SDK has been initiated in the context of a Bundle.
10051 *
10052 * This will return true, if `bundle_id` is present in the SDK init parameters.
10053 *
10054 * ```php
10055 * $my_fs = fs_dynamic_init( array(
10056 * // ...
10057 * 'bundle_id' => 'XXXX', // Will return true since we have bundle id.
10058 * 'bundle_public_key' => 'pk_XXXX',
10059 * ) );
10060 * ```
10061 *
10062 * @author Swashata Ghosh (@swashata)
10063 * @since 2.5.0
10064 *
10065 * @return bool True if we are running in bundle context, false otherwise.
10066 */
10067 private function has_bundle_context() {
10068 return ! is_null( $this->get_bundle_id() );
10069 }
10070
10071 /**
10072 * @author Vova Feldman (@svovaf)
10073 * @since 1.2.1.5
10074 *
10075 * @return string Freemius SDK version
10076 */
10077 function get_sdk_version() {
10078 return $this->version;
10079 }
10080
10081 /**
10082 * @author Vova Feldman (@svovaf)
10083 * @since 1.2.1.5
10084 *
10085 * @return number Parent plugin ID (if parent exist).
10086 */
10087 function get_parent_id() {
10088 return $this->is_addon() ?
10089 $this->get_parent_instance()->get_id() :
10090 $this->_plugin->id;
10091 }
10092
10093 /**
10094 * @author Vova Feldman (@svovaf)
10095 * @since 2.3.1
10096 *
10097 * @return string
10098 */
10099 function get_usage_tracking_terms_url() {
10100 return $this->apply_filters(
10101 'usage_tracking_terms_url',
10102 "https://freemius.com/product/opt-in/{$this->_plugin->id}/{$this->_slug}/"
10103 );
10104 }
10105
10106 /**
10107 * @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.
10108 * @since 2.5.8
10109 *
10110 * @return string
10111 */
10112 function get_license_activation_terms_url() {
10113 return $this->apply_filters(
10114 'license_activation_terms_url',
10115 "https://freemius.com/product/license-activation/{$this->_plugin->id}/{$this->_slug}/"
10116 );
10117 }
10118
10119 /**
10120 * @author Vova Feldman (@svovaf)
10121 * @since 2.3.1
10122 *
10123 * @return string
10124 */
10125 function get_eula_url() {
10126 return $this->apply_filters(
10127 'eula_url',
10128 "https://freemius.com/product/{$this->_plugin->id}/{$this->_slug}/legal/eula/"
10129 );
10130 }
10131
10132 /**
10133 * @author Vova Feldman (@svovaf)
10134 * @since 1.0.1
10135 *
10136 * @return string Plugin public key.
10137 */
10138 function get_public_key() {
10139 return $this->_plugin->public_key;
10140 }
10141
10142 /**
10143 * Will be available only on sandbox mode.
10144 *
10145 * @author Vova Feldman (@svovaf)
10146 * @since 1.0.4
10147 *
10148 * @return mixed Plugin secret key.
10149 */
10150 function get_secret_key() {
10151 return $this->_plugin->secret_key;
10152 }
10153
10154 /**
10155 * @author Vova Feldman (@svovaf)
10156 * @since 1.1.1
10157 *
10158 * @return bool
10159 */
10160 function has_secret_key() {
10161 return ! empty( $this->_plugin->secret_key );
10162 }
10163
10164 /**
10165 * @author Vova Feldman (@svovaf)
10166 * @since 1.0.9
10167 *
10168 * @param string|bool $premium_suffix
10169 *
10170 * @return string
10171 */
10172 function get_plugin_name( $premium_suffix = false ) {
10173 $this->_logger->entrance();
10174
10175 /**
10176 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10177 *
10178 * @author Vova Feldman
10179 */
10180 if ( ! isset( $this->_plugin_name ) ) {
10181 // Name is not yet set.
10182 $this->set_name( $premium_suffix );
10183 } else if (
10184 ! empty( $premium_suffix ) &&
10185 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10186 ) {
10187 // Name is already set, but there's a change in the premium suffix.
10188 $this->set_name( $premium_suffix );
10189 }
10190
10191 return $this->_plugin_name;
10192 }
10193
10194 /**
10195 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10196 *
10197 * @author Vova Feldman (@svovaf)
10198 * @since 2.2.1
10199 *
10200 * @param string $premium_suffix
10201 */
10202 private function set_name( $premium_suffix = '' ) {
10203 $plugin_data = $this->get_plugin_data();
10204
10205 // Get name.
10206 $this->_plugin_name = $plugin_data['Name'];
10207
10208 if ( is_string( $premium_suffix ) ) {
10209 $premium_suffix = trim( $premium_suffix );
10210
10211 if ( ! empty( $premium_suffix ) ) {
10212 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10213 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10214 $suffix_len = strlen( $suffix );
10215
10216 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10217 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10218 ) {
10219 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10220 }
10221 }
10222 }
10223
10224 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10225 }
10226
10227 /**
10228 * @author Vova Feldman (@svovaf)
10229 * @since 1.0.0
10230 *
10231 * @param bool $reparse_plugin_metadata
10232 *
10233 * @return string
10234 */
10235 function get_plugin_version( $reparse_plugin_metadata = false ) {
10236 $this->_logger->entrance();
10237
10238 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10239
10240 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10241
10242 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10243 }
10244
10245 /**
10246 * @author Vova Feldman (@svovaf)
10247 * @since 1.2.1.7
10248 *
10249 * @return string
10250 */
10251 function get_plugin_title() {
10252 $this->_logger->entrance();
10253
10254 $title = $this->_plugin->title;
10255
10256 return $this->apply_filters( 'plugin_title', $title );
10257 }
10258
10259 /**
10260 * @author Vova Feldman (@svovaf)
10261 * @since 1.2.2.7
10262 *
10263 * @param bool $lowercase
10264 *
10265 * @return string
10266 */
10267 function get_module_label( $lowercase = false ) {
10268 $label = $this->is_addon() ?
10269 $this->get_text_inline( 'Add-On', 'addon' ) :
10270 ( $this->is_plugin() ?
10271 $this->get_text_inline( 'Plugin', 'plugin' ) :
10272 $this->get_text_inline( 'Theme', 'theme' ) );
10273
10274 if ( $lowercase ) {
10275 $label = strtolower( $label );
10276 }
10277
10278 return $label;
10279 }
10280
10281 /**
10282 * @author Vova Feldman (@svovaf)
10283 * @since 1.0.4
10284 *
10285 * @return string
10286 */
10287 function get_plugin_basename() {
10288 if ( ! isset( $this->_plugin_basename ) ) {
10289 if ( $this->is_plugin() ) {
10290 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10291 } else {
10292 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10293 }
10294 }
10295
10296 return $this->_plugin_basename;
10297 }
10298
10299 function get_plugin_folder_name() {
10300 $this->_logger->entrance();
10301
10302 $plugin_folder = $this->_plugin_basename;
10303
10304 while ( '.' !== dirname( $plugin_folder ) ) {
10305 $plugin_folder = dirname( $plugin_folder );
10306 }
10307
10308 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10309
10310 return $plugin_folder;
10311 }
10312
10313 #endregion ------------------------------------------------------------------
10314
10315 /* Account
10316 ------------------------------------------------------------------------------------------------------------------*/
10317
10318 /**
10319 * Find plugin's slug by plugin's basename.
10320 *
10321 * @author Vova Feldman (@svovaf)
10322 * @since 1.0.9
10323 *
10324 * @param string $plugin_base_name
10325 *
10326 * @return false|string
10327 */
10328 private static function find_slug_by_basename( $plugin_base_name ) {
10329 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10330
10331 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10332 return false;
10333 }
10334
10335 return $file_slug_map[ $plugin_base_name ];
10336 }
10337
10338 /**
10339 * Store the map between the plugin's basename to the slug.
10340 *
10341 * @author Vova Feldman (@svovaf)
10342 * @since 1.0.9
10343 */
10344 private function store_file_slug_map() {
10345 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10346
10347 if ( ! array( $file_slug_map ) ) {
10348 $file_slug_map = array();
10349 }
10350
10351 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10352 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10353 ) {
10354 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10355 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10356 }
10357 }
10358
10359 /**
10360 * @return array[number]FS_User
10361 */
10362 static function get_all_users() {
10363 $users = self::maybe_get_entities_account_option( 'users', array() );
10364
10365 if ( ! is_array( $users ) ) {
10366 $users = array();
10367 }
10368
10369 return $users;
10370 }
10371
10372 /**
10373 * @param string $module_type
10374 * @param null|int $blog_id Since 2.0.0
10375 *
10376 * @return array[string]FS_Site
10377 */
10378 public static function get_all_sites(
10379 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10380 $blog_id = null,
10381 $is_backup = false
10382 ) {
10383 $sites = self::get_account_option(
10384 ( $is_backup ? 'prev_' : '' ) . 'sites',
10385 $module_type,
10386 $blog_id
10387 );
10388
10389 if ( ! is_array( $sites ) ) {
10390 $sites = array();
10391 }
10392
10393 return $sites;
10394 }
10395
10396 /**
10397 * @author Leo Fajardo (@leorw)
10398 *
10399 * @since 1.2.2
10400 *
10401 * @param string $option_name
10402 * @param string $module_type
10403 * @param null|int $network_level_or_blog_id Since 2.0.0
10404 *
10405 * @return mixed
10406 */
10407 public static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10408 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10409 $option_name = $module_type . '_' . $option_name;
10410 }
10411
10412 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10413 }
10414
10415 /**
10416 * @author Leo Fajardo (@leorw)
10417 *
10418 * @since 1.2.2
10419 *
10420 * @param string $option_name
10421 * @param mixed $option_value
10422 * @param bool $store
10423 * @param null|int $network_level_or_blog_id Since 2.0.0
10424 */
10425 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10426 self::set_account_option_by_module(
10427 $this->_module_type,
10428 $option_name,
10429 $option_value,
10430 $store,
10431 $network_level_or_blog_id
10432 );
10433 }
10434
10435 /**
10436 * @author Vova Feldman (@svovaf)
10437 *
10438 * @since 1.2.2.7
10439 *
10440 * @param string $module_type
10441 * @param string $option_name
10442 * @param mixed $option_value
10443 * @param bool $store
10444 * @param null|int $network_level_or_blog_id Since 2.0.0
10445 */
10446 private static function set_account_option_by_module(
10447 $module_type,
10448 $option_name,
10449 $option_value,
10450 $store,
10451 $network_level_or_blog_id = null
10452 ) {
10453 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10454 $option_name = $module_type . '_' . $option_name;
10455 }
10456
10457 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10458 }
10459
10460 /**
10461 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10462 *
10463 * @author Leo Fajardo (@leorw)
10464 * @since 2.3.1
10465 *
10466 * @param string $option_name
10467 * @param mixed $default
10468 * @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).
10469 *
10470 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10471 */
10472 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10473 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10474
10475 $class_name = '';
10476
10477 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10478 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10479 }
10480
10481 switch ( $option_name ) {
10482 case 'plugins':
10483 case 'themes':
10484 case 'addons':
10485 $class_name = FS_Plugin::get_class_name();
10486 break;
10487 case 'users':
10488 $class_name = FS_User::get_class_name();
10489 break;
10490 case 'sites':
10491 $class_name = FS_Site::get_class_name();
10492 break;
10493 case 'licenses':
10494 case 'all_licenses':
10495 $class_name = FS_Plugin_License::get_class_name();
10496 break;
10497 case 'plans':
10498 $class_name = FS_Plugin_Plan::get_class_name();
10499 break;
10500 case 'updates':
10501 $class_name = FS_Plugin_Tag::get_class_name();
10502 break;
10503 }
10504
10505 if ( empty( $class_name ) ) {
10506 return $option;
10507 }
10508
10509 return fs_get_entities( $option, $class_name );
10510 }
10511
10512 /**
10513 * @author Vova Feldman (@svovaf)
10514 * @since 1.0.6
10515 *
10516 * @param number|null $module_id
10517 *
10518 * @return FS_Plugin_License[]
10519 */
10520 private static function get_all_licenses( $module_id = null ) {
10521 $licenses = self::get_account_option( 'all_licenses' );
10522
10523 if ( ! is_array( $licenses ) ) {
10524 $licenses = array();
10525 }
10526
10527 if ( is_null( $module_id ) ) {
10528 return $licenses;
10529 }
10530
10531 $licenses = isset( $licenses[ $module_id ] ) ?
10532 $licenses[ $module_id ] :
10533 array();
10534
10535 return $licenses;
10536 }
10537
10538 /**
10539 * @author Leo Fajardo (@leorw)
10540 * @since 2.0.0
10541 *
10542 * @param number $module_id
10543 * @param number|null $user_id
10544 *
10545 * @return array
10546 */
10547 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10548 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10549
10550 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10551 $all_modules_user_id_license_ids_map = array();
10552 }
10553
10554 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10555 $all_modules_user_id_license_ids_map[ $module_id ] :
10556 array();
10557
10558 if ( FS_User::is_valid_id( $user_id ) ) {
10559 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10560 $user_id_license_ids_map[ $user_id ] :
10561 array();
10562 }
10563
10564 return $user_id_license_ids_map;
10565 }
10566
10567 /**
10568 * @author Leo Fajardo (@leorw)
10569 * @since 2.0.0
10570 *
10571 * @param array $new_user_id_license_ids_map
10572 * @param number $module_id
10573 * @param number|null $user_id
10574 */
10575 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10576 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10577 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10578 $all_modules_user_id_license_ids_map = array();
10579 }
10580
10581 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10582 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10583 }
10584
10585 if ( FS_User::is_valid_id( $user_id ) ) {
10586 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10587 } else {
10588 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10589 }
10590
10591 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10592 }
10593
10594 /**
10595 * Get a collection of the user's linked license IDs.
10596 *
10597 * @author Vova Feldman (@svovaf)
10598 * @since 2.0.0
10599 *
10600 * @param number $user_id
10601 *
10602 * @return number[]
10603 */
10604 private function get_user_linked_license_ids( $user_id ) {
10605 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10606 }
10607
10608 /**
10609 * Override the user's linked license IDs with a new IDs collection.
10610 *
10611 * @author Vova Feldman (@svovaf)
10612 * @since 2.0.0
10613 *
10614 * @param number $user_id
10615 * @param number[] $license_ids
10616 */
10617 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10618 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10619 }
10620
10621 /**
10622 * Link a specified license ID to a given user.
10623 *
10624 * @author Vova Feldman (@svovaf)
10625 * @since 2.0.0
10626 *
10627 * @param number $license_id
10628 * @param number $user_id
10629 */
10630 private function link_license_2_user( $license_id, $user_id ) {
10631 $license_ids = $this->get_user_linked_license_ids( $user_id );
10632
10633 if ( in_array( $license_id, $license_ids ) ) {
10634 // License already linked.
10635 return;
10636 }
10637
10638 $license_ids[] = $license_id;
10639
10640 $this->set_user_linked_license_ids( $user_id, $license_ids );
10641 }
10642
10643 /**
10644 * @param string|bool $module_type
10645 *
10646 * @return FS_Plugin_Plan[]
10647 */
10648 private static function get_all_plans( $module_type = false ) {
10649 $plans = self::get_account_option( 'plans', $module_type );
10650
10651 if ( ! is_array( $plans ) ) {
10652 $plans = array();
10653 }
10654
10655 return $plans;
10656 }
10657
10658 /**
10659 * @author Vova Feldman (@svovaf)
10660 * @since 1.0.4
10661 *
10662 * @return FS_Plugin_Tag[]
10663 */
10664 private static function get_all_updates() {
10665 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10666
10667 if ( ! is_array( $updates ) ) {
10668 $updates = array();
10669 }
10670
10671 return $updates;
10672 }
10673
10674 /**
10675 * @author Vova Feldman (@svovaf)
10676 * @since 1.0.6
10677 *
10678 * @return array<number,FS_Plugin[]>|false
10679 */
10680 public static function get_all_addons() {
10681 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10682
10683 if ( ! is_array( $addons ) ) {
10684 $addons = array();
10685 }
10686
10687 return $addons;
10688 }
10689
10690 /**
10691 * @author Vova Feldman (@svovaf)
10692 * @since 1.0.6
10693 *
10694 * @return number[]|false
10695 */
10696 public static function get_all_account_addons() {
10697 $addons = self::$_accounts->get_option( 'account_addons', array() );
10698
10699 if ( ! is_array( $addons ) ) {
10700 $addons = array();
10701 }
10702
10703 return $addons;
10704 }
10705
10706 /**
10707 * Check if user has connected his account (opted-in).
10708 *
10709 * Note:
10710 * If the user opted-in and opted-out on a later stage,
10711 * this will still return true. If you want to check if the
10712 * user is currently opted-in, use:
10713 * `$fs->is_registered() && $fs->is_tracking_allowed()`
10714 *
10715 * @author Vova Feldman (@svovaf)
10716 * @since 1.0.1
10717 *
10718 * @param bool $ignore_anonymous_state Since 2.5.1
10719 *
10720 * @return bool
10721 */
10722 function is_registered( $ignore_anonymous_state = false ) {
10723 return (
10724 is_object( $this->_user ) &&
10725 (
10726 $this->is_premium() ||
10727 $ignore_anonymous_state ||
10728 ! $this->is_anonymous()
10729 )
10730 );
10731 }
10732
10733 /**
10734 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
10735 *
10736 * @author Leo Fajardo (@leorw)
10737 * @since 1.2.1.5
10738 *
10739 * @return bool
10740 */
10741 function is_tracking_allowed( $blog_id = null, $install = null ) {
10742 if ( is_null( $install ) ) {
10743 $install = is_null( $blog_id ) ?
10744 $this->_site :
10745 $this->get_install_by_blog_id( $blog_id );
10746 }
10747
10748 return (
10749 is_object( $install ) &&
10750 FS_Permission_Manager::instance( $this )->is_homepage_url_tracking_allowed( $blog_id )
10751 );
10752 }
10753
10754 /**
10755 * Returns TRUE if the user never opted-in or manually opted-out.
10756 *
10757 * @author Vova Feldman (@svovaf)
10758 * @since 1.2.1.5
10759 *
10760 * @param int|null $blog_id
10761 *
10762 * @return bool
10763 */
10764 function is_tracking_prohibited( $blog_id = null ) {
10765 return (
10766 ! $this->is_registered( true ) ||
10767 ! $this->is_tracking_allowed( $blog_id )
10768 );
10769 }
10770
10771 /**
10772 * @author Leo Fajardo (@leorw)
10773 * @since 2.4.0
10774 *
10775 * @return bool
10776 */
10777 function is_bundle_license_auto_activation_enabled() {
10778 return $this->is_addon() ?
10779 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
10780 $this->_is_bundle_license_auto_activation_enabled;
10781 }
10782
10783 /**
10784 * @author Vova Feldman (@svovaf)
10785 * @since 1.0.4
10786 *
10787 * @return FS_Plugin
10788 */
10789 function get_plugin() {
10790 return $this->_plugin;
10791 }
10792
10793 /**
10794 * @author Vova Feldman (@svovaf)
10795 * @since 1.0.3
10796 *
10797 * @return FS_User
10798 */
10799 function get_user() {
10800 return $this->_user;
10801 }
10802
10803 /**
10804 * @author Vova Feldman (@svovaf)
10805 * @since 1.0.3
10806 *
10807 * @return FS_Site
10808 */
10809 function get_site() {
10810 return $this->_site;
10811 }
10812
10813 /**
10814 * @author Daniele Alessandra (@danielealessandra)
10815 * @return FS_Storage
10816 * @since 2.6.2
10817 *
10818 */
10819 public function get_storage() {
10820 return $this->_storage;
10821 }
10822
10823 /**
10824 * @author Leo Fajardo (@leorw)
10825 * @since 2.5.0
10826 */
10827 function store_site( $site ) {
10828 $this->_site = $site;
10829 $this->_store_site( true );
10830 }
10831
10832 /**
10833 * 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).
10834 *
10835 * @author Leo Fajardo (@leorw)
10836 * @since 2.5.0
10837 */
10838 function delete_current_install( $back_up ) {
10839 // Back up and delete the unique ID.
10840 if ( $back_up ) {
10841 self::$_accounts->set_option( 'prev_unique_id', $this->get_anonymous_id() );
10842 }
10843
10844 self::$_accounts->set_option( 'unique_id', null );
10845
10846 if ( $back_up ) {
10847 // 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).
10848 $this->back_up_site();
10849 }
10850
10851 $this->_delete_site();
10852 $this->_site = null;
10853 }
10854
10855 /**
10856 * @author Leo Fajardo (@leorw)
10857 * @since 2.5.0
10858 */
10859 function restore_backup_site() {
10860 self::$_accounts->set_option(
10861 'unique_id',
10862 self::$_accounts->get_option( 'prev_unique_id' )
10863 );
10864
10865 $sites = self::get_all_sites( $this->_module_type, null, true );
10866 $this->store_site( clone $sites[ $this->_slug ] );
10867 }
10868
10869 /**
10870 * Get plugin add-ons.
10871 *
10872 * @author Vova Feldman (@svovaf)
10873 * @since 1.0.6
10874 *
10875 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
10876 *
10877 * @param bool $flush
10878 *
10879 * @return FS_Plugin[]|false
10880 */
10881 function get_addons( $flush = false ) {
10882 $this->_logger->entrance();
10883
10884 if ( ! $this->_has_addons ) {
10885 return false;
10886 }
10887
10888 $addons = $this->sync_addons( $flush );
10889
10890 return ( ! is_array( $addons ) || empty( $addons ) ) ?
10891 false :
10892 $addons;
10893 }
10894
10895 /**
10896 * @author Vova Feldman (@svovaf)
10897 * @since 1.0.6
10898 *
10899 * @return number[]|false
10900 */
10901 function get_account_addons() {
10902 $this->_logger->entrance();
10903
10904 $addons = self::get_all_account_addons();
10905
10906 if ( ! is_array( $addons ) ||
10907 ! isset( $addons[ $this->_plugin->id ] ) ||
10908 ! is_array( $addons[ $this->_plugin->id ] ) ||
10909 0 === count( $addons[ $this->_plugin->id ] )
10910 ) {
10911 return false;
10912 }
10913
10914 return $addons[ $this->_plugin->id ];
10915 }
10916
10917 /**
10918 * Check if user has any
10919 *
10920 * @author Vova Feldman (@svovaf)
10921 * @since 1.1.6
10922 *
10923 * @return bool
10924 */
10925 function has_account_addons() {
10926 $addons = $this->get_account_addons();
10927
10928 return is_array( $addons ) && ( 0 < count( $addons ) );
10929 }
10930
10931
10932 /**
10933 * Get add-on by ID (from local data).
10934 *
10935 * @author Vova Feldman (@svovaf)
10936 * @since 1.0.6
10937 *
10938 * @param number $id
10939 *
10940 * @return FS_Plugin|false
10941 */
10942 function get_addon( $id ) {
10943 $this->_logger->entrance();
10944
10945 $addons = $this->get_addons();
10946
10947 if ( is_array( $addons ) ) {
10948 foreach ( $addons as $addon ) {
10949 if ( $id == $addon->id ) {
10950 return $addon;
10951 }
10952 }
10953 }
10954
10955 return false;
10956 }
10957
10958 /**
10959 * Get add-on by slug (from local data).
10960 *
10961 * @author Vova Feldman (@svovaf)
10962 * @since 1.0.6
10963 *
10964 * @param string $slug
10965 *
10966 * @param bool $flush
10967 *
10968 * @return FS_Plugin|false
10969 */
10970 function get_addon_by_slug( $slug, $flush = false ) {
10971 $this->_logger->entrance();
10972
10973 $addons = $this->get_addons( $flush );
10974
10975 if ( is_array( $addons ) ) {
10976 foreach ( $addons as $addon ) {
10977 if ( $slug === $addon->slug ) {
10978 return $addon;
10979 }
10980 }
10981 }
10982
10983 return false;
10984 }
10985
10986 /**
10987 * @var array<number,object[]> {
10988 * @key number Add-on ID.
10989 * @val object[] The add-on's plans and prices object.
10990 * }
10991 */
10992 private $plans_and_pricing_by_addon_id;
10993
10994 /**
10995 * @author Leo Fajardo (@leorw)
10996 * @since 2.3.0
10997 *
10998 * @return array<number,object[]> {
10999 * @key number Add-on ID.
11000 * @val object[] The add-on's plans and prices object.
11001 * }
11002 */
11003 function _get_addons_plans_and_pricing_map_by_id() {
11004 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
11005 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
11006
11007 $plans_and_pricing_by_addon_id = array();
11008 if ( $this->is_api_result_object( $result, 'addons' ) ) {
11009 foreach ( $result->addons as $addon ) {
11010 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
11011 }
11012 }
11013
11014 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
11015 }
11016
11017 return $this->plans_and_pricing_by_addon_id;
11018 }
11019
11020 /**
11021 * @author Leo Fajardo (@leorw)
11022 * @since 2.3.0
11023 *
11024 * @param number $addon_id
11025 * @param bool $is_installed
11026 *
11027 * @return array
11028 */
11029 function _get_addon_info( $addon_id, $is_installed ) {
11030 $addon = $this->get_addon( $addon_id );
11031
11032 if ( ! is_object( $addon ) ) {
11033 // Unexpected call.
11034 return array();
11035 }
11036
11037 $slug = $addon->slug;
11038
11039 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
11040
11041 if ( ! fs_is_network_admin() ) {
11042 // Get blog-level activated installations.
11043 $sites = self::maybe_get_entities_account_option( 'sites', array() );
11044 } else {
11045 $sites = null;
11046
11047 if ( $this->is_addon_activated( $addon_id ) &&
11048 $this->get_addon_instance( $addon_id )->is_network_active()
11049 ) {
11050 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
11051 // Get network-level activated installations.
11052 $sites = self::maybe_get_entities_account_option(
11053 'sites',
11054 array(),
11055 $addon_storage->network_install_blog_id
11056 );
11057 }
11058 }
11059 }
11060
11061 $addon_info = array(
11062 'is_connected' => false,
11063 'slug' => $slug,
11064 'title' => $addon->title,
11065 'is_whitelabeled' => $addon_storage->is_whitelabeled
11066 );
11067
11068 if ( ! $is_installed ) {
11069 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
11070
11071 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
11072 $has_paid_plan = false;
11073 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
11074
11075 if ( is_array( $plans ) && count( $plans ) > 0 ) {
11076 foreach ( $plans as $plan ) {
11077 if ( isset( $plan->pricing ) &&
11078 is_array( $plan->pricing ) &&
11079 count( $plan->pricing ) > 0
11080 ) {
11081 $has_paid_plan = true;
11082 break;
11083 }
11084 }
11085 }
11086
11087 $addon_info['has_paid_plan'] = $has_paid_plan;
11088 }
11089 }
11090
11091 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
11092 return $addon_info;
11093 }
11094
11095 $site = $sites[ $slug ];
11096
11097 $addon_info['is_connected'] = (
11098 ( $addon->parent_plugin_id == $this->get_id() ) &&
11099 is_object( $site ) &&
11100 FS_Site::is_valid_id( $site->id ) &&
11101 FS_User::is_valid_id( $site->user_id ) &&
11102 FS_Plugin_Plan::is_valid_id( $site->plan_id )
11103 );
11104
11105 if ( $addon_info['is_connected'] && $is_installed ) {
11106 return $addon_info;
11107 }
11108
11109 $addon_info['site'] = $site;
11110
11111 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
11112 if ( isset( $plugins_data[ $slug ] ) ) {
11113 $plugin_data = $plugins_data[ $slug ];
11114
11115 $addon_info['version'] = $plugin_data->version;
11116 }
11117
11118 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
11119 if ( isset( $all_plans[ $slug ] ) ) {
11120 $plans = $all_plans[ $slug ];
11121
11122 foreach ( $plans as $plan ) {
11123 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
11124 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
11125 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
11126 break;
11127 }
11128 }
11129 }
11130
11131 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
11132 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
11133 foreach ( $licenses[ $addon_id ] as $license ) {
11134 if ( $license->id == $site->license_id ) {
11135 $addon_info['license'] = $license;
11136 break;
11137 }
11138 }
11139 }
11140
11141 if ( isset( $addon_info['license'] ) ) {
11142 if ( isset( $addon_storage->subscriptions ) &&
11143 ! empty( $addon_storage->subscriptions )
11144 ) {
11145 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11146
11147 foreach ( $addon_subscriptions as $subscription ) {
11148 if ( $subscription->license_id == $site->license_id ) {
11149 $addon_info['subscription'] = $subscription;
11150 break;
11151 }
11152 }
11153 }
11154 }
11155
11156 return $addon_info;
11157 }
11158
11159 /**
11160 * @author Vova Feldman (@svovaf)
11161 * @since 2.0.0
11162 *
11163 * @param number $user_id
11164 *
11165 * @return FS_User
11166 */
11167 static function _get_user_by_id( $user_id ) {
11168 self::$_static_logger->entrance( "user_id = {$user_id}" );
11169
11170 $users = self::get_all_users();
11171
11172 if ( is_array( $users ) ) {
11173 if ( isset( $users[ $user_id ] ) &&
11174 $users[ $user_id ] instanceof FS_User &&
11175 $user_id == $users[ $user_id ]->id
11176 ) {
11177 return $users[ $user_id ];
11178 }
11179
11180 // If user wasn't found by the key, iterate over all the users collection.
11181 foreach ( $users as $user ) {
11182 /**
11183 * @var FS_User $user
11184 */
11185 if ( $user_id == $user->id ) {
11186 return $user;
11187 }
11188 }
11189 }
11190
11191 return null;
11192 }
11193
11194 /**
11195 * Checks if a Freemius user_id is associated with a super-admin.
11196 *
11197 * @author Vova Feldman (@svovaf)
11198 * @since 2.0.0
11199 *
11200 * @param number $user_id
11201 *
11202 * @return bool
11203 */
11204 private static function is_super_admin( $user_id ) {
11205 $is_super_admin = false;
11206
11207 $user = self::_get_user_by_id( $user_id );
11208
11209 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11210 self::require_pluggable_essentials();
11211
11212 $wp_user = get_user_by( 'email', $user->email );
11213
11214 if ( $wp_user instanceof WP_User ) {
11215 $super_admins = get_super_admins();
11216 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11217 }
11218 }
11219
11220 return $is_super_admin;
11221 }
11222
11223 #----------------------------------------------------------------------------------
11224 #region Plans & Licensing
11225 #----------------------------------------------------------------------------------
11226
11227 /**
11228 * Check if running premium plugin code.
11229 *
11230 * @author Vova Feldman (@svovaf)
11231 * @since 1.0.5
11232 *
11233 * @return bool
11234 */
11235 function is_premium() {
11236 /**
11237 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11238 * `register_constructor_hooks` method.
11239 *
11240 * @author Leo Fajardo (@leorw)
11241 * @since 2.2.3
11242 */
11243 return is_object( $this->_plugin ) ?
11244 $this->_plugin->is_premium :
11245 false;
11246 }
11247
11248 /**
11249 * Get site's plan ID.
11250 *
11251 * @author Vova Feldman (@svovaf)
11252 * @since 1.0.2
11253 *
11254 * @return number
11255 */
11256 function get_plan_id() {
11257 return $this->_site->plan_id;
11258 }
11259
11260 /**
11261 * Get site's plan title.
11262 *
11263 * @author Vova Feldman (@svovaf)
11264 * @since 1.0.2
11265 *
11266 * @return string
11267 */
11268 function get_plan_title() {
11269 $plan = $this->get_plan();
11270
11271 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11272 }
11273
11274 /**
11275 * Get site's plan name.
11276 *
11277 * @author Vova Feldman (@svovaf)
11278 * @since 2.0.0
11279 *
11280 * @return string
11281 */
11282 function get_plan_name() {
11283 $plan = $this->get_plan();
11284
11285 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11286 }
11287
11288 /**
11289 * @author Vova Feldman (@svovaf)
11290 * @since 1.0.9
11291 *
11292 * @return FS_Plugin_Plan|false
11293 */
11294 function get_plan() {
11295 if ( ! is_object( $this->_site ) ) {
11296 return false;
11297 }
11298
11299 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11300 $this->_get_plan_by_id( $this->_site->plan_id ) :
11301 false;
11302 }
11303
11304 /**
11305 * @author Vova Feldman (@svovaf)
11306 * @since 1.0.3
11307 *
11308 * @return bool
11309 */
11310 function is_trial() {
11311 $this->_logger->entrance();
11312
11313 if ( ! $this->is_registered( true ) || ! is_object( $this->_site ) ) {
11314 return false;
11315 }
11316
11317 return $this->_site->is_trial();
11318 }
11319
11320 /**
11321 * Check if currently in a trial with payment method (credit card or paypal).
11322 *
11323 * @author Vova Feldman (@svovaf)
11324 * @since 1.1.7
11325 *
11326 * @return bool
11327 */
11328 function is_paid_trial() {
11329 $this->_logger->entrance();
11330
11331 if ( ! $this->is_trial() ) {
11332 return false;
11333 }
11334
11335 if ( ! $this->has_active_valid_license() ) {
11336 return false;
11337 }
11338
11339 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11340 return false;
11341 }
11342
11343 /**
11344 * @var FS_Subscription $subscription
11345 */
11346 $subscription = $this->_get_subscription( $this->_license->id );
11347
11348 return ( is_object( $subscription ) && $subscription->is_active() );
11349 }
11350
11351 /**
11352 * Check if trial already utilized.
11353 *
11354 * @since 1.0.9
11355 *
11356 * @return bool
11357 */
11358 function is_trial_utilized() {
11359 $this->_logger->entrance();
11360
11361 if ( ! $this->is_registered() ) {
11362 return false;
11363 }
11364
11365 return $this->_site->is_trial_utilized();
11366 }
11367
11368 /**
11369 * Get trial plan information (if in trial).
11370 *
11371 * @author Vova Feldman (@svovaf)
11372 * @since 1.0.9
11373 *
11374 * @return bool|FS_Plugin_Plan
11375 */
11376 function get_trial_plan() {
11377 $this->_logger->entrance();
11378
11379 if ( ! $this->is_trial() ) {
11380 return false;
11381 }
11382
11383 // Try to load plan from local cache.
11384 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11385
11386 if ( ! is_object( $trial_plan ) ) {
11387 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11388
11389 /**
11390 * If managed to fetch the plan, add it to the plans collection.
11391 */
11392 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11393 if ( ! is_array( $this->_plans ) ) {
11394 $this->_plans = array();
11395 }
11396
11397 $this->_plans[] = $trial_plan;
11398 $this->_store_plans();
11399 }
11400 }
11401
11402 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11403 return $trial_plan;
11404 }
11405
11406 /**
11407 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11408 */
11409 $trial_plan = new FS_Plugin_Plan();
11410 $trial_plan->id = $this->_site->trial_plan_id;
11411 $trial_plan->name = 'pro';
11412 $trial_plan->title = 'Pro';
11413
11414 return $trial_plan;
11415 }
11416
11417 /**
11418 * Check if the user has an activate, non-expired license on current plugin's install.
11419 *
11420 * @since 1.0.9
11421 *
11422 * @return bool
11423 */
11424 function is_paying() {
11425 $this->_logger->entrance();
11426
11427 if ( ! $this->is_registered( true ) ) {
11428 return false;
11429 }
11430
11431 if ( ! $this->has_paid_plan() ) {
11432 return false;
11433 }
11434
11435 return (
11436 ! $this->is_trial() &&
11437 'free' !== $this->get_plan_name() &&
11438 $this->has_active_valid_license()
11439 );
11440 }
11441
11442 /**
11443 * @author Vova Feldman (@svovaf)
11444 * @since 1.0.4
11445 *
11446 * @return bool
11447 */
11448 function is_free_plan() {
11449 if ( ! $this->is_registered() ) {
11450 return true;
11451 }
11452
11453 if ( ! $this->has_paid_plan() ) {
11454 return true;
11455 }
11456
11457 return (
11458 'free' === $this->get_plan_name() ||
11459 ! $this->has_features_enabled_license()
11460 );
11461 }
11462
11463 /**
11464 * @author Vova Feldman (@svovaf)
11465 * @since 1.0.5
11466 *
11467 * @return bool
11468 */
11469 function _has_premium_license() {
11470 $this->_logger->entrance();
11471
11472 $premium_license = $this->_get_available_premium_license();
11473
11474 return ( false !== $premium_license );
11475 }
11476
11477 /**
11478 * Check if user has any licenses associated with the plugin (including expired or blocking).
11479 *
11480 * @author Vova Feldman (@svovaf)
11481 * @since 1.1.7.3
11482 *
11483 * @param bool $including_foreign
11484 *
11485 * @return bool
11486 */
11487 function has_any_license( $including_foreign = true ) {
11488 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11489 return false;
11490 }
11491
11492 if ( $including_foreign ) {
11493 return true;
11494 }
11495
11496 foreach ( $this->_licenses as $license ) {
11497 if ( $this->_user->id == $license->user_id ) {
11498 return true;
11499 }
11500 }
11501
11502 return false;
11503 }
11504
11505 /**
11506 * @author Vova Feldman (@svovaf)
11507 * @since 1.0.5
11508 *
11509 * @param bool|null $is_localhost
11510 *
11511 * @return FS_Plugin_License|false
11512 */
11513 function _get_available_premium_license( $is_localhost = null ) {
11514 $this->_logger->entrance();
11515
11516 $licenses = $this->get_available_premium_licenses( $is_localhost );
11517 if ( ! empty( $licenses ) ) {
11518 return $licenses[0];
11519 }
11520
11521 return false;
11522 }
11523
11524 /**
11525 * @author Vova Feldman (@svovaf)
11526 * @since 1.0.5
11527 *
11528 * @param bool|null $is_localhost
11529 *
11530 * @return FS_Plugin_License[]
11531 */
11532 function get_available_premium_licenses( $is_localhost = null ) {
11533 $this->_logger->entrance();
11534
11535 $licenses = array();
11536 if ( ! $this->has_paid_plan() ) {
11537 return $licenses;
11538 }
11539
11540 if ( is_array( $this->_licenses ) ) {
11541 foreach ( $this->_licenses as $license ) {
11542 if ( ! $license->can_activate( $is_localhost ) ) {
11543 continue;
11544 }
11545
11546 $licenses[] = $license;
11547 }
11548 }
11549
11550 return $licenses;
11551 }
11552
11553 /**
11554 * Sync local plugin plans with remote server.
11555 *
11556 * 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).
11557 *
11558 * @author Vova Feldman (@svovaf)
11559 * @since 1.0.5
11560 *
11561 * @return FS_Plugin_Plan[]|object
11562 */
11563 function _sync_plans() {
11564 $plans = $this->_fetch_plugin_plans();
11565
11566 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11567 $plans_map = array();
11568 foreach ( $plans as $plan ) {
11569 $plans_map[ $plan->id ] = true;
11570 }
11571
11572 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11573
11574 foreach ( $plans_ids_to_keep as $plan_id ) {
11575 if ( isset( $plans_map[ $plan_id ] ) ) {
11576 continue;
11577 }
11578
11579 $missing_plan = self::_get_plan_by_id( $plan_id );
11580
11581 if ( is_object( $missing_plan ) ) {
11582 $plans[] = $missing_plan;
11583 }
11584 }
11585
11586 $this->_plans = $plans;
11587 $this->_store_plans();
11588 }
11589
11590 $this->do_action( 'after_plans_sync', $plans );
11591
11592 return $this->_plans;
11593 }
11594
11595 /**
11596 * Check if specified plan exists locally. If not, fetch it and store it.
11597 *
11598 * @author Vova Feldman (@svovaf)
11599 * @since 2.0.0
11600 *
11601 * @param number $plan_id
11602 *
11603 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11604 */
11605 private function sync_plan_if_not_exist( $plan_id ) {
11606 $plan = self::_get_plan_by_id( $plan_id );
11607
11608 if ( is_object( $plan ) ) {
11609 // Plan already exists.
11610 return $plan;
11611 }
11612
11613 $plan = $this->fetch_plan_by_id( $plan_id );
11614
11615 if ( $plan instanceof FS_Plugin_Plan ) {
11616 $this->_plans[] = $plan;
11617 $this->_store_plans();
11618
11619 return $plan;
11620 }
11621
11622 return $plan;
11623 }
11624
11625 /**
11626 * Check if specified license exists locally. If not, fetch it and store it.
11627 *
11628 * @author Vova Feldman (@svovaf)
11629 * @since 2.0.0
11630 *
11631 * @param number $license_id
11632 * @param string $license_key
11633 *
11634 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11635 */
11636 private function sync_license_if_not_exist( $license_id, $license_key ) {
11637 $license = $this->_get_license_by_id( $license_id );
11638
11639 if ( is_object( $license ) ) {
11640 // License already exists.
11641 return $license;
11642 }
11643
11644 $license = $this->fetch_license_by_key( $license_id, $license_key );
11645
11646 if ( $license instanceof FS_Plugin_License ) {
11647 $this->_licenses[] = $license;
11648
11649 $this->set_license( $license );
11650
11651 $this->_store_licenses();
11652
11653 return $license;
11654 }
11655
11656 return $license;
11657 }
11658
11659 /**
11660 * Get a collection of unique plan IDs that are associated with any installs in the network.
11661 *
11662 * @author Leo Fajardo (@leorw)
11663 * @since 2.0.0
11664 *
11665 * @return number[]
11666 */
11667 private function get_plans_ids_associated_with_installs() {
11668 if ( ! is_multisite() ) {
11669 if ( ! is_object( $this->_site ) ||
11670 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11671 ) {
11672 return array();
11673 }
11674
11675 return array( $this->_site->plan_id );
11676 }
11677
11678 $plan_ids = array();
11679 $sites = self::get_sites();
11680 foreach ( $sites as $site ) {
11681 $blog_id = self::get_site_blog_id( $site );
11682 $install = $this->get_install_by_blog_id( $blog_id );
11683
11684 if ( ! is_object( $install ) ||
11685 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11686 ) {
11687 continue;
11688 }
11689
11690 $plan_ids[ $install->plan_id ] = true;
11691 }
11692
11693 return array_keys( $plan_ids );
11694 }
11695
11696 /**
11697 * Get a collection of unique license IDs that are associated with any installs in the network.
11698 *
11699 * @author Leo Fajardo (@leorw)
11700 * @since 2.0.0
11701 *
11702 * @return number[]
11703 */
11704 private function get_license_ids_associated_with_installs() {
11705 if ( ! $this->_is_network_active ) {
11706 if ( ! is_object( $this->_site ) ||
11707 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11708 ) {
11709 return array();
11710 }
11711
11712 return array( $this->_site->license_id );
11713 }
11714
11715 $license_ids = array();
11716 $sites = self::get_sites();
11717 foreach ( $sites as $site ) {
11718 $blog_id = self::get_site_blog_id( $site );
11719 $install = $this->get_install_by_blog_id( $blog_id );
11720
11721 if ( ! is_object( $install ) ||
11722 ! FS_Plugin_License::is_valid_id( $install->license_id )
11723 ) {
11724 continue;
11725 }
11726
11727 $license_ids[ $install->license_id ] = true;
11728 }
11729
11730 return array_keys( $license_ids );
11731 }
11732
11733 /**
11734 * @author Vova Feldman (@svovaf)
11735 * @since 1.0.5
11736 *
11737 * @param number $id
11738 *
11739 * @return FS_Plugin_Plan|false
11740 */
11741 function _get_plan_by_id( $id ) {
11742 $this->_logger->entrance();
11743
11744 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11745 $this->_sync_plans();
11746 }
11747
11748 foreach ( $this->_plans as $plan ) {
11749 if ( $id == $plan->id ) {
11750 return $plan;
11751 }
11752 }
11753
11754 return false;
11755 }
11756
11757 /**
11758 * @author Vova Feldman (@svovaf)
11759 * @since 1.1.8.1
11760 *
11761 * @param string $name
11762 *
11763 * @return FS_Plugin_Plan|false
11764 */
11765 private function get_plan_by_name( $name ) {
11766 $this->_logger->entrance();
11767
11768 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11769 $this->_sync_plans();
11770 }
11771
11772 foreach ( $this->_plans as $plan ) {
11773 if ( $name == $plan->name ) {
11774 return $plan;
11775 }
11776 }
11777
11778 return false;
11779 }
11780
11781 /**
11782 * Sync local licenses with remote server.
11783 *
11784 * @author Vova Feldman (@svovaf)
11785 * @since 1.0.6
11786 *
11787 * @param number|bool $site_license_id
11788 * @param number|null $blog_id
11789 *
11790 * @return FS_Plugin_License[]|object
11791 */
11792 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
11793 $this->_logger->entrance();
11794
11795 $is_network_admin = fs_is_network_admin();
11796
11797 if ( $is_network_admin && is_null( $blog_id ) ) {
11798 $all_licenses = self::get_all_licenses( $this->_module_id );
11799 } else {
11800 $all_licenses = $this->get_user_licenses( $this->_user->id );
11801 }
11802
11803 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
11804
11805 $all_licenses_map = array();
11806 foreach ( $all_licenses as $license ) {
11807 $all_licenses_map[ $license->id ] = true;
11808 }
11809
11810 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
11811
11812 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
11813 $licenses_map = array();
11814 foreach ( $licenses as $license ) {
11815 $licenses_map[ $license->id ] = true;
11816 }
11817
11818 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
11819 // foreach ( $license_ids_to_keep as $license_id ) {
11820 // if ( isset( $licenses_map[ $license_id ] ) ) {
11821 // continue;
11822 // }
11823 //
11824 // $missing_license = self::_get_license_by_id( $license_id, false );
11825 // if ( is_object( $missing_license ) ) {
11826 // $licenses[] = $missing_license;
11827 // $licenses_map[ $missing_license->id ] = true;
11828 // }
11829 // }
11830
11831 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
11832
11833 foreach ( $user_license_ids as $key => $license_id ) {
11834 if ( ! isset( $licenses_map[ $license_id ] ) ) {
11835 // Remove access to licenses that no longer exist.
11836 unset( $user_license_ids[ $key ] );
11837 }
11838 }
11839
11840 if ( ! empty( $user_license_ids ) ) {
11841 foreach ( $licenses_map as $license_id => $value ) {
11842 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
11843 // Associate new licenses with the user who triggered the license syncing.
11844 $user_license_ids[] = $license_id;
11845 }
11846 }
11847
11848 $user_license_ids = array_unique( $user_license_ids );
11849 } else {
11850 $user_license_ids = array_keys( $licenses_map );
11851 }
11852
11853 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
11854 $user_licenses = array();
11855 foreach ( $licenses as $license ) {
11856 if ( ! in_array( $license->id, $user_license_ids ) ) {
11857 continue;
11858 }
11859
11860 $user_licenses[] = $license;
11861 }
11862
11863 $this->_licenses = $user_licenses;
11864 } else {
11865 $this->_licenses = $licenses;
11866 }
11867
11868 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
11869
11870 $this->_store_licenses( true, $this->_module_id, $licenses );
11871 }
11872
11873 // Update current license.
11874 if ( is_object( $this->_license ) ) {
11875 $license = $this->_get_license_by_id( $this->_license->id );
11876
11877 if ( is_object( $license ) ) {
11878 /**
11879 * `$license` can be `false` in case a user change action has just been completed and this method
11880 * has synced the `$this->_licenses` collection for the new user. In this case, the
11881 * `$this->_licenses` collection may have only the newly activated license that is associated with
11882 * the new user. `set_license` will eventually be called in the same request by the logic that
11883 * follows outside this method which will detect that the install's license has been updated, and
11884 * then `_update_site_license` will be called which in turn will call `set_license`.
11885 *
11886 * @author Leo Fajardo (@leorw)
11887 * @since 2.3.2
11888 */
11889 $this->set_license( $license );
11890 }
11891 }
11892
11893 return $this->_licenses;
11894 }
11895
11896 /**
11897 * @author Vova Feldman (@svovaf)
11898 * @since 1.0.5
11899 *
11900 * @param number $id
11901 * @param bool $sync_licenses
11902 *
11903 * @return FS_Plugin_License|false
11904 */
11905 function _get_license_by_id( $id, $sync_licenses = true ) {
11906 $this->_logger->entrance();
11907
11908 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
11909 return false;
11910 }
11911
11912 /**
11913 * When running from the network level admin and opted-in from the network,
11914 * check if the license exists in the network user licenses collection.
11915 *
11916 * @author Vova Feldman (@svovaf)
11917 * @since 2.0.0
11918 */
11919 if ( fs_is_network_admin() &&
11920 $this->is_network_registered() &&
11921 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
11922 ) {
11923 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
11924
11925 foreach ( $licenses as $license ) {
11926 if ( $id == $license->id ) {
11927 return $license;
11928 }
11929 }
11930 }
11931
11932 if ( ! $this->has_any_license() && $sync_licenses ) {
11933 $this->_sync_licenses( $id );
11934 }
11935
11936 if ( is_array( $this->_licenses ) ) {
11937 foreach ( $this->_licenses as $license ) {
11938 if ( $id == $license->id ) {
11939 return $license;
11940 }
11941 }
11942 }
11943
11944 return false;
11945 }
11946
11947 /**
11948 * 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.
11949 *
11950 * @author Vova Feldman (@svovaf)
11951 * @since 2.0.0
11952 *
11953 * @param number $id
11954 *
11955 * @return FS_Plugin_License
11956 */
11957 private function get_license_by_id( $id ) {
11958 $licenses = self::get_all_licenses( $this->_module_id );
11959
11960 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
11961 foreach ( $licenses as $license ) {
11962 if ( $id == $license->id ) {
11963 return $license;
11964 }
11965 }
11966 }
11967
11968 return null;
11969 }
11970
11971 /**
11972 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
11973 *
11974 * @author Vova Feldman (@svovaf)
11975 * @since 2.0.0
11976 *
11977 * @return \FS_Plugin_License|mixed
11978 */
11979 private function sync_site_license() {
11980 $api = $this->get_api_user_scope();
11981
11982 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
11983
11984 if ( ! $this->is_api_result_entity( $result ) ) {
11985 return $result;
11986 }
11987
11988 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
11989 $this->_store_licenses();
11990
11991 return $license;
11992 }
11993
11994 /**
11995 * Get all user's available licenses for the current module.
11996 *
11997 * @author Vova Feldman (@svovaf)
11998 * @since 2.0.0
11999 *
12000 * @param number $user_id
12001 *
12002 * @return FS_Plugin_License[]
12003 */
12004 private function get_user_licenses( $user_id ) {
12005 $all_licenses = self::get_all_licenses( $this->_module_id );
12006 if ( empty( $all_licenses ) ) {
12007 return array();
12008 }
12009
12010 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
12011 if ( empty( $user_license_ids ) ) {
12012 return array();
12013 }
12014
12015 $licenses = array();
12016 foreach ( $all_licenses as $license ) {
12017 if ( in_array( $license->id, $user_license_ids ) ) {
12018 $licenses[] = $license;
12019 }
12020 }
12021
12022 return $licenses;
12023 }
12024
12025 /**
12026 * Checks if the context license is network activated except on the given blog ID.
12027 *
12028 * @author Vova Feldman (@svovaf)
12029 * @since 2.0.0
12030 *
12031 * @param int $except_blog_id
12032 *
12033 * @return bool
12034 */
12035 private function is_license_network_active( $except_blog_id = 0 ) {
12036 $this->_logger->entrance();
12037
12038 if ( ! is_object( $this->_license ) ) {
12039 return false;
12040 }
12041
12042 $sites = self::get_sites();
12043
12044 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
12045 // There are more sites than the number of activations, so license cannot be network activated.
12046 return false;
12047 }
12048
12049 foreach ( $sites as $site ) {
12050 $blog_id = self::get_site_blog_id( $site );
12051
12052 if ( $except_blog_id == $blog_id ) {
12053 // Skip excluded blog.
12054 continue;
12055 }
12056
12057 $install = $this->get_install_by_blog_id( $blog_id );
12058
12059 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
12060 return false;
12061 }
12062 }
12063
12064 return true;
12065 }
12066
12067 /**
12068 * 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.
12069 *
12070 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
12071 *
12072 * @author Vova Feldman (@svovaf)
12073 * @since 2.0.0
12074 *
12075 * @param \FS_User $user
12076 * @param \FS_Plugin_License $license
12077 *
12078 * @return bool
12079 */
12080 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
12081 $this->_logger->entrance();
12082
12083 $result = $this->can_activate_license_on_network( $license );
12084
12085 if ( false === $result ) {
12086 return false;
12087 }
12088
12089 $installs_without_license = $result['installs'];
12090 if ( ! empty( $installs_without_license ) ) {
12091 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
12092 }
12093
12094 $disconnected_site_ids = $result['sites'];
12095 if ( ! empty( $disconnected_site_ids ) ) {
12096 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
12097 }
12098
12099 $this->link_license_2_user( $license->id, $user->id );
12100
12101 // Sync license after activations.
12102 $license->activated += $result['production_count'];
12103 $license->activated_local += $result['localhost_count'];
12104
12105 // $this->_store_licenses()
12106
12107 return true;
12108 }
12109
12110 /**
12111 * Checks if the given license can be activated on the whole network.
12112 *
12113 * @author Vova Feldman (@svovaf)
12114 * @since 2.0.0
12115 *
12116 * @param \FS_Plugin_License $license
12117 *
12118 * @return false|array {
12119 * @type array[int]FS_Site $installs Blog ID to install map.
12120 * @type int[] $sites Non-connected blog IDs.
12121 * @type int $production_count Production sites count.
12122 * @type int $localhost_count Production sites count.
12123 * }
12124 */
12125 private function can_activate_license_on_network( FS_Plugin_License $license ) {
12126 $sites = self::get_sites();
12127
12128 $production_count = 0;
12129 $localhost_count = 0;
12130
12131 $installs_without_license = array();
12132 $disconnected_site_ids = array();
12133
12134 foreach ( $sites as $site ) {
12135 $blog_id = self::get_site_blog_id( $site );
12136 $install = $this->get_install_by_blog_id( $blog_id );
12137
12138 if ( is_object( $install ) ) {
12139 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
12140 // License already activated on the install.
12141 continue;
12142 }
12143
12144 $url = $install->url;
12145
12146 $installs_without_license[ $blog_id ] = $install;
12147 } else {
12148 $url = is_object( $site ) ?
12149 $site->siteurl :
12150 self::get_unfiltered_site_url( $blog_id );
12151
12152 $disconnected_site_ids[] = $blog_id;
12153 }
12154
12155 if ( FS_Site::is_localhost_by_address( $url ) ) {
12156 $localhost_count ++;
12157 } else {
12158 $production_count ++;
12159 }
12160 }
12161
12162 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12163 return false;
12164 }
12165
12166 return array(
12167 'installs' => $installs_without_license,
12168 'sites' => $disconnected_site_ids,
12169 'production_count' => $production_count,
12170 'localhost_count' => $localhost_count,
12171 );
12172 }
12173
12174 /**
12175 * Activate a given license on a collection of installs.
12176 *
12177 * @author Vova Feldman (@svovaf)
12178 * @since 2.0.0
12179 *
12180 * @param \FS_User $user
12181 * @param string $license_key
12182 * @param array $blog_2_install_map {
12183 * @key int Blog ID.
12184 * @value FS_Site Blog's associated install.
12185 * }
12186 *
12187 * @return mixed|true
12188 */
12189 private function activate_license_on_many_installs(
12190 FS_User $user,
12191 $license_key,
12192 array $blog_2_install_map
12193 ) {
12194 $params = array(
12195 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12196 );
12197
12198 $install_2_blog_map = array();
12199 foreach ( $blog_2_install_map as $blog_id => $install ) {
12200 $params[] = array( 'id' => $install->id, 'url' => $install->url );
12201
12202 $install_2_blog_map[ $install->id ] = $blog_id;
12203 }
12204
12205 $result = $this->get_api_user_scope_by_user( $user )->call(
12206 "plugins/{$this->_plugin->id}/installs.json",
12207 'PUT',
12208 $params
12209 );
12210
12211 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12212 return $result;
12213 }
12214
12215 foreach ( $result->installs as $r_install ) {
12216 $install = new FS_Site( $r_install );
12217 $install->is_disconnected = false;
12218
12219 // Update install.
12220 $this->_store_site(
12221 true,
12222 $install_2_blog_map[ $r_install->id ],
12223 $install
12224 );
12225 }
12226
12227 return true;
12228 }
12229
12230 /**
12231 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12232 *
12233 * @author Vova Feldman (@svovaf)
12234 * @since 2.3.1
12235 *
12236 * @param \FS_User $user
12237 * @param string $license_key
12238 *
12239 * @return true|mixed True if successful, otherwise, the API result.
12240 */
12241 private function activate_license_on_site( FS_User $user, $license_key ) {
12242 return $this->activate_license_on_many_sites( $user, $license_key );
12243 }
12244
12245 /**
12246 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12247 *
12248 * @author Vova Feldman (@svovaf)
12249 * @since 2.0.0
12250 *
12251 * @param \FS_User $user
12252 * @param string $license_key
12253 * @param int[] $site_ids
12254 *
12255 * @return true|mixed True if successful, otherwise, the API result.
12256 */
12257 private function activate_license_on_many_sites(
12258 FS_User $user,
12259 $license_key,
12260 array $site_ids = array()
12261 ) {
12262 $sites = array();
12263 foreach ( $site_ids as $site_id ) {
12264 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12265 }
12266
12267 // Install the plugin.
12268 $result = $this->create_installs_with_user(
12269 $user,
12270 $license_key,
12271 false,
12272 $sites,
12273 false,
12274 true
12275 );
12276
12277 if ( ! $this->is_api_result_entity( $result ) &&
12278 ! $this->is_api_result_object( $result, 'installs' )
12279 ) {
12280 return $result;
12281 }
12282
12283 $installs = array();
12284
12285 if ( $this->is_api_result_entity( $result ) ) {
12286 $install = new FS_Site( $result );
12287
12288 $this->_user = $user;
12289
12290 $this->_store_site( true, null, $install );
12291
12292 $this->_site = $install;
12293
12294 $this->reset_anonymous_mode();
12295 } else {
12296 foreach ( $result->installs as $install ) {
12297 $installs[] = new FS_Site( $install );
12298 }
12299
12300 // Map site addresses to their blog IDs.
12301 $address_to_blog_map = $this->get_address_to_blog_map();
12302
12303 $first_blog_id = null;
12304
12305 foreach ( $installs as $install ) {
12306 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12307 $blog_id = $address_to_blog_map[ $address ];
12308
12309 $this->_store_site( true, $blog_id, $install );
12310
12311 $this->reset_anonymous_mode( $blog_id );
12312
12313 if ( is_null( $first_blog_id ) ) {
12314 $first_blog_id = $blog_id;
12315 }
12316 }
12317
12318 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12319 $this->_storage->network_install_blog_id = $first_blog_id;
12320 }
12321 }
12322
12323 return true;
12324 }
12325
12326 /**
12327 * Sync site's license with user licenses.
12328 *
12329 * @author Vova Feldman (@svovaf)
12330 * @since 1.0.6
12331 *
12332 * @param FS_Plugin_License|null $new_license
12333 *
12334 * @return FS_Plugin_License|null
12335 */
12336 function _update_site_license( $new_license ) {
12337 $this->_logger->entrance();
12338
12339 /**
12340 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12341 * accordingly so that it will also handle the case when an ownership change is done via license
12342 * activation.
12343 *
12344 * @author Leo Fajardo (@leorw)
12345 * @since 2.3.2
12346 */
12347 $this->set_license( $new_license );
12348
12349 if ( ! is_object( $new_license ) ) {
12350 $this->_site->license_id = null;
12351 $this->_sync_site_subscription( null );
12352
12353 return $this->_license;
12354 }
12355
12356 $this->_site->license_id = $this->_license->id;
12357
12358 if ( ! is_array( $this->_licenses ) ) {
12359 $this->_licenses = array();
12360 }
12361
12362 $is_license_found = false;
12363 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12364 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12365 $this->_licenses[ $i ] = $new_license;
12366
12367 $is_license_found = true;
12368 break;
12369 }
12370 }
12371
12372 // If new license just append.
12373 if ( ! $is_license_found ) {
12374 $this->_licenses[] = $new_license;
12375 }
12376
12377 $this->_sync_site_subscription( $new_license );
12378
12379 return $this->_license;
12380 }
12381
12382 /**
12383 * @author Vova Feldman (@svovaf)
12384 * @since 2.3.1
12385 *
12386 * @param \FS_Plugin_License $license
12387 */
12388 private function set_license( FS_Plugin_License $license = null ) {
12389 $this->_license = $license;
12390
12391 $this->maybe_update_whitelabel_flag( $license );
12392 }
12393
12394 /**
12395 * @author Leo Fajardo (@leorw)
12396 * @since 2.3.1
12397 *
12398 * @param FS_Plugin_License $license
12399 */
12400 private function maybe_update_whitelabel_flag( $license ) {
12401 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12402 $this->_storage->is_whitelabeled :
12403 false;
12404
12405 if ( is_object( $license ) ) {
12406 $license_user = self::_get_user_by_id( $license->user_id );
12407
12408 if ( ! is_object( $license_user ) ) {
12409 // If foreign license, do not update the `is_whitelabeled` flag.
12410 return;
12411 }
12412
12413 if ( $this->is_addon() ) {
12414 /**
12415 * Store the last license data to the parent's storage since it's needed only when showing the
12416 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12417 * iterate over the add-ons just to get the last license data.
12418 */
12419 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12420 } else {
12421 $this->store_last_activated_license_data( $license );
12422 }
12423
12424 if ( $license->is_whitelabeled ) {
12425 // Activated a developer license, data should be hidden.
12426 $is_whitelabeled = true;
12427 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12428 // The account owner activated a regular license key, no need to hide the data.
12429 $is_whitelabeled = false;
12430 }
12431 }
12432
12433 $this->_storage->is_whitelabeled = $is_whitelabeled;
12434
12435 // Reset the whitelabeled status after update.
12436 $this->is_whitelabeled = null;
12437 if ( $this->is_addon() ) {
12438 $parent_fs = $this->get_parent_instance();
12439
12440 if ( is_object( $parent_fs ) ) {
12441 $parent_fs->is_whitelabeled = null;
12442 }
12443 }
12444 }
12445
12446 /**
12447 * @author Leo Fajardo (@leorw)
12448 * @since 2.3.1
12449 *
12450 * @param FS_Plugin_License $license
12451 * @param FS_User $license_user
12452 */
12453 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12454 if ( ! is_object( $license_user ) ) {
12455 $this->_storage->last_license_key = md5( $license->secret_key );
12456 $this->_storage->last_license_user_id = null;
12457 } else {
12458 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12459 $this->_storage->last_license_user_id = $license_user->id;
12460 }
12461 }
12462
12463 /**
12464 * @author Leo Fajardo (@leorw)
12465 * @since 2.3.1
12466 *
12467 * @param bool $ignore_data_debug_mode
12468 *
12469 * @return bool
12470 */
12471 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12472 if ( true !== $this->_storage->is_whitelabeled ) {
12473 return false;
12474 } else if ( $ignore_data_debug_mode ) {
12475 return true;
12476 }
12477
12478 $fs = $this->is_addon() ?
12479 $this->get_parent_instance() :
12480 $this;
12481
12482 return ! $fs->is_data_debug_mode();
12483 }
12484
12485 /**
12486 * @author Leo Fajardo (@leorw)
12487 * @since 2.3.1
12488 *
12489 * @return number
12490 */
12491 function get_last_license_user_id() {
12492 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12493 $this->_storage->last_license_user_id :
12494 null;
12495 }
12496
12497 /**
12498 * @author Leo Fajardo (@leorw)
12499 * @since 2.3.1
12500 *
12501 * @param int $blog_id
12502 * @param bool $ignore_data_debug_mode
12503 *
12504 * @return bool
12505 */
12506 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12507 if ( ! is_null( $blog_id ) ) {
12508 $this->switch_to_blog( $blog_id );
12509 }
12510
12511 if ( ! is_null( $this->is_whitelabeled ) ) {
12512 $is_whitelabeled = $this->is_whitelabeled;
12513 } else {
12514 $is_whitelabeled = false;
12515
12516 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12517
12518 if ( ! $this->has_addons() ) {
12519 $is_whitelabeled = $is_whitelabeled_flag;
12520 } else if ( $is_whitelabeled_flag ) {
12521 $is_whitelabeled = true;
12522 } else {
12523 if ( $this->is_registered() || $this->is_premium() ) {
12524 $addon_ids = $this->get_updated_account_addons();
12525 } else {
12526 $addons = self::get_all_addons();
12527
12528 $plugin_addons = isset( $addons[ $this->_plugin->id ] ) ?
12529 $addons[ $this->_plugin->id ] :
12530 array();
12531
12532 $addon_ids = array();
12533 foreach ( $plugin_addons as $addon ) {
12534 $addon_ids[] = $addon->id;
12535 }
12536 }
12537
12538 $installed_addons = $this->get_installed_addons();
12539 foreach ( $installed_addons as $fs_addon ) {
12540 $addon_ids[] = $fs_addon->get_id();
12541 }
12542
12543 if ( ! empty( $addon_ids ) ) {
12544 $addon_ids = array_unique( $addon_ids );
12545
12546 $is_network_level = (
12547 fs_is_network_admin() &&
12548 $this->is_network_active()
12549 );
12550
12551 foreach ( $addon_ids as $addon_id ) {
12552 $addon = $this->get_addon( $addon_id );
12553
12554 if ( ! is_object( $addon ) ) {
12555 continue;
12556 }
12557
12558 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12559 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12560 self::get_addon_instance( $addon_id ) :
12561 null;
12562
12563 $was_addon_network_activated = false;
12564
12565 if ( is_object( $fs_addon ) ) {
12566 $was_addon_network_activated = $fs_addon->is_network_active();
12567 } else if ( $is_network_level ) {
12568 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12569 }
12570
12571 $network_delegated_connection = (
12572 $was_addon_network_activated &&
12573 $addon_storage->get( 'is_delegated_connection', false, true )
12574 );
12575
12576 if (
12577 $is_network_level &&
12578 ( ! $was_addon_network_activated || $network_delegated_connection )
12579 ) {
12580 $sites = self::get_sites();
12581
12582 /**
12583 * If in network admin area and the add-on was not network-activated or network-activated
12584 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12585 */
12586 foreach ( $sites as $site ) {
12587 $site_info = $this->get_site_info( $site );
12588
12589 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12590 $is_whitelabeled = true;
12591 break;
12592 }
12593 }
12594
12595 if ( $is_whitelabeled ) {
12596 break;
12597 }
12598 } else {
12599 /**
12600 * This will be executed when any of the following is met:
12601 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12602 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12603 * 3. Add-on was not network-activated and in site admin area.
12604 */
12605 if ( true === $addon_storage->is_whitelabeled ) {
12606 $is_whitelabeled = true;
12607 break;
12608 }
12609 }
12610 }
12611 }
12612 }
12613
12614 $this->is_whitelabeled = $is_whitelabeled;
12615
12616 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12617 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12618 }
12619
12620 if ( ! is_null( $blog_id ) ) {
12621 $this->restore_current_blog();
12622 }
12623 }
12624
12625 return (
12626 $is_whitelabeled &&
12627 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12628 );
12629 }
12630
12631 /**
12632 * Sync site's subscription.
12633 *
12634 * @author Vova Feldman (@svovaf)
12635 * @since 1.0.9
12636 *
12637 * @param FS_Plugin_License|null $license
12638 *
12639 * @return bool|\FS_Subscription
12640 */
12641 private function _sync_site_subscription( $license ) {
12642 if ( ! is_object( $license ) ) {
12643 $this->delete_unused_subscriptions();
12644
12645 return false;
12646 }
12647
12648 // Load subscription details if not lifetime.
12649 $subscription = $license->is_lifetime() ?
12650 false :
12651 $this->_fetch_site_license_subscription();
12652
12653 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12654 $this->store_subscription( $subscription );
12655 } else {
12656 $this->delete_unused_subscriptions();
12657 }
12658
12659 return $subscription;
12660 }
12661
12662 /**
12663 * @author Vova Feldman (@svovaf)
12664 * @since 1.0.6
12665 *
12666 * @return bool|\FS_Plugin_License
12667 */
12668 function _get_license() {
12669 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12670 return $this->_license;
12671 }
12672
12673 return $this->_get_available_premium_license();
12674 }
12675
12676 /**
12677 * @param number $license_id
12678 *
12679 * @return null|\FS_Subscription
12680 */
12681 function _get_subscription( $license_id ) {
12682 if ( ! isset( $this->_storage->subscriptions ) ||
12683 empty( $this->_storage->subscriptions )
12684 ) {
12685 return null;
12686 }
12687
12688 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12689 if ( $subscription->license_id == $license_id ) {
12690 return $subscription;
12691 }
12692 }
12693
12694 return null;
12695 }
12696
12697 /**
12698 * @author Leo Fajardo (@leorw)
12699 * @since 2.0.0
12700 *
12701 * @param FS_Subscription $subscription
12702 */
12703 function store_subscription( FS_Subscription $subscription ) {
12704 if ( ! isset( $this->_storage->subscriptions ) ) {
12705 $this->_storage->subscriptions = array();
12706 }
12707
12708 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12709 $this->_storage->subscriptions = array( $subscription );
12710
12711 return;
12712 }
12713
12714 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
12715
12716 $updated_subscription = false;
12717 foreach ( $subscriptions as $key => $existing_subscription ) {
12718 if ( $existing_subscription->id == $subscription->id ) {
12719 $subscriptions[ $key ] = $subscription;
12720 $updated_subscription = true;
12721 break;
12722 }
12723 }
12724
12725 if ( ! $updated_subscription ) {
12726 $subscriptions[] = $subscription;
12727 }
12728
12729 $this->_storage->subscriptions = $subscriptions;
12730 }
12731
12732 /**
12733 * @author Leo Fajardo (@leorw)
12734 * @since 2.0.0
12735 */
12736 function delete_unused_subscriptions() {
12737 if ( ! isset( $this->_storage->subscriptions ) ||
12738 empty( $this->_storage->subscriptions ) ||
12739 // Clean up only if there are already at least 3 subscriptions.
12740 ( count( $this->_storage->subscriptions ) < 3 )
12741 ) {
12742 return;
12743 }
12744
12745 if ( ! is_multisite() ) {
12746 // If not multisite, there should only be 1 subscription, so just clear the array.
12747 $this->_storage->subscriptions = array();
12748
12749 return;
12750 }
12751
12752 $subscriptions_to_keep_by_license_id_map = array();
12753 $sites = self::get_sites();
12754 foreach ( $sites as $site ) {
12755 $blog_id = self::get_site_blog_id( $site );
12756 $install = $this->get_install_by_blog_id( $blog_id );
12757
12758 if ( ! is_object( $install ) ||
12759 ! FS_Plugin_License::is_valid_id( $install->license_id )
12760 ) {
12761 continue;
12762 }
12763
12764 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
12765 }
12766
12767 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
12768 $this->_storage->subscriptions = array();
12769
12770 return;
12771 }
12772
12773 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
12774 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
12775 unset( $this->_storage->subscriptions[ $key ] );
12776 }
12777 }
12778 }
12779
12780 /**
12781 * @author Vova Feldman (@svovaf)
12782 * @since 1.0.2
12783 *
12784 * @param string $plan Plan name
12785 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12786 *
12787 * @return bool
12788 */
12789 function is_plan( $plan, $exact = false ) {
12790 $this->_logger->entrance();
12791
12792 if ( ! $this->is_registered() ) {
12793 return false;
12794 }
12795
12796 $plan = strtolower( $plan );
12797
12798 $current_plan_name = $this->get_plan_name();
12799
12800 if ( $current_plan_name === $plan ) {
12801 // Exact plan.
12802 return true;
12803 } else if ( $exact ) {
12804 // Required exact, but plans are different.
12805 return false;
12806 }
12807
12808 $current_plan_order = - 1;
12809 $required_plan_order = PHP_INT_MAX;
12810 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12811 if ( $plan === $this->_plans[ $i ]->name ) {
12812 $required_plan_order = $i;
12813 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
12814 $current_plan_order = $i;
12815 }
12816 }
12817
12818 return ( $current_plan_order > $required_plan_order );
12819 }
12820
12821 /**
12822 * Check if module has only one plan.
12823 *
12824 * @author Vova Feldman (@svovaf)
12825 * @since 1.2.1.7
12826 *
12827 * @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.
12828 *
12829 * @return bool
12830 */
12831 function is_single_plan( $double_check = false ) {
12832 $this->_logger->entrance();
12833
12834 if ( ! $this->is_registered() ||
12835 ! is_array( $this->_plans ) ||
12836 0 === count( $this->_plans )
12837 ) {
12838 return true;
12839 }
12840
12841 $has_free_plan = $this->has_free_plan();
12842
12843 if ( ! $has_free_plan && $double_check ) {
12844 foreach ( $this->_plans as $plan ) {
12845 if ( $plan->is_free() ) {
12846 $has_free_plan = true;
12847 break;
12848 }
12849 }
12850 }
12851
12852 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
12853 }
12854
12855 /**
12856 * Check if plan based on trial. If not in trial mode, should return false.
12857 *
12858 * @since 1.0.9
12859 *
12860 * @param string $plan Plan name
12861 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12862 *
12863 * @return bool
12864 */
12865 function is_trial_plan( $plan, $exact = false ) {
12866 $this->_logger->entrance();
12867
12868 if ( ! $this->is_registered() ) {
12869 return false;
12870 }
12871
12872 if ( ! $this->is_trial() ) {
12873 return false;
12874 }
12875
12876 $trial_plan = $this->get_trial_plan();
12877
12878 if ( $trial_plan->name === $plan ) {
12879 // Exact plan.
12880 return true;
12881 } else if ( $exact ) {
12882 // Required exact, but plans are different.
12883 return false;
12884 }
12885
12886 $current_plan_order = - 1;
12887 $required_plan_order = - 1;
12888 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12889 if ( $plan === $this->_plans[ $i ]->name ) {
12890 $required_plan_order = $i;
12891 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
12892 $current_plan_order = $i;
12893 }
12894 }
12895
12896 return ( $current_plan_order > $required_plan_order );
12897 }
12898
12899 /**
12900 * Check if plugin has any paid plans.
12901 *
12902 * @author Vova Feldman (@svovaf)
12903 * @since 1.0.7
12904 *
12905 * @return bool
12906 */
12907 function has_paid_plan() {
12908 return $this->_has_paid_plans ||
12909 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
12910 }
12911
12912 /**
12913 * Check if plugin has any plan with a trail.
12914 *
12915 * @author Vova Feldman (@svovaf)
12916 * @since 1.0.9
12917 *
12918 * @return bool
12919 */
12920 function has_trial_plan() {
12921 /**
12922 * @author Vova Feldman(@svovaf)
12923 * @since 1.2.1.5
12924 *
12925 * Allow setting a trial from the SDK without calling the API.
12926 * But, if the user did opt-in, continue using the real data from the API.
12927 */
12928 if ( $this->_trial_days >= 0 ) {
12929 return true;
12930 }
12931
12932 return $this->_storage->get( 'has_trial_plan', false );
12933 }
12934
12935 /**
12936 * Check if plugin has any free plan, or is it premium only.
12937 *
12938 * Note: If no plans configured, assume plugin is free.
12939 *
12940 * @author Vova Feldman (@svovaf)
12941 * @since 1.0.7
12942 *
12943 * @return bool
12944 */
12945 function has_free_plan() {
12946 return ! $this->is_only_premium();
12947 }
12948
12949 /**
12950 * Displays a license activation dialog box when the user clicks on the "Activate License"
12951 * or "Change License" link on the plugins
12952 * page.
12953 *
12954 * @author Leo Fajardo (@leorw)
12955 * @since 1.1.9
12956 */
12957 function _add_license_activation_dialog_box() {
12958 $vars = array(
12959 'id' => $this->_module_id,
12960 );
12961
12962 fs_require_template( 'forms/license-activation.php', $vars );
12963 fs_require_template( 'forms/resend-key.php', $vars );
12964 }
12965
12966 /**
12967 * Displays an email address update dialog box when the user clicks on the email address "Edit" button on the "Account" page.
12968 *
12969 * @author Leo Fajardo (@leorw)
12970 * @since 2.5.0
12971 */
12972 function _add_email_address_update_dialog_box() {
12973 $vars = array( 'id' => $this->_module_id );
12974
12975 fs_require_template( 'forms/email-address-update.php', $vars );
12976 }
12977
12978 /**
12979 * @author Leo Fajardo (@leorw)
12980 * @since 2.5.0
12981 */
12982 function _add_email_address_update_option() {
12983 if ( ! $this->should_handle_user_change() ) {
12984 return;
12985 }
12986
12987 // Add email address update AJAX handler.
12988 $this->add_ajax_action( 'update_email_address', array( &$this, '_email_address_update_ajax_handler' ) );
12989 }
12990
12991 /**
12992 * @author Leo Fajardo (@leorw)
12993 * @since 2.5.0
12994 */
12995 function _email_address_update_ajax_handler() {
12996 $this->check_ajax_referer( 'update_email_address' );
12997
12998 $new_email_address = fs_request_get( 'email_address' );
12999 $transfer_type = fs_request_get( 'transfer_type' );
13000
13001 $result = $this->update_email( $new_email_address );
13002
13003 if ( ! FS_Api::is_api_error( $result ) ) {
13004 self::shoot_ajax_success();
13005 }
13006
13007 $error = '';
13008
13009 if ( FS_Api::is_api_error_object( $result ) ) {
13010 switch ( $result->error->code ) {
13011 case 'user_exist':
13012 case 'account_verification_required':
13013 $error = array(
13014 'code' => 'change_ownership',
13015 'url' => $this->get_account_url( 'change_owner', array(
13016 'state' => 'init',
13017 'candidate_email' => $new_email_address,
13018 'transfer_type' => $transfer_type,
13019 ) ),
13020 );
13021
13022 break;
13023 }
13024 }
13025
13026 if ( empty( $error ) ) {
13027 $error = is_object( $result ) ?
13028 var_export( $result->error, true ) :
13029 $result;
13030 }
13031
13032 self::shoot_ajax_failure( $error );
13033 }
13034
13035 /**
13036 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
13037 *
13038 * @author Leo Fajardo (@leorw)
13039 * @since 2.3.2
13040 *
13041 * @return number[]
13042 */
13043 function get_installs_ids_with_foreign_licenses() {
13044 $installs = array();
13045
13046 if (
13047 is_object( $this->_license ) &&
13048 $this->_site->user_id != $this->_license->user_id
13049 ) {
13050 $installs[] = $this->_site->id;
13051 }
13052
13053 /**
13054 * Also try to get foreign licenses for the context product's add-ons.
13055 */
13056 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
13057
13058 foreach ( $installs_by_slug_map as $slug => $install_info ) {
13059 if ( $slug == $this->get_slug() ) {
13060 continue;
13061 }
13062
13063 $install = $install_info['install'];
13064 $license = $install_info['license'];
13065
13066 if (
13067 is_object( $license ) &&
13068 $install->user_id != $license->user_id
13069 ) {
13070 $installs[] = $install->id;
13071 }
13072 }
13073
13074 return $installs;
13075 }
13076
13077 /**
13078 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
13079 *
13080 * @author Leo Fajardo (@leorw)
13081 * @since 2.3.2
13082 *
13083 * @param number[] $install_ids
13084 */
13085 function _add_user_change_dialog_box( $install_ids ) {
13086 $vars = array(
13087 'id' => $this->_module_id,
13088 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
13089 );
13090
13091 fs_require_template( 'forms/user-change.php', $vars );
13092 }
13093
13094 /**
13095 * @author Leo Fajardo (@leorw)
13096 * @since 2.3.1
13097 */
13098 function _add_data_debug_mode_dialog_box() {
13099 $vars = array(
13100 'id' => $this->_module_id,
13101 );
13102
13103 fs_require_template( 'forms/data-debug-mode.php', $vars );
13104 }
13105
13106 /**
13107 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
13108 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
13109 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
13110 * is only activated on a single production site.
13111 *
13112 * @author Leo Fajardo (@leorw)
13113 * @since 2.2.1
13114 *
13115 * @param bool $is_license_deactivation
13116 *
13117 * @return array
13118 */
13119 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
13120 if ( fs_is_network_admin() ) {
13121 // Subscription cancellation dialog box is currently not supported for multisite networks.
13122 return array();
13123 }
13124
13125 if ( $this->is_whitelabeled() ) {
13126 return array();
13127 }
13128
13129 $license = $this->_get_license();
13130
13131 /**
13132 * 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.
13133 *
13134 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
13135 * @since 2.2.1
13136 */
13137 if ( ! is_object( $license ) ||
13138 $license->is_lifetime() ||
13139 ( ! $license->is_single_site() && $license->activated > 1 )
13140 ) {
13141 return array();
13142 }
13143
13144 /**
13145 * @var FS_Subscription $subscription
13146 */
13147 $subscription = $this->_get_subscription( $license->id );
13148 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
13149 return array();
13150 }
13151
13152 return array(
13153 'id' => $this->_module_id,
13154 'license' => $license,
13155 'has_trial' => $this->is_paid_trial(),
13156 'is_license_deactivation' => $is_license_deactivation,
13157 );
13158 }
13159
13160 /**
13161 * @author Leo Fajardo (@leorw)
13162 * @since 2.0.2
13163 */
13164 function _add_premium_version_upgrade_selection_dialog_box() {
13165 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
13166 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
13167 return;
13168 }
13169
13170 $vars = array(
13171 'id' => $this->_module_id,
13172 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
13173 $modules_update->response[ $this->_plugin_basename ]->new_version :
13174 $modules_update->response[ $this->_plugin_basename ]['new_version']
13175 );
13176
13177 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13178 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13179 }
13180
13181 /**
13182 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13183 * page.
13184 *
13185 * @author Leo Fajardo (@leorw)
13186 * @since 1.2.1.5
13187 */
13188 function _add_optout_dialog() {
13189 if ( $this->is_theme() ) {
13190 $vars = null;
13191 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13192 }
13193
13194 $vars = array( 'id' => $this->_module_id );
13195 fs_require_template( 'forms/optout.php', $vars );
13196 }
13197
13198 /**
13199 * Prepare page to include all required UI and logic for the license activation dialog.
13200 *
13201 * @author Vova Feldman (@svovaf)
13202 * @since 1.2.0
13203 */
13204 function _add_license_activation() {
13205 if ( $this->is_migration() ) {
13206 return;
13207 }
13208
13209 if ( ! $this->is_user_admin() ) {
13210 // Only admins can activate a license.
13211 return;
13212 }
13213
13214 if ( ! $this->has_paid_plan() ) {
13215 // Module doesn't have any paid plans.
13216 return;
13217 }
13218
13219 if (
13220 $this->has_premium_version() &&
13221 ! $this->is_premium() &&
13222 /**
13223 * Also handle the case when an upgrade was made using the free version.
13224 *
13225 * @author Leo Fajardo (@leorw)
13226 * @since 2.3.2
13227 */
13228 ! is_object( $this->_get_license() )
13229 ) {
13230 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13231 return;
13232 }
13233
13234 // Add license activation link and AJAX request handler.
13235 if ( self::is_plugins_page() ) {
13236 $is_network_admin = fs_is_network_admin();
13237
13238 if (
13239 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13240 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13241 ) {
13242 if (
13243 $this->is_premium() ||
13244 ( $this->has_paid_plan() && ! $this->has_premium_version() )
13245 ) {
13246 /**
13247 * @since 1.2.0 Add license action link only on plugins page.
13248 */
13249 $this->_add_license_action_link();
13250 }
13251 }
13252 }
13253
13254 // Add license activation AJAX callback.
13255 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13256
13257 // Add resend license AJAX callback.
13258 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13259 }
13260
13261 /**
13262 * Prepares page to include all required UI and logic for the "Change User" dialog.
13263 *
13264 * @author Leo Fajardo (@leorw)
13265 * @since 2.3.2
13266 */
13267 function _add_user_change_option() {
13268 if ( ! $this->should_handle_user_change() ) {
13269 return;
13270 }
13271
13272 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13273
13274 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13275 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13276 return;
13277 }
13278
13279 // Add user change AJAX handler.
13280 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13281 }
13282
13283 /**
13284 * @author Leo Fajardo (@leorw)
13285 * @since 2.3.2
13286 */
13287 function should_handle_user_change() {
13288 if ( ! $this->is_user_admin() ) {
13289 // Only admins can change user.
13290 return false;
13291 }
13292
13293 if ( $this->is_addon() ) {
13294 return false;
13295 }
13296
13297 if ( ! $this->is_registered() ) {
13298 return false;
13299 }
13300
13301 if (
13302 $this->is_network_active() &&
13303 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13304 ) {
13305 // Handle only on site-level "Account" section for now.
13306 return false;
13307 }
13308
13309 return true;
13310 }
13311
13312 /**
13313 * @author Leo Fajardo (@leorw)
13314 * @since 2.0.2
13315 */
13316 function _add_premium_version_upgrade_selection() {
13317 if ( ! $this->is_user_admin() ) {
13318 return;
13319 }
13320
13321 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13322 // This is relevant only to the free versions and premium versions without an active license.
13323 return;
13324 }
13325
13326 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13327 $this->_add_premium_version_upgrade_selection_action();
13328 }
13329 }
13330
13331 /**
13332 * @author Edgar Melkonyan
13333 * @since 2.4.1
13334 *
13335 * @throws Freemius_Exception
13336 */
13337 function _toggle_whitelabel_mode_ajax_handler() {
13338 $this->_logger->entrance();
13339
13340 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13341
13342 if ( ! $this->is_user_admin() ) {
13343 // Only for admins.
13344 self::shoot_ajax_failure();
13345 }
13346
13347 $license = $this->get_api_user_scope()->call(
13348 "/licenses/{$this->_site->license_id}.json",
13349 'put',
13350 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13351 );
13352
13353 if ( ! $this->is_api_result_entity( $license ) ) {
13354 self::shoot_ajax_failure(
13355 FS_Api::is_api_error_object( $license ) ?
13356 $license->error->message :
13357 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13358 );
13359 }
13360
13361 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13362 $this->_store_licenses();
13363
13364 $this->_sync_license();
13365
13366 if ( ! $license->is_whitelabeled ) {
13367 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13368 } else {
13369 $this->_admin_notices->add_sticky(
13370 sprintf(
13371 $this->get_text_inline(
13372 '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.',
13373 'license_whitelabeled'
13374 ),
13375 "<strong>{$this->get_plugin_title()}</strong>",
13376 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13377 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13378 ),
13379 'license_whitelabeled'
13380 );
13381 }
13382
13383 self::shoot_ajax_response( array( 'success' => true ) );
13384 }
13385
13386 /**
13387 * @author Leo Fajardo (@leorw)
13388 * @since 2.3.0
13389 */
13390 function _add_beta_mode_update_handler() {
13391 if ( ! $this->is_user_admin() ) {
13392 return;
13393 }
13394
13395 if ( ! $this->is_premium() ) {
13396 return;
13397 }
13398
13399 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13400 }
13401
13402 /**
13403 * @author Leo Fajardo (@leorw)
13404 * @since 2.3.0
13405 */
13406 function _set_beta_mode_ajax_handler() {
13407 $this->_logger->entrance();
13408
13409 $this->check_ajax_referer( 'set_beta_mode' );
13410
13411 if ( ! $this->is_user_admin() ) {
13412 // Only for admins.
13413 self::shoot_ajax_failure();
13414 }
13415
13416 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13417
13418 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13419 self::shoot_ajax_failure();
13420 }
13421
13422 $site = $this->api_site_call(
13423 '',
13424 'put',
13425 array(
13426 'is_beta' => ( 'true' == $is_beta ),
13427 'fields' => 'is_beta'
13428 )
13429 );
13430
13431 if ( ! $this->is_api_result_entity( $site ) ) {
13432 self::shoot_ajax_failure(
13433 FS_Api::is_api_error_object( $site ) ?
13434 $site->error->message :
13435 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13436 );
13437 }
13438
13439 $this->_site->is_beta = $site->is_beta;
13440 $this->_store_site();
13441
13442 self::shoot_ajax_response( array( 'success' => true ) );
13443 }
13444
13445 /**
13446 * License activation WP AJAX handler.
13447 *
13448 * @author Leo Fajardo (@leorw)
13449 * @since 1.1.9
13450 *
13451 * @uses Freemius::activate_license()
13452 */
13453 function _activate_license_ajax_action() {
13454 $this->_logger->entrance();
13455
13456 $this->check_ajax_referer( 'activate_license' );
13457
13458 $license_key = trim( fs_request_get_raw( 'license_key' ) );
13459
13460 if ( empty( $license_key ) ) {
13461 $license_id = trim( fs_request_get_raw( 'license_id' ) );
13462
13463 if ( FS_Plugin_License::is_valid_id( $license_id ) ) {
13464 $license = $this->_get_license_by_id( $license_id, false );
13465
13466 if ( is_object( $license ) ) {
13467 $license_key = $license->secret_key;
13468 }
13469 }
13470 }
13471
13472 if ( empty( $license_key ) ) {
13473 exit;
13474 }
13475
13476 $sites = fs_is_network_admin() ?
13477 fs_request_get( 'sites', array(), 'post' ) :
13478 array();
13479
13480 $result = $this->activate_license(
13481 $license_key,
13482 $sites,
13483 fs_request_get_bool( 'is_marketing_allowed', null ),
13484 fs_request_get( 'blog_id', null ),
13485 fs_request_get( 'module_id', null, 'post' ),
13486 fs_request_get( 'user_id', null ),
13487 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
13488 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null )
13489 );
13490
13491 if (
13492 $result['success'] &&
13493 $this->is_bundle_license_auto_activation_enabled()
13494 ) {
13495 $license = new FS_Plugin_License();
13496 $license->secret_key = $license_key;
13497
13498 $this->maybe_activate_bundle_license( $license, $sites );
13499 }
13500
13501 echo json_encode( $result );
13502
13503 exit;
13504 }
13505
13506 /**
13507 * User change WP AJAX handler.
13508 *
13509 * @author Leo Fajardo (@leorw)
13510 * @since 2.3.2
13511 */
13512 function _user_change_ajax_action() {
13513 $this->_logger->entrance();
13514
13515 $this->check_ajax_referer( 'change_user' );
13516
13517 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13518 $new_user_id = fs_request_get( 'user_id' );
13519
13520 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13521 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13522 }
13523
13524 $params = array();
13525
13526 if ( ! empty( $new_email_address ) ) {
13527 $params['user_email'] = $new_email_address;
13528 } else {
13529 $params['user_id'] = $new_user_id;
13530 }
13531
13532 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13533 $install_ids = array();
13534
13535 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13536 $install_ids[ $slug ] = $install_info['install']->id;
13537 }
13538
13539 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13540
13541 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13542
13543 if ( FS_Api::is_api_error( $install ) ) {
13544 $error = '';
13545
13546 if ( is_object( $install ) ) {
13547 switch ( $install->error->code ) {
13548 case 'user_exist':
13549 $error = (
13550 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13551 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
13552 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>' ) .
13553 sprintf(
13554 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13555 $this->get_account_url( 'change_owner', array(
13556 'state' => 'init',
13557 'candidate_email' => $new_email_address
13558 ) ),
13559 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
13560 )
13561 );
13562 break;
13563 }
13564 }
13565
13566 if ( empty( $error ) ) {
13567 $error = FS_Api::is_api_error_object( $install ) ?
13568 $install->error->message :
13569 var_export( $install->error, true );
13570 }
13571
13572 self::shoot_ajax_failure( $error );
13573 } else {
13574 if (
13575 // If successful ownership change.
13576 $this->get_user()->id != $install->user_id ||
13577 ! empty( $new_email_address )
13578 ) {
13579 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
13580 }
13581 }
13582
13583 self::shoot_ajax_success();
13584 }
13585
13586 /**
13587 * @author Leo Fajardo (@leorw)
13588 * @since 2.3.2.14
13589 */
13590 function starting_migration() {
13591 if ( ! empty( $this->_storage->license_migration ) ) {
13592 // Do not overwrite the data if already set.
13593 return;
13594 }
13595
13596 $this->_storage->license_migration = array(
13597 'is_migrating' => true,
13598 'start_timestamp' => time()
13599 );
13600 }
13601
13602 /**
13603 * @author Leo Fajardo (@leorw)
13604 * @since 2.3.2.14
13605 */
13606 function is_migration() {
13607 if ( $this->is_addon() ) {
13608 return $this->get_parent_instance()->is_migration();
13609 }
13610
13611 if ( empty( $this->_storage->license_migration ) ) {
13612 return false;
13613 }
13614
13615 if ( ! $this->_storage->license_migration['is_migrating'] ) {
13616 return false;
13617 }
13618
13619 return (
13620 // Return `true` if the migration is within 5 minutes from the starting time.
13621 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
13622 );
13623 }
13624
13625 /**
13626 *
13627 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
13628 *
13629 * @author Vova Feldman (@svovaf)
13630 * @since 2.3.0
13631 *
13632 * @param string $license_key
13633 * @param null|bool $is_marketing_allowed
13634 * @param null|number $plugin_id
13635 * @param array $sites
13636 * @param int $blog_id
13637 *
13638 * @return array {
13639 * @var bool $success
13640 * @var string $error
13641 * @var string $next_page
13642 * }
13643 *
13644 * @uses Freemius::activate_license()
13645 */
13646 function activate_migrated_license(
13647 $license_key,
13648 $is_marketing_allowed = null,
13649 $plugin_id = null,
13650 $sites = array(),
13651 $blog_id = null
13652 ) {
13653 $this->_logger->entrance();
13654
13655 $result = $this->activate_license(
13656 $license_key,
13657 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
13658 $this->get_sites_for_network_level_optin() :
13659 $sites,
13660 $is_marketing_allowed,
13661 $blog_id,
13662 $plugin_id
13663 );
13664
13665 // No need to show the sticky after license activation notice after migrating a license.
13666 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13667
13668 return $result;
13669 }
13670
13671 /**
13672 * @author Leo Fajardo (@leorw)
13673 * @since 2.3.1
13674 *
13675 * @return string
13676 */
13677 function get_pricing_js_path() {
13678 if ( ! isset( $this->_pricing_js_path ) ) {
13679 $default_path = WP_FS__DIR_JS . '/pricing/freemius-pricing.js';
13680
13681 $pricing_js_path = $this->apply_filters( 'freemius_pricing_js_path', $default_path );
13682
13683 // Backward compatibility for people who placed the freemius-pricing inside `includes` directory. Let it take more preference than the default path.
13684 if ( empty( $pricing_js_path ) ) {
13685 global $fs_active_plugins;
13686
13687 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
13688 if ( $data->plugin_path == $this->get_plugin_basename() ) {
13689 $plugin_or_theme_root_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
13690
13691 $pricing_js_path = $plugin_or_theme_root_dir
13692 . '/'
13693 // The basename will be `plugins`, `themes`, or the basename of a custom plugins or themes directory.
13694 . str_replace( '../' . basename( $plugin_or_theme_root_dir ) . '/', '', $sdk_path )
13695 . '/includes/freemius-pricing/freemius-pricing.js';
13696
13697 break;
13698 }
13699 }
13700 }
13701
13702 // If it is still empty, load the default pricing JS.
13703 if ( ! file_exists( $pricing_js_path ) ) {
13704 $pricing_js_path = $default_path;
13705 }
13706
13707 $this->_pricing_js_path = $pricing_js_path;
13708 }
13709
13710 return $this->_pricing_js_path;
13711 }
13712
13713 /**
13714 * @author Leo Fajardo (@leorw)
13715 * @since 2.3.1
13716 *
13717 * @deprecated Since v2.9.0 we have removed the iFrame based pricing. This will always return `false`.
13718 *
13719 * @return bool
13720 */
13721 function should_use_external_pricing() {
13722 return false;
13723 }
13724
13725 /**
13726 * The implementation of this method was previously in `_activate_license_ajax_action()`.
13727 *
13728 * @author Vova Feldman (@svovaf)
13729 * @since 2.2.4
13730 * @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).
13731 * @param string $license_key
13732 * @param array $sites
13733 * @param null|bool $is_marketing_allowed
13734 * @param null|int $blog_id
13735 * @param null|number $plugin_id
13736 * @param null|number $license_owner_id
13737 * @param bool|null $is_extensions_tracking_allowed
13738 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint.
13739 *
13740 *
13741 * @return array {
13742 * @var bool $success
13743 * @var string $error
13744 * @var string $next_page
13745 * }
13746 */
13747 private function activate_license(
13748 $license_key,
13749 $sites = array(),
13750 $is_marketing_allowed = null,
13751 $blog_id = null,
13752 $plugin_id = null,
13753 $license_owner_id = null,
13754 $is_extensions_tracking_allowed = null,
13755 $is_diagnostic_tracking_allowed = null
13756 ) {
13757 $this->_logger->entrance();
13758
13759 $license_key = trim( $license_key );
13760
13761 $is_network_activation_or_migration = (
13762 fs_is_network_admin() ||
13763 ( ! empty( $sites ) && $this->is_migration() )
13764 );
13765
13766 if ( ! $is_network_activation_or_migration ) {
13767 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
13768 $sites = array();
13769 }
13770
13771 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
13772 $this :
13773 $this->get_addon_instance( $plugin_id );
13774
13775 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
13776 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
13777 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
13778 ) );
13779
13780 $error = false;
13781 $next_page = false;
13782
13783 $has_valid_blog_id = is_numeric( $blog_id );
13784
13785 $user = null;
13786
13787 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
13788 /**
13789 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
13790 *
13791 * @author Vova Feldman (@svovaf)
13792 */
13793 $user = $fs->get_parent_instance()->get_current_or_network_user();
13794 } else if ( $fs->is_registered() ) {
13795 $user = $fs->get_current_or_network_user();
13796 }
13797
13798 if ( $has_valid_blog_id ) {
13799 /**
13800 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
13801 *
13802 * @author Leo Fajardo (@leorw)
13803 */
13804 $fs->switch_to_blog( $blog_id );
13805 }
13806
13807 if ( is_object( $user ) ) {
13808 $result = true;
13809
13810 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
13811 // If no specific blog ID was provided, activate the license for all sites in the network.
13812 $blog_2_install_map = array();
13813 $site_ids = array();
13814
13815 foreach ( $sites as $site ) {
13816 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
13817 continue;
13818 }
13819
13820 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
13821
13822 if ( is_object( $install ) ) {
13823 $blog_2_install_map[ $site['blog_id'] ] = $install;
13824 } else {
13825 $site_ids[] = $site['blog_id'];
13826 }
13827 }
13828
13829 if ( ! empty( $blog_2_install_map ) ) {
13830 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
13831 }
13832
13833 if ( true === $result && ! empty( $site_ids ) ) {
13834 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
13835 }
13836 } else {
13837 if ( $fs->is_registered() ) {
13838 $params = array(
13839 'license_key' => $fs->apply_filters( 'license_key', $license_key )
13840 );
13841
13842 $install_ids = array();
13843
13844 $change_owner = FS_User::is_valid_id( $license_owner_id );
13845
13846 if ( $change_owner ) {
13847 $params['user_id'] = $license_owner_id;
13848
13849 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
13850
13851 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13852 $install_ids[ $slug ] = $install_info['install']->id;
13853 }
13854
13855 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13856 }
13857
13858 $api = $fs->get_api_site_scope();
13859
13860 $result = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
13861
13862 if ( ! FS_Api::is_api_error( $result ) ) {
13863 $install = $result;
13864
13865 $fs->reconnect_locally( $has_valid_blog_id );
13866
13867 if (
13868 $change_owner &&
13869 // If successful ownership change.
13870 $fs->get_user()->id != $install->user_id
13871 ) {
13872 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
13873 }
13874 }
13875 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
13876 $result = $fs->activate_license_on_site( $user, $license_key );
13877 }
13878 }
13879
13880 $is_connected = null;
13881
13882 if ( true !== $result && ! FS_Api::is_api_result_entity( $result ) ) {
13883 if ( FS_Api::is_blocked( $result ) ) {
13884 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
13885
13886 $is_connected = false;
13887 }
13888
13889 $error = FS_Api::is_api_error_object( $result ) ?
13890 $result->error->message :
13891 var_export( $result, true );
13892 } else {
13893 $is_connected = true;
13894
13895 $fs->network_upgrade_mode_completed();
13896
13897 $fs->_user = $user;
13898
13899 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
13900 $fs->_site = $fs->get_network_install();
13901 }
13902
13903 $fs->_sync_license( true, $has_valid_blog_id );
13904
13905 $this->maybe_sync_install_user();
13906
13907 $next_page = $fs->is_addon() ?
13908 $fs->get_parent_instance()->get_account_url() :
13909 $fs->get_after_activation_url( 'after_connect_url' );
13910 }
13911
13912 $fs->update_connectivity_info( $is_connected );
13913 } else {
13914 $next_page = $fs->opt_in(
13915 false,
13916 false,
13917 false,
13918 $license_key,
13919 false,
13920 false,
13921 false,
13922 $is_marketing_allowed,
13923 $sites
13924 );
13925
13926 if ( isset( $next_page->error ) ) {
13927 $error = $next_page->error;
13928 } else {
13929 if ( $is_network_activation_or_migration ) {
13930 /**
13931 * Get the list of sites that were just opted-in (and license activated).
13932 * This is an optimization for the next part below saving some DB queries.
13933 */
13934 $connected_sites = array();
13935 foreach ( $sites as $site ) {
13936 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
13937 $connected_sites[ $site['blog_id'] ] = true;
13938 }
13939 }
13940
13941 $all_sites = self::get_sites();
13942 $pending_blog_ids = array();
13943
13944 /**
13945 * 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.
13946 *
13947 * @author Vova Feldman (@svovaf)
13948 */
13949 foreach ( $all_sites as $site ) {
13950 $blog_id = self::get_site_blog_id( $site );
13951
13952 if ( isset( $connected_sites[ $blog_id ] ) ) {
13953 // Site was just connected.
13954 continue;
13955 }
13956
13957 if ( $fs->is_installed_on_site( $blog_id ) ) {
13958 // Site was already connected before.
13959 continue;
13960 }
13961
13962 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
13963 // Site's connection was delegated.
13964 continue;
13965 }
13966
13967 if ( $fs->is_anonymous_site( $blog_id ) ) {
13968 // Site connection was already skipped.
13969 continue;
13970 }
13971
13972 $pending_blog_ids[] = $blog_id;
13973 }
13974
13975 if ( ! empty( $pending_blog_ids ) ) {
13976 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
13977 $fs->skip_connection( $pending_blog_ids );
13978 } else {
13979 $fs->delegate_connection( $pending_blog_ids );
13980 }
13981 }
13982 }
13983 }
13984 }
13985
13986 if ( false === $error && true === $fs->_storage->require_license_activation ) {
13987 $fs->_storage->require_license_activation = false;
13988 }
13989
13990 $result = array(
13991 'success' => ( false === $error )
13992 );
13993
13994 if ( false !== $error ) {
13995 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
13996 } else {
13997 if ( $fs->is_addon() || $fs->has_addons() ) {
13998 /**
13999 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
14000 * an updated valid user licenses collection will be fetched from the server which is used to also
14001 * update the account add-ons (add-ons the user has licenses for).
14002 *
14003 * @author Leo Fajardo (@leorw)
14004 * @since 2.2.4
14005 */
14006 $fs->purge_valid_user_licenses_cache();
14007 }
14008
14009 $result['next_page'] = $next_page;
14010 }
14011
14012 return $result;
14013 }
14014
14015 /**
14016 * @author Leo Fajardo (@leorw)
14017 * @since 2.3.2
14018 *
14019 * @return array {
14020 * @key string Product slug.
14021 * @value array {
14022 * @property FS_Site $site
14023 * @property FS_Plugin_License $license
14024 * }
14025 * }
14026 */
14027 private function get_parent_and_addons_installs_info() {
14028 $fs = $this->is_addon() ?
14029 $this->get_parent_instance() :
14030 $this;
14031
14032 $installed_addons_ids = array();
14033
14034 $installed_addons_instances = $fs->get_installed_addons();
14035 foreach ( $installed_addons_instances as $instance ) {
14036 $installed_addons_ids[] = $instance->get_id();
14037 }
14038
14039 $addons_ids = array_unique( array_merge(
14040 $installed_addons_ids,
14041 $fs->get_updated_account_addons()
14042 ) );
14043
14044 // Add parent product info.
14045 $installs_info_by_slug_map = array(
14046 $fs->get_slug() => array(
14047 'install' => $fs->get_site(),
14048 'license' => $fs->_get_license()
14049 )
14050 );
14051
14052 foreach ( $addons_ids as $addon_id ) {
14053 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
14054
14055 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
14056
14057 if ( ! isset( $addon_info['is_connected'] ) || ! $addon_info['is_connected'] ) {
14058 // Add-on is not associated with an install entity.
14059 continue;
14060 }
14061
14062 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
14063 'install' => $addon_info['site'],
14064 'license' => isset( $addon_info['license'] ) ?
14065 $addon_info['license'] :
14066 null
14067 );
14068 }
14069
14070 return $installs_info_by_slug_map;
14071 }
14072
14073 /**
14074 * @author Leo Fajardo (@leorw)
14075 * @since 1.2.3.1
14076 */
14077 function _network_activate_ajax_action() {
14078 $this->_logger->entrance();
14079
14080 $this->check_ajax_referer( 'network_activate' );
14081
14082 $plugin_id = fs_request_get( 'module_id', '', 'post' );
14083 $fs = ( $plugin_id == $this->_module_id ) ?
14084 $this :
14085 $this->get_addon_instance( $plugin_id );
14086
14087 $error = false;
14088
14089 $sites = fs_request_get( 'sites', array(), 'post' );
14090 if ( is_array( $sites ) && ! empty( $sites ) ) {
14091 $sites_by_action = array(
14092 'allow' => array(),
14093 'delegate' => array(),
14094 'skip' => array()
14095 );
14096
14097 foreach ( $sites as $site ) {
14098 $sites_by_action[ $site['action'] ][] = $site;
14099 }
14100
14101 $total_sites = count( $sites );
14102 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
14103
14104 $next_page = '';
14105
14106 $has_any_install = fs_request_get_bool( 'has_any_install' );
14107
14108 if ( $total_sites === $total_sites_to_delegate &&
14109 ! $this->is_network_upgrade_mode() &&
14110 ! $has_any_install
14111 ) {
14112 $this->delegate_connection();
14113 } else {
14114 if ( ! empty( $sites_by_action['delegate'] ) ) {
14115 $this->delegate_connection( self::get_sites_blog_ids( $sites_by_action['delegate'] ) );
14116 }
14117
14118 if ( ! empty( $sites_by_action['skip'] ) ) {
14119 $this->skip_connection( self::get_sites_blog_ids( $sites_by_action['skip'] ) );
14120 }
14121
14122 if ( empty( $sites_by_action['allow'] ) ) {
14123 if ( $has_any_install ) {
14124 $first_install = $fs->find_first_install();
14125
14126 if ( ! is_null( $first_install ) ) {
14127 $fs->_site = $first_install['install'];
14128 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
14129
14130 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
14131 $fs->_storage->network_user_id = $fs->_user->id;
14132 }
14133 }
14134 } else {
14135 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
14136 $next_page = $fs->opt_in(
14137 false,
14138 false,
14139 false,
14140 false,
14141 false,
14142 false,
14143 false,
14144 fs_request_get_bool( 'is_marketing_allowed', null ),
14145 $sites_by_action['allow']
14146 );
14147 } else {
14148 $next_page = $fs->install_with_user(
14149 $this->get_network_user(),
14150 false,
14151 false,
14152 false,
14153 true,
14154 $sites_by_action['allow']
14155 );
14156 }
14157
14158 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
14159 $error = $next_page->error;
14160 }
14161 }
14162 }
14163
14164 if ( empty( $next_page ) ) {
14165 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
14166 }
14167 } else {
14168 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
14169 }
14170
14171 $result = array(
14172 'success' => ( false === $error )
14173 );
14174
14175 if ( false !== $error ) {
14176 $result['error'] = $error;
14177 } else {
14178 $result['next_page'] = $next_page;
14179 }
14180
14181 echo json_encode( $result );
14182
14183 exit;
14184 }
14185
14186 /**
14187 * Billing update AJAX callback.
14188 *
14189 * @author Vova Feldman (@svovaf)
14190 * @since 1.2.1.5
14191 */
14192 function _update_billing_ajax_action() {
14193 $this->_logger->entrance();
14194
14195 $this->check_ajax_referer( 'update_billing' );
14196
14197 if ( ! $this->is_user_admin() ) {
14198 // Only for admins.
14199 self::shoot_ajax_failure();
14200 }
14201
14202 $billing = fs_request_get( 'billing' );
14203
14204 $api = $this->get_api_user_scope();
14205 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
14206 'plugin_id' => $this->get_parent_id(),
14207 ) ) );
14208
14209 if ( ! $this->is_api_result_entity( $result ) ) {
14210 self::shoot_ajax_failure();
14211 }
14212
14213 // Purge cached billing.
14214 $this->get_api_user_scope()->purge_cache( 'billing.json' );
14215
14216 self::shoot_ajax_success();
14217 }
14218
14219 /**
14220 * Trial start for anonymous users (AJAX callback).
14221 *
14222 * @author Vova Feldman (@svovaf)
14223 * @since 1.2.1.5
14224 */
14225 function _start_trial_ajax_action() {
14226 $this->_logger->entrance();
14227
14228 $this->check_ajax_referer( 'start_trial' );
14229
14230 if ( ! $this->is_user_admin() ) {
14231 // Only for admins.
14232 self::shoot_ajax_failure();
14233 }
14234
14235 $trial_data = fs_request_get( 'trial' );
14236
14237 $next_page = $this->opt_in(
14238 false,
14239 false,
14240 false,
14241 false,
14242 false,
14243 $trial_data['plan_id']
14244 );
14245
14246 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14247 self::shoot_ajax_failure(
14248 isset( $next_page->error ) ?
14249 $next_page->error->message :
14250 var_export( $next_page, true )
14251 );
14252 }
14253
14254 $this->shoot_ajax_success( array(
14255 'next_page' => $next_page,
14256 ) );
14257 }
14258
14259 /**
14260 * @author Leo Fajardo (@leorw)
14261 * @since 1.2.0
14262 */
14263 function _resend_license_key_ajax_action() {
14264 $this->_logger->entrance();
14265
14266 $this->check_ajax_referer( 'resend_license_key' );
14267
14268 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14269
14270 if ( empty( $email_address ) ) {
14271 exit;
14272 }
14273
14274 $error = false;
14275
14276 $api = $this->get_api_plugin_scope();
14277 $result = $api->call( '/licenses/resend.json', 'post',
14278 array(
14279 'email' => $email_address,
14280 'url' => home_url(),
14281 )
14282 );
14283
14284 if ( is_object( $result ) && isset( $result->error ) ) {
14285 $error = $result->error;
14286
14287 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14288 $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' );
14289 } else if ( 'no_license' === $error->code ) {
14290 $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' );
14291 } else {
14292 $error = $error->message;
14293 }
14294 }
14295
14296 $licenses = array(
14297 'success' => ( false === $error )
14298 );
14299
14300 if ( false !== $error ) {
14301 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14302 }
14303
14304 echo json_encode( $licenses );
14305
14306 exit;
14307 }
14308
14309 /**
14310 * @author Vova Feldman (@svovaf)
14311 * @since 1.2.1.8
14312 *
14313 * @var string
14314 */
14315 private static $_pagenow;
14316
14317 /**
14318 * Get current page or the referer if executing a WP AJAX request.
14319 *
14320 * @author Vova Feldman (@svovaf)
14321 * @since 1.2.1.8
14322 *
14323 * @return string
14324 */
14325 static function get_current_page() {
14326 if ( ! isset( self::$_pagenow ) ) {
14327 global $pagenow;
14328 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14329 /**
14330 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14331 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14332 *
14333 * @author Leo Fajardo (@leorw)
14334 * @since 2.2.3
14335 */
14336 if ( is_network_admin() ) {
14337 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14338 } else if ( is_user_admin() ) {
14339 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14340 } else {
14341 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14342 }
14343
14344 $pagenow = $self_matches[1];
14345 $pagenow = trim( $pagenow, '/' );
14346 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14347 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14348 $pagenow = 'index.php';
14349 } else {
14350 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14351 $pagenow = strtolower( $self_matches[1] );
14352 if ( '.php' !== substr($pagenow, -4, 4) )
14353 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14354 }
14355 }
14356
14357 self::$_pagenow = $pagenow;
14358
14359 if ( self::is_ajax() &&
14360 'admin-ajax.php' === $pagenow
14361 ) {
14362 $referer = fs_get_raw_referer();
14363
14364 if ( is_string( $referer ) ) {
14365 $parts = explode( '?', $referer );
14366
14367 self::$_pagenow = basename( $parts[0] );
14368 }
14369 }
14370 }
14371
14372 return self::$_pagenow;
14373 }
14374
14375 /**
14376 * Helper method to check if user in the plugins page.
14377 *
14378 * @author Vova Feldman (@svovaf)
14379 * @since 1.2.1.5
14380 *
14381 * @return bool
14382 */
14383 static function is_plugins_page() {
14384 return ( 'plugins.php' === self::get_current_page() );
14385 }
14386
14387 /**
14388 * @author Leo Fajardo (@leorw)
14389 * @since 2.2.3
14390 *
14391 * @return bool
14392 */
14393 static function is_plugin_install_page() {
14394 return ( 'plugin-install.php' === self::get_current_page() );
14395 }
14396
14397 /**
14398 * @author Leo Fajardo (@leorw)
14399 * @since 2.0.2
14400 *
14401 * @return bool
14402 */
14403 static function is_updates_page() {
14404 return ( 'update-core.php' === self::get_current_page() );
14405 }
14406
14407 /**
14408 * Helper method to check if user in the themes page.
14409 *
14410 * @author Vova Feldman (@svovaf)
14411 * @since 1.2.2.6
14412 *
14413 * @return bool
14414 */
14415 static function is_themes_page() {
14416 return ( 'themes.php' === self::get_current_page() );
14417 }
14418
14419 #----------------------------------------------------------------------------------
14420 #region Affiliation
14421 #----------------------------------------------------------------------------------
14422
14423 /**
14424 * @author Leo Fajardo (@leorw)
14425 * @since 1.2.3
14426 *
14427 * @return bool
14428 */
14429 function has_affiliate_program() {
14430 if ( ! is_object( $this->_plugin ) ) {
14431 return false;
14432 }
14433
14434 return $this->_plugin->has_affiliate_program();
14435 }
14436
14437 /**
14438 * Get Plugin ID under which we will track affiliate application.
14439 *
14440 * This could either be the Bundle ID or the main plugin ID.
14441 *
14442 * @return number Bundle ID if developer has provided one, else the main plugin ID.
14443 */
14444 private function get_plugin_id_for_affiliate_terms() {
14445 return $this->has_bundle_context() ?
14446 $this->get_bundle_id() :
14447 $this->_plugin->id;
14448 }
14449
14450 /**
14451 * @author Leo Fajardo (@leorw)
14452 * @since 1.2.4
14453 */
14454 private function fetch_affiliate_terms() {
14455 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14456 /**
14457 * In case we have a bundle set in SDK configuration, we would like to use that for affiliates, not the main plugin.
14458 */
14459 $plugins_api = $this->has_bundle_context() ?
14460 $this->get_api_bundle_scope() :
14461 $this->get_api_plugin_scope();
14462
14463 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14464
14465 /**
14466 * 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.
14467 */
14468 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14469 return;
14470 }
14471
14472 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14473 }
14474 }
14475
14476 /**
14477 * @author Leo Fajardo (@leorw)
14478 * @since 1.2.4
14479 */
14480 private function fetch_affiliate_and_custom_terms() {
14481 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14482 $application_data = $this->_storage->affiliate_application_data;
14483 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14484
14485 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14486
14487 $users_api = $this->get_api_user_scope();
14488 $result = $users_api->get( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14489 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14490 if ( ! empty( $result->affiliates ) ) {
14491 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14492
14493 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14494 $application_data['status'] = $affiliate->status;
14495 $this->_storage->affiliate_application_data = $application_data;
14496 }
14497
14498 if ( $affiliate->is_using_custom_terms ) {
14499 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14500 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14501 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14502 }
14503 }
14504
14505 $this->affiliate = $affiliate;
14506 }
14507 }
14508 }
14509 }
14510
14511 /**
14512 * @author Leo Fajardo (@leorw)
14513 * @since 1.2.3
14514 */
14515 private function fetch_affiliate_and_terms() {
14516 $this->_logger->entrance();
14517
14518 $this->fetch_affiliate_terms();
14519 $this->fetch_affiliate_and_custom_terms();
14520 }
14521
14522 /**
14523 * @author Leo Fajardo (@leorw)
14524 * @since 1.2.3
14525 *
14526 * @return FS_Affiliate
14527 */
14528 function get_affiliate() {
14529 return $this->affiliate;
14530 }
14531
14532
14533 /**
14534 * @author Leo Fajardo (@leorw)
14535 * @since 1.2.3
14536 *
14537 * @return FS_AffiliateTerms
14538 */
14539 function get_affiliate_terms() {
14540 return is_object( $this->custom_affiliate_terms ) ?
14541 $this->custom_affiliate_terms :
14542 $this->plugin_affiliate_terms;
14543 }
14544
14545 /**
14546 * @author Leo Fajardo (@leorw)
14547 * @since 1.2.3
14548 */
14549 function _submit_affiliate_application() {
14550 $this->_logger->entrance();
14551
14552 $this->check_ajax_referer( 'submit_affiliate_application' );
14553
14554 if ( ! $this->is_user_admin() ) {
14555 // Only for admins.
14556 self::shoot_ajax_failure();
14557 }
14558
14559 $affiliate = fs_request_get( 'affiliate' );
14560
14561 if ( empty( $affiliate['promotion_methods'] ) ) {
14562 unset( $affiliate['promotion_methods'] );
14563 }
14564
14565 if ( ! empty( $affiliate['additional_domains'] ) ) {
14566 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
14567 }
14568
14569 if ( ! $this->is_registered() ) {
14570 $email_address = isset( $affiliate['email'] ) ? $affiliate['email'] : '';
14571
14572 if ( ! is_email( $email_address ) ) {
14573 self::shoot_ajax_failure('Invalid email address.');
14574 }
14575
14576 // Opt in but don't track usage.
14577 $next_page = $this->opt_in(
14578 $email_address,
14579 false,
14580 false,
14581 false,
14582 false,
14583 false,
14584 true
14585 );
14586
14587 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14588 self::shoot_ajax_failure(
14589 isset( $next_page->error ) ?
14590 $next_page->error->message :
14591 var_export( $next_page, true )
14592 );
14593 } else if ( $this->is_pending_activation() ) {
14594 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' ) );
14595 }
14596 }
14597
14598 $this->fetch_affiliate_terms();
14599
14600 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14601
14602 $api = $this->get_api_user_scope();
14603 $result = $api->call(
14604 ( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
14605 'post',
14606 $affiliate
14607 );
14608
14609 if ( $this->is_api_error( $result ) ) {
14610 self::shoot_ajax_failure(
14611 isset( $result->error ) ?
14612 $result->error->message :
14613 var_export( $result, true )
14614 );
14615 } else {
14616 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
14617 $this->_admin_notices->remove_sticky( 'affiliate_program' );
14618 }
14619
14620 $affiliate_application_data = array(
14621 'status' => 'pending',
14622 'stats_description' => $affiliate['stats_description'],
14623 'promotion_method_description' => $affiliate['promotion_method_description'],
14624 );
14625
14626 if ( ! empty( $affiliate['promotion_methods'] ) ) {
14627 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
14628 }
14629
14630 if ( ! empty( $affiliate['domain'] ) ) {
14631 $affiliate_application_data['domain'] = $affiliate['domain'];
14632 }
14633
14634 if ( ! empty( $affiliate['additional_domains'] ) ) {
14635 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
14636 }
14637
14638 $this->_storage->affiliate_application_data = $affiliate_application_data;
14639 }
14640
14641 // Purge cached affiliate.
14642 $api->purge_cache( 'affiliate.json' );
14643
14644 self::shoot_ajax_success( $result );
14645 }
14646
14647 /**
14648 * @author Leo Fajardo (@leorw)
14649 * @since 1.2.3
14650 *
14651 * @return array|null
14652 */
14653 function get_affiliate_application_data() {
14654 if ( empty( $this->_storage->affiliate_application_data ) ) {
14655 return null;
14656 }
14657
14658 return $this->_storage->affiliate_application_data;
14659 }
14660
14661 #endregion Affiliation ------------------------------------------------------------
14662
14663 #----------------------------------------------------------------------------------
14664 #region URL Generators
14665 #----------------------------------------------------------------------------------
14666
14667 /**
14668 * Alias to pricing_url().
14669 *
14670 * @author Vova Feldman (@svovaf)
14671 * @since 1.0.2
14672 *
14673 * @uses pricing_url()
14674 *
14675 * @param string $period Billing cycle
14676 * @param bool $is_trial
14677 *
14678 * @return string
14679 */
14680 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14681 return $this->pricing_url( $period, $is_trial );
14682 }
14683
14684 /**
14685 * @author Vova Feldman (@svovaf)
14686 * @since 1.0.9
14687 *
14688 * @uses get_upgrade_url()
14689 *
14690 * @return string
14691 */
14692 function get_trial_url() {
14693 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
14694 }
14695
14696 /**
14697 * @author Leo Fajardo (@leorw)
14698 * @since 2.1.4
14699 *
14700 * @param string $new_version
14701 *
14702 * @return string
14703 */
14704 function version_upgrade_checkout_link( $new_version ) {
14705 if ( ! is_object( $this->_license ) ) {
14706 $url = $this->pricing_url();
14707
14708 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
14709 } else {
14710 $subscription = $this->_get_subscription( $this->_license->id );
14711
14712 $url = $this->checkout_url(
14713 is_object( $subscription ) ?
14714 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
14715 WP_FS__PERIOD_LIFETIME,
14716 false,
14717 array( 'licenses' => $this->_license->quota )
14718 );
14719
14720 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
14721 }
14722
14723 return sprintf(
14724 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
14725 sprintf(
14726 '<a href="%s">%s</a>',
14727 $this->apply_filters( 'update_notice_checkout_url', $url ),
14728 $purchase_license_text
14729 ),
14730 $new_version
14731 );
14732 }
14733
14734 /**
14735 * Plugin's pricing URL.
14736 *
14737 * @author Vova Feldman (@svovaf)
14738 * @since 1.0.4
14739 *
14740 * @param string $billing_cycle Billing cycle
14741 *
14742 * @param bool $is_trial
14743 *
14744 * @return string
14745 */
14746 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14747 $this->_logger->entrance();
14748
14749 $params = array(
14750 'billing_cycle' => $billing_cycle
14751 );
14752
14753 if ( $is_trial ) {
14754 $params['trial'] = 'true';
14755 }
14756
14757 $url = $this->is_addon() ?
14758 $this->_parent->addon_url( $this->_slug ) :
14759 $this->_get_admin_page_url( 'pricing', $params );
14760
14761 return $this->get_pricing_url_with_filter( $url );
14762 }
14763
14764 /**
14765 * Retrieves the filtered pricing URL.
14766 *
14767 * @author Leo Fajardo (@leorw)
14768 * @since 2.7.4
14769 *
14770 * @param string $url
14771 *
14772 * @return string
14773 */
14774 private function get_pricing_url_with_filter( $url ) {
14775 return $this->apply_filters( 'pricing_url', $url );
14776 }
14777
14778 /**
14779 * Checkout page URL.
14780 *
14781 * @author Vova Feldman (@svovaf)
14782 * @since 1.0.6
14783 *
14784 * @param string $billing_cycle Billing cycle
14785 * @param bool $is_trial
14786 * @param array $extra (optional) Extra parameters, override other query params.
14787 * @param bool|null $network
14788 *
14789 * @return string
14790 */
14791 function checkout_url(
14792 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14793 $is_trial = false,
14794 $extra = array(),
14795 $network = null
14796 ) {
14797 $this->_logger->entrance();
14798
14799 $params = array(
14800 'checkout' => 'true',
14801 'billing_cycle' => $billing_cycle,
14802 );
14803
14804 if ( $is_trial ) {
14805 $params['trial'] = 'true';
14806 }
14807
14808 /**
14809 * Params in extra override other params.
14810 */
14811 $params = array_merge( $params, $extra );
14812
14813 return $this->apply_filters( 'checkout_url', $this->_get_admin_page_url( 'pricing', $params, $network ) );
14814 }
14815
14816 /**
14817 * Add-on checkout URL.
14818 *
14819 * @author Vova Feldman (@svovaf)
14820 * @since 1.1.7
14821 *
14822 * @param number $addon_id
14823 * @param number $pricing_id
14824 * @param string $billing_cycle
14825 * @param bool $is_trial
14826 * @param bool|null $network
14827 *
14828 * @return string
14829 */
14830 function addon_checkout_url(
14831 $addon_id,
14832 $pricing_id,
14833 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14834 $is_trial = false,
14835 $network = null
14836 ) {
14837 return $this->checkout_url( $billing_cycle, $is_trial, array(
14838 'plugin_id' => $addon_id,
14839 'pricing_id' => $pricing_id,
14840 ), $network );
14841 }
14842
14843 #endregion
14844
14845 #endregion ------------------------------------------------------------------
14846
14847 /**
14848 * Check if plugin has any add-ons.
14849 *
14850 * @author Vova Feldman (@svovaf)
14851 * @since 1.0.5
14852 *
14853 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
14854 *
14855 * @return bool
14856 */
14857 function has_addons() {
14858 $this->_logger->entrance();
14859
14860 return $this->_has_addons;
14861 }
14862
14863 /**
14864 * Check if plugin can work in anonymous mode.
14865 *
14866 * @author Vova Feldman (@svovaf)
14867 * @since 1.0.9
14868 *
14869 * @return bool
14870 *
14871 * @deprecated Please use is_enable_anonymous() instead.
14872 */
14873 function enable_anonymous() {
14874 return $this->_enable_anonymous;
14875 }
14876
14877 /**
14878 * Check if plugin can work in anonymous mode.
14879 *
14880 * @author Vova Feldman (@svovaf)
14881 * @since 1.1.9
14882 *
14883 * @return bool
14884 */
14885 function is_enable_anonymous() {
14886 return $this->_enable_anonymous;
14887 }
14888
14889 /**
14890 * Check if plugin is premium only (no free plans).
14891 *
14892 * @author Vova Feldman (@svovaf)
14893 * @since 1.1.9
14894 *
14895 * @return bool
14896 */
14897 function is_only_premium() {
14898 return $this->_is_premium_only;
14899 }
14900
14901 /**
14902 * Checks if the plugin's type is "plugin". The other type is "theme".
14903 *
14904 * @author Leo Fajardo (@leorw)
14905 * @since 1.2.2
14906 *
14907 * @return bool
14908 */
14909 function is_plugin() {
14910 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
14911 }
14912
14913 /**
14914 * @author Leo Fajardo (@leorw)
14915 * @since 1.2.2
14916 *
14917 * @return string
14918 */
14919 function get_module_type() {
14920 if ( ! isset( $this->_module_type ) ) {
14921 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
14922 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
14923 }
14924
14925 return $this->_module_type;
14926 }
14927
14928 /**
14929 * @author Leo Fajardo (@leorw)
14930 * @since 1.2.2
14931 *
14932 * @return string
14933 */
14934 function get_plugin_main_file_path() {
14935 return $this->_plugin_main_file_path;
14936 }
14937
14938 /**
14939 * Check if module has a premium code version.
14940 *
14941 * Serviceware module might be freemium without any
14942 * premium code version, where the paid features
14943 * are all part of the service.
14944 *
14945 * @author Vova Feldman (@svovaf)
14946 * @since 1.2.1.6
14947 *
14948 * @return bool
14949 */
14950 function has_premium_version() {
14951 return $this->_has_premium_version;
14952 }
14953
14954 /**
14955 * Check if feature supported with current site's plan.
14956 *
14957 * @author Vova Feldman (@svovaf)
14958 * @since 1.0.1
14959 *
14960 * @todo IMPLEMENT
14961 *
14962 * @param number $feature_id
14963 *
14964 * @throws Exception
14965 */
14966 function is_feature_supported( $feature_id ) {
14967 throw new Exception( 'not implemented' );
14968 }
14969
14970 /**
14971 * @author Vova Feldman (@svovaf)
14972 * @since 1.0.1
14973 *
14974 * @return bool Is running in SSL/HTTPS
14975 */
14976 function is_ssl() {
14977 return WP_FS__IS_HTTPS;
14978 }
14979
14980 /**
14981 * @author Vova Feldman (@svovaf)
14982 * @since 1.0.9
14983 *
14984 * @return bool Is running in AJAX call.
14985 *
14986 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
14987 */
14988 static function is_ajax() {
14989 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
14990 }
14991
14992 /**
14993 * Check if it's an AJAX call targeted for the current module.
14994 *
14995 * @author Vova Feldman (@svovaf)
14996 * @since 1.2.0
14997 *
14998 * @param array|string $actions Collection of AJAX actions.
14999 *
15000 * @return bool
15001 */
15002 function is_ajax_action( $actions ) {
15003 // Verify it's an ajax call.
15004 if ( ! self::is_ajax() ) {
15005 return false;
15006 }
15007
15008 // Verify the call is relevant for the plugin.
15009 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
15010 return false;
15011 }
15012
15013 // Verify it's one of the specified actions.
15014 if ( is_string( $actions ) ) {
15015 $actions = explode( ',', $actions );
15016 }
15017
15018 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15019 $ajax_action = fs_request_get( 'action' );
15020
15021 foreach ( $actions as $action ) {
15022 if ( $ajax_action === $this->get_action_tag( $action ) ) {
15023 return true;
15024 }
15025 }
15026 }
15027
15028 return false;
15029 }
15030
15031 /**
15032 * Check if it's an AJAX call targeted for current request.
15033 *
15034 * @author Vova Feldman (@svovaf)
15035 * @since 1.2.0
15036 *
15037 * @param array|string $actions Collection of AJAX actions.
15038 * @param number|null $module_id
15039 *
15040 * @return bool
15041 */
15042 static function is_ajax_action_static( $actions, $module_id = null ) {
15043 // Verify it's an ajax call.
15044 if ( ! self::is_ajax() ) {
15045 return false;
15046 }
15047
15048
15049 if ( ! empty( $module_id ) ) {
15050 // Verify the call is relevant for the plugin.
15051 if ( $module_id != fs_request_get( 'module_id' ) ) {
15052 return false;
15053 }
15054 }
15055
15056 // Verify it's one of the specified actions.
15057 if ( is_string( $actions ) ) {
15058 $actions = explode( ',', $actions );
15059 }
15060
15061 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15062 $ajax_action = fs_request_get( 'action' );
15063
15064 foreach ( $actions as $action ) {
15065 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
15066 return true;
15067 }
15068 }
15069 }
15070
15071 return false;
15072 }
15073
15074 /**
15075 * @author Vova Feldman (@svovaf)
15076 * @since 1.1.7
15077 *
15078 * @return bool
15079 */
15080 static function is_cron() {
15081 return ( defined( 'DOING_CRON' ) && DOING_CRON );
15082 }
15083
15084 /**
15085 * @author Leo Fajardo (@leorw)
15086 * @since 2.5.0
15087 *
15088 * @return bool
15089 */
15090 static function is_admin_post() {
15091 return ( 'admin-post.php' === self::get_current_page() );
15092 }
15093
15094 /**
15095 * Check if a real user is visiting the admin dashboard.
15096 *
15097 * @author Vova Feldman (@svovaf)
15098 * @since 1.1.7
15099 *
15100 * @return bool
15101 */
15102 function is_user_in_admin() {
15103 return (
15104 is_admin() &&
15105 ! self::is_ajax() &&
15106 ! self::is_cron() &&
15107 ! self::is_admin_post()
15108 );
15109 }
15110
15111 /**
15112 * Check if a real user is in the customizer view.
15113 *
15114 * @author Vova Feldman (@svovaf)
15115 * @since 1.2.2.7
15116 *
15117 * @return bool
15118 */
15119 static function is_customizer() {
15120 return is_customize_preview();
15121 }
15122
15123 /**
15124 * Check if running in HTTPS and if site's plan matching the specified plan.
15125 *
15126 * @param string $plan
15127 * @param bool $exact
15128 *
15129 * @return bool
15130 */
15131 function is_ssl_and_plan( $plan, $exact = false ) {
15132 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
15133 }
15134
15135 /**
15136 * Construct plugin's settings page URL.
15137 *
15138 * @author Vova Feldman (@svovaf)
15139 * @since 1.0.4
15140 *
15141 * @param string $page
15142 * @param array $params
15143 * @param bool|null $network
15144 *
15145 * @return string
15146 */
15147 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
15148 if ( is_null( $network ) ) {
15149 $network = (
15150 $this->_is_network_active &&
15151 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
15152 );
15153 }
15154
15155 if ( 0 < count( $params ) ) {
15156 foreach ( $params as $k => $v ) {
15157 $params[ $k ] = urlencode( $v );
15158 }
15159 }
15160
15161 $page_param = $this->_menu->get_slug( $page );
15162
15163 if ( empty( $page ) &&
15164 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
15165 $this->show_opt_in_on_themes_page()
15166 ) {
15167 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
15168
15169 return add_query_arg(
15170 $params,
15171 $this->admin_url( 'themes.php', 'admin', $network )
15172 );
15173 }
15174
15175 if ( ! $this->has_settings_menu() ) {
15176 if ( ! empty( $page ) ) {
15177 // Module doesn't have a setting page, but since the request is for
15178 // a specific Freemius page, use the admin.php path.
15179 return add_query_arg( array_merge( $params, array(
15180 'page' => $page_param,
15181 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15182 } else {
15183 if ( $this->is_activation_mode() ) {
15184 /**
15185 * @author Vova Feldman
15186 * @since 1.2.1.6
15187 *
15188 * If plugin doesn't have a settings page, create one for the opt-in screen.
15189 */
15190 return add_query_arg( array_merge( $params, array(
15191 'page' => $this->_slug,
15192 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15193 } else {
15194 // Plugin without a settings page.
15195 return add_query_arg(
15196 $params,
15197 $this->admin_url( 'plugins.php', 'admin', $network )
15198 );
15199 }
15200 }
15201 }
15202
15203 // Module has a submenu settings page.
15204 if ( ! $this->_menu->is_top_level() ) {
15205 $parent_slug = $this->_menu->get_parent_slug();
15206 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
15207 $parent_slug :
15208 'admin.php';
15209
15210 return add_query_arg( array_merge( $params, array(
15211 'page' => $page_param,
15212 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
15213 }
15214
15215 // Module has a top level CPT settings page.
15216 if ( $this->_menu->is_cpt() ) {
15217 if ( empty( $page ) && $this->is_activation_mode() ) {
15218 return add_query_arg( array_merge( $params, array(
15219 'page' => $page_param
15220 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15221 } else {
15222 if ( ! empty( $page ) ) {
15223 $params['page'] = $page_param;
15224 }
15225
15226 return add_query_arg(
15227 $params,
15228 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
15229 );
15230 }
15231 }
15232
15233 // Module has a custom top level settings page.
15234 return add_query_arg( array_merge( $params, array(
15235 'page' => $page_param,
15236 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15237 }
15238
15239 #--------------------------------------------------------------------------------
15240 #region Multisite
15241 #--------------------------------------------------------------------------------
15242
15243 /**
15244 * @author Leo Fajardo (@leorw)
15245 * @since 2.0.0
15246 *
15247 * @return bool
15248 */
15249 function is_network_active() {
15250 return $this->_is_network_active;
15251 }
15252
15253 /**
15254 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
15255 *
15256 * @author Leo Fajardo (@leorw)
15257 * @since 2.0.0
15258 *
15259 * @param bool|int[] $all_or_blog_ids
15260 */
15261 private function delegate_connection( $all_or_blog_ids = true ) {
15262 $this->_logger->entrance();
15263
15264 $this->_admin_notices->remove_sticky( 'connect_account' );
15265
15266 if ( true === $all_or_blog_ids ) {
15267 // All sites delegation.
15268 $this->_storage->store( 'is_delegated_connection', true, true );
15269 } else {
15270 // Specified sites delegation.
15271 foreach ( $all_or_blog_ids as $blog_id ) {
15272 $this->delegate_site_connection( $blog_id );
15273 }
15274 }
15275
15276 $this->network_upgrade_mode_completed();
15277 }
15278
15279 /**
15280 * Delegate specific network site conncetion to the site admin.
15281 *
15282 * @author Vova Feldman (@svovaf)
15283 * @since 2.0.0
15284 *
15285 * @param int $blog_id
15286 */
15287 private function delegate_site_connection( $blog_id ) {
15288 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
15289 }
15290
15291 /**
15292 * Check if super-admin delegated the connection of ALL sites to the site admins.
15293 *
15294 * @author Vova Feldman (@svovaf)
15295 * @since 2.0.0
15296 *
15297 * @return bool
15298 */
15299 function is_network_delegated_connection() {
15300 if ( ! $this->_is_network_active ) {
15301 return false;
15302 }
15303
15304 return $this->_storage->get( 'is_delegated_connection', false, true );
15305 }
15306
15307 /**
15308 * @author Leo Fajardo (@leorw)
15309 * @since 2.0.0
15310 *
15311 * @param int $blog_id
15312 *
15313 * @return bool
15314 */
15315 function is_site_delegated_connection( $blog_id = 0 ) {
15316 if ( ! $this->_is_network_active ) {
15317 return false;
15318 }
15319
15320 if ( 0 == $blog_id ) {
15321 $blog_id = get_current_blog_id();
15322 }
15323
15324 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15325 }
15326
15327 /**
15328 * Check if delegated the connection. When running within the network admin,
15329 * and haven't specified the blog ID, checks if network level delegated. If running
15330 * within a site admin or specified a blog ID, check if delegated the connection for
15331 * the current context site.
15332 *
15333 * If executed outside the the admin, check if delegated the connection
15334 * for the current context site OR the whole network.
15335 *
15336 * @author Vova Feldman (@svovaf)
15337 * @since 2.0.0
15338 *
15339 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15340 *
15341 * @return bool
15342 */
15343 function is_delegated_connection( $blog_id = 0 ) {
15344 if ( ! $this->_is_network_active ) {
15345 return false;
15346 }
15347
15348 if ( fs_is_network_admin() && 0 == $blog_id ) {
15349 return $this->is_network_delegated_connection();
15350 }
15351
15352 return (
15353 $this->is_network_delegated_connection() ||
15354 $this->is_site_delegated_connection( $blog_id )
15355 );
15356 }
15357
15358 /**
15359 * Check if the current module is active for the site.
15360 *
15361 * @author Vova Feldman (@svovaf)
15362 * @since 2.0.0
15363 *
15364 * @param int $blog_id
15365 *
15366 * @return bool
15367 */
15368 function is_active_for_site( $blog_id ) {
15369 if ( ! is_multisite() ) {
15370 // Not a multisite and this code is executed, means that the plugin is active.
15371 return true;
15372 }
15373
15374 if ( $this->is_theme() ) {
15375 // All themes are site level activated.
15376 return true;
15377 }
15378
15379 if ( $this->_is_network_active ) {
15380 // Plugin was network activated so it's active.
15381 return true;
15382 }
15383
15384 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15385 }
15386
15387 /**
15388 * @todo Implement pagination when accessing the subsites collection.
15389 *
15390 * @author Leo Fajardo (@leorw)
15391 * @since 2.0.0
15392 *
15393 * @param int $limit Default to 1,000
15394 * @param int $offset Default to 0
15395 *
15396 * @return array Active & public sites collection.
15397 */
15398 static function get_sites( $limit = 1000, $offset = 0 ) {
15399 if ( ! is_multisite() ) {
15400 return array();
15401 }
15402
15403 /**
15404 * For consistency with get_blog_list() which only return active public sites.
15405 *
15406 * @author Vova Feldman (@svovaf)
15407 */
15408 $args = array(
15409 /**
15410 * Commented out in order to handle the migration of site options whether the site is public or not.
15411 *
15412 * @author Leo Fajardo (@leorw)
15413 * @since 2.2.1
15414 */
15415 // 'public' => 1,
15416 'archived' => 0,
15417 'mature' => 0,
15418 'spam' => 0,
15419 'deleted' => 0,
15420 'number' => $limit,
15421 'offset' => $offset,
15422 );
15423
15424 return get_sites( $args );
15425 }
15426
15427 /**
15428 * Checks if a given blog is active.
15429 *
15430 * @author Vova Feldman (@svovaf)
15431 * @since 2.0.0
15432 *
15433 * @param $blog_id
15434 *
15435 * @return bool
15436 */
15437 private static function is_site_active( $blog_id ) {
15438 global $wpdb;
15439
15440 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15441
15442 if ( ! is_object( $blog_info ) ) {
15443 return false;
15444 }
15445
15446 return (
15447 true == $blog_info->public &&
15448 false == $blog_info->archived &&
15449 false == $blog_info->mature &&
15450 false == $blog_info->spam &&
15451 false == $blog_info->deleted
15452 );
15453 }
15454
15455 /**
15456 * Get a mapping between the site addresses to their blog IDs.
15457 *
15458 * @author Vova Feldman (@svovaf)
15459 * @since 2.0.0
15460 *
15461 * @return array {
15462 * @key string Site address without protocol with a trailing slash.
15463 * @value int Site's blog ID.
15464 * }
15465 */
15466 private function get_address_to_blog_map() {
15467 $sites = self::get_sites();
15468
15469 // Map site addresses to their blog IDs.
15470 $address_to_blog_map = array();
15471 foreach ( $sites as $site ) {
15472 $blog_id = self::get_site_blog_id( $site );
15473 $address = self::get_unfiltered_site_url( $blog_id, true, true );
15474 $address_to_blog_map[ $address ] = $blog_id;
15475 }
15476
15477 return $address_to_blog_map;
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 int Site's blog ID.
15488 * @value FS_Site Associated install.
15489 * }
15490 */
15491 function get_blog_install_map() {
15492 $sites = self::get_sites();
15493
15494 // Map site blog ID to its install.
15495 $install_map = array();
15496
15497 foreach ( $sites as $site ) {
15498 $blog_id = self::get_site_blog_id( $site );
15499 $install = $this->get_install_by_blog_id( $blog_id );
15500
15501 if ( is_object( $install ) ) {
15502 $install_map[ $blog_id ] = $install;
15503 }
15504 }
15505
15506 return $install_map;
15507 }
15508
15509 /**
15510 * @author Vova Feldman (@svovaf)
15511 * @since 2.5.1
15512 *
15513 * @param bool|null $is_delegated When `true`, returns only connection delegated blog IDs. When `false`, only non-delegated blog IDs.
15514 *
15515 * @return int[]
15516 */
15517 private function get_blog_ids( $is_delegated = null ) {
15518 $blog_ids = array();
15519
15520 $sites = self::get_sites();
15521 foreach ( $sites as $site ) {
15522 $blog_id = self::get_site_blog_id( $site );
15523
15524 if (
15525 is_null( $is_delegated ) ||
15526 $is_delegated === $this->is_site_delegated_connection( $blog_id )
15527 ) {
15528 $blog_ids[] = $blog_id;
15529 }
15530 }
15531
15532 return $blog_ids;
15533 }
15534
15535 /**
15536 * @author Vova Feldman (@svovaf)
15537 * @since 2.5.1
15538 *
15539 * @return int[]
15540 */
15541 private function get_non_delegated_blog_ids() {
15542 return $this->get_blog_ids( false );
15543 }
15544
15545 /**
15546 * Gets a map of module IDs that the given user has opted-in to.
15547 *
15548 * @author Leo Fajardo (@leorw)
15549 * @since 2.1.0
15550 *
15551 * @param number $fs_user_id
15552 *
15553 * @return array {
15554 * @key number $plugin_id
15555 * @value bool Always true.
15556 * }
15557 */
15558 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15559 self::$_static_logger->entrance();
15560
15561 if ( ! is_multisite() ) {
15562 $installs = array_merge(
15563 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15564 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15565 );
15566 } else {
15567 $sites = self::get_sites();
15568
15569 $installs = array();
15570 foreach ( $sites as $site ) {
15571 $blog_id = self::get_site_blog_id( $site );
15572
15573 $installs = array_merge(
15574 $installs,
15575 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15576 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15577 );
15578 }
15579 }
15580
15581 $module_ids_map = array();
15582 foreach ( $installs as $install ) {
15583 if ( is_object( $install ) &&
15584 FS_Site::is_valid_id( $install->id ) &&
15585 FS_User::is_valid_id( $install->user_id ) &&
15586 ( $install->user_id == $fs_user_id )
15587 ) {
15588 $module_ids_map[ $install->plugin_id ] = true;
15589 }
15590 }
15591
15592 return $module_ids_map;
15593 }
15594
15595 /**
15596 * @author Leo Fajardo (@leorw)
15597 *
15598 * @return null|array {
15599 * 'install' => FS_Site Module's install,
15600 * 'blog_id' => string The associated blog ID.
15601 * }
15602 */
15603 function find_first_install() {
15604 $sites = self::get_sites();
15605
15606 foreach ( $sites as $site ) {
15607 $blog_id = self::get_site_blog_id( $site );
15608 $install = $this->get_install_by_blog_id( $blog_id );
15609
15610 if ( is_object( $install ) ) {
15611 return array(
15612 'install' => $install,
15613 'blog_id' => $blog_id
15614 );
15615 }
15616 }
15617
15618 return null;
15619 }
15620
15621 /**
15622 * Switches the Freemius site level context to a specified blog.
15623 *
15624 * @author Vova Feldman (@svovaf)
15625 * @since 2.0.0
15626 *
15627 * @param int $blog_id
15628 * @param FS_Site $install
15629 * @param bool $flush
15630 *
15631 * @return bool Since 2.3.1 returns if a switch was made.
15632 */
15633 function switch_to_blog( $blog_id, FS_Site $install = null, $flush = false ) {
15634 if ( ! is_numeric( $blog_id ) ) {
15635 return false;
15636 }
15637
15638 if ( ! $flush && $blog_id == $this->_context_is_network_or_blog_id ) {
15639 return false;
15640 }
15641
15642 switch_to_blog( $blog_id );
15643 $this->_context_is_network_or_blog_id = $blog_id;
15644
15645 self::$_accounts->set_site_blog_context( $blog_id );
15646 $this->_storage->set_site_blog_context( $blog_id );
15647 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
15648
15649 $this->_site = is_object( $install ) ?
15650 $install :
15651 $this->get_install_by_blog_id( $blog_id );
15652
15653 $this->_user = false;
15654 $this->_licenses = false;
15655 $this->_license = null;
15656 $this->is_whitelabeled = null;
15657
15658 if ( is_object( $this->_site ) ) {
15659 // Try to fetch user from install.
15660 $this->_user = self::_get_user_by_id( $this->_site->user_id );
15661
15662 if ( ! is_object( $this->_user ) &&
15663 FS_User::is_valid_id( $this->_storage->prev_user_id )
15664 ) {
15665 // Try to fetch previously saved user.
15666 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
15667
15668 if ( ! is_object( $this->_user ) ) {
15669 // Fallback to network's user.
15670 $this->_user = $this->get_network_user();
15671 }
15672 }
15673
15674 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
15675
15676 if ( ! empty( $all_plugin_licenses ) ) {
15677 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
15678 $this->_license = null;
15679 } else {
15680 $license_found = false;
15681 foreach ( $all_plugin_licenses as $license ) {
15682 if ( $license->id == $this->_site->license_id ) {
15683 // License found.
15684 $this->_license = $license;
15685 $license_found = true;
15686 break;
15687 }
15688 }
15689
15690 if ( $license_found ) {
15691 $this->link_license_2_user( $this->_license->id, $this->_user->id );
15692 }
15693 }
15694
15695 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15696 }
15697 }
15698
15699 unset( $this->_site_api );
15700 unset( $this->_user_api );
15701
15702 return true;
15703 }
15704
15705 /**
15706 * Restore the blog context to the blog that originally loaded the module.
15707 *
15708 * @author Vova Feldman (@svovaf)
15709 * @since 2.0.0
15710 */
15711 function restore_current_blog() {
15712 $this->switch_to_blog( $this->_blog_id );
15713 }
15714
15715 /**
15716 * @author Vova Feldman (@svovaf)
15717 * @since 2.0.0
15718 *
15719 * @param array|WP_Site $site
15720 *
15721 * @return int
15722 */
15723 static function get_site_blog_id( &$site ) {
15724 return ( $site instanceof WP_Site ) ?
15725 $site->blog_id :
15726 ( is_object( $site ) && isset( $site->userblog_id ) ?
15727 $site->userblog_id :
15728 $site['blog_id'] );
15729 }
15730
15731 /**
15732 * @author Vova Feldman (@svovaf)
15733 * @since 2.5.1
15734 *
15735 * @param WP_Site[]|array[] $sites
15736 *
15737 * @return int[]
15738 */
15739 static function get_sites_blog_ids( $sites ) {
15740 $blog_ids = array();
15741 foreach ( $sites as $site ) {
15742 $blog_ids[] = self::get_site_blog_id( $site );
15743 }
15744
15745 return $blog_ids;
15746 }
15747
15748 /**
15749 * @author Leo Fajardo (@leorw)
15750 * @since 2.0.0
15751 *
15752 * @param array|WP_Site|null $site
15753 * @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.
15754 *
15755 * @return array
15756 */
15757 function get_site_info( $site = null, $load_registration = false ) {
15758 $this->_logger->entrance();
15759
15760 $switched = false;
15761
15762 $registration_date = null;
15763
15764 if ( is_null( $site ) ) {
15765 $url = self::get_unfiltered_site_url();
15766 $name = get_bloginfo( 'name' );
15767 $blog_id = null;
15768 } else {
15769 $blog_id = self::get_site_blog_id( $site );
15770
15771 if ( get_current_blog_id() != $blog_id ) {
15772 switch_to_blog( $blog_id );
15773 $switched = true;
15774 }
15775
15776 if ( $site instanceof WP_Site ) {
15777 $url = $site->siteurl;
15778 $name = $site->blogname;
15779 $registration_date = $site->registered;
15780 } else {
15781 $url = self::get_unfiltered_site_url( $blog_id );
15782 $name = get_bloginfo( 'name' );
15783 }
15784 }
15785
15786 if ( empty( $registration_date ) && $load_registration ) {
15787 $blog_details = get_blog_details( $blog_id, false );
15788
15789 if ( is_object( $blog_details ) && isset( $blog_details->registered ) ) {
15790 $registration_date = $blog_details->registered;
15791 }
15792 }
15793
15794 $info = array(
15795 'uid' => $this->get_anonymous_id( $blog_id ),
15796 'url' => $url,
15797 );
15798
15799 // Add these diagnostic information only if user allowed to track.
15800 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
15801 $info = array_merge( $info, array(
15802 'title' => $name,
15803 'language' => self::get_sanitized_language(),
15804 ) );
15805 }
15806
15807 if ( is_numeric( $blog_id ) ) {
15808 $info['blog_id'] = $blog_id;
15809 }
15810
15811 if ( ! empty( $registration_date ) ) {
15812 $info[ 'registration_date' ] = $registration_date;
15813 }
15814
15815 if ( $switched ) {
15816 restore_current_blog();
15817 }
15818
15819 return $info;
15820 }
15821
15822 /**
15823 * Load the module's install based on the blog ID.
15824 *
15825 * @author Vova Feldman (@svovaf)
15826 * @since 2.0.0
15827 *
15828 * @param int|null $blog_id
15829 *
15830 * @return FS_Site
15831 */
15832 function get_install_by_blog_id( $blog_id = null ) {
15833 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15834 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15835
15836 if ( is_object( $install ) &&
15837 is_numeric( $install->id ) &&
15838 is_numeric( $install->user_id ) &&
15839 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15840 ) {
15841 // Load site.
15842 $install = clone $install;
15843 }
15844
15845 return $install;
15846 }
15847
15848 /**
15849 * Check if module is installed on a specified site.
15850 *
15851 * @author Vova Feldman (@svovaf)
15852 * @since 2.0.0
15853 *
15854 * @param int|null $blog_id
15855 *
15856 * @return bool
15857 */
15858 function is_installed_on_site( $blog_id = null ) {
15859 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15860 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15861
15862 return (
15863 is_object( $install ) &&
15864 is_numeric( $install->id ) &&
15865 is_numeric( $install->user_id ) &&
15866 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15867 );
15868 }
15869
15870 /**
15871 * Check if super-admin connected at least one site via the network opt-in.
15872 *
15873 * @author Vova Feldman (@svovaf)
15874 * @since 2.0.0
15875 *
15876 * @return bool
15877 */
15878 function is_network_registered() {
15879 if ( ! $this->_is_network_active ) {
15880 return false;
15881 }
15882
15883 return FS_User::is_valid_id( $this->_storage->network_user_id );
15884 }
15885
15886 /**
15887 * Returns the main user associated with the network.
15888 *
15889 * @author Vova Feldman (@svovaf)
15890 * @since 2.0.0
15891 *
15892 * @return FS_User
15893 */
15894 function get_network_user() {
15895 if ( ! $this->_is_network_active ) {
15896 return null;
15897 }
15898
15899 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
15900 self::_get_user_by_id( $this->_storage->network_user_id ) :
15901 null;
15902 }
15903
15904 /**
15905 * Returns the current context user or the network's main user.
15906 *
15907 * @author Vova Feldman (@svovaf)
15908 * @since 2.0.0
15909 *
15910 * @return FS_User
15911 */
15912 function get_current_or_network_user() {
15913 return ( $this->_user instanceof FS_User ) ?
15914 $this->_user :
15915 $this->get_network_user();
15916 }
15917
15918 /**
15919 * Returns the main install associated with the network.
15920 *
15921 * @author Vova Feldman (@svovaf)
15922 * @since 2.0.0
15923 *
15924 * @return FS_Site
15925 */
15926 function get_network_install() {
15927 if ( ! $this->_is_network_active ) {
15928 return null;
15929 }
15930
15931 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15932 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
15933 null;
15934 }
15935
15936 /**
15937 * Returns the blog ID that is associated with the main install.
15938 *
15939 * @author Leo Fajardo (@leorw)
15940 * @since 2.0.0
15941 *
15942 * @return int|null
15943 */
15944 function get_network_install_blog_id() {
15945 if ( ! $this->_is_network_active ) {
15946 return null;
15947 }
15948
15949 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15950 $this->_storage->network_install_blog_id :
15951 null;
15952 }
15953
15954 /**
15955 * Returns the current context install or the network's main install.
15956 *
15957 * @author Vova Feldman (@svovaf)
15958 * @since 2.0.0
15959 *
15960 * @return FS_Site
15961 */
15962 function get_current_or_network_install() {
15963 return ( $this->_site instanceof FS_Site ) ?
15964 $this->_site :
15965 $this->get_network_install();
15966 }
15967
15968 /**
15969 * Check if executing a site level action from the network level admin.
15970 *
15971 * @author Vova Feldman (@svovaf)
15972 * @since 2.0.0
15973 *
15974 * @return false|int If yes, return the requested blog ID.
15975 */
15976 private function is_network_level_site_specific_action() {
15977 if ( ! $this->_is_network_active ) {
15978 return false;
15979 }
15980
15981 if ( ! fs_is_network_admin() ) {
15982 return false;
15983 }
15984
15985 $blog_id = fs_request_get( 'blog_id', '' );
15986
15987 return is_numeric( $blog_id ) ? $blog_id : false;
15988 }
15989
15990 /**
15991 * Check if executing an action from the network level admin.
15992 *
15993 * @author Vova Feldman (@svovaf)
15994 * @since 2.0.0
15995 *
15996 * @return bool
15997 */
15998 private function is_network_level_action() {
15999 return ( $this->_is_network_active && fs_is_network_admin() );
16000 }
16001
16002 /**
16003 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
16004 * 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.
16005 *
16006 * @author Vova Feldman (@svovaf)
16007 * @since 2.0.0
16008 *
16009 * @param int $context_blog_id
16010 */
16011 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
16012 $this->_logger->entrance();
16013
16014 if ( $this->_is_network_active ) {
16015 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
16016 $installs_map = $this->get_blog_install_map();
16017
16018 foreach ( $installs_map as $blog_id => $install ) {
16019 /**
16020 * @var FS_Site $install
16021 */
16022 if ( $context_blog_id == $blog_id ) {
16023 continue;
16024 }
16025
16026 if ( $install->user_id != $this->_storage->network_user_id ) {
16027 continue;
16028 }
16029
16030 // Switch reference to a blog that is opted-in and belong to the same super-admin.
16031 $this->_storage->network_install_blog_id = $blog_id;
16032 break;
16033 }
16034 }
16035 }
16036
16037 if ( ! $this->is_registered() ) {
16038 return;
16039 }
16040
16041 if ( $this->is_sync_cron_scheduled() &&
16042 $context_blog_id == $this->get_sync_cron_blog_id()
16043 ) {
16044 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
16045 }
16046
16047 if ( $this->is_install_sync_scheduled() &&
16048 $context_blog_id == $this->get_install_sync_cron_blog_id()
16049 ) {
16050 $this->maybe_schedule_install_sync_cron( $context_blog_id );
16051 }
16052 }
16053
16054 /**
16055 * Executed after site deactivation, archive, or flag as spam.
16056 *
16057 * @author Vova Feldman (@svovaf)
16058 * @since 2.0.0
16059 *
16060 * @param int $context_blog_id
16061 */
16062 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
16063 $this->_logger->entrance();
16064
16065 $install = $this->get_install_by_blog_id( $context_blog_id );
16066
16067 if ( ! is_object( $install ) ) {
16068 // Site not connected.
16069 return;
16070 }
16071
16072 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16073
16074 if ( ! $this->is_registered() ) {
16075 return;
16076 }
16077
16078 $current_blog_id = get_current_blog_id();
16079
16080 $this->switch_to_blog( $context_blog_id );
16081
16082 // Send deactivation event.
16083 $this->sync_install( array(
16084 'is_active' => false,
16085 ) );
16086
16087 $this->switch_to_blog( $current_blog_id );
16088 }
16089
16090 /**
16091 * Executed after site deletion.
16092 *
16093 * @author Vova Feldman (@svovaf)
16094 * @since 2.0.0
16095 *
16096 * @param int $context_blog_id
16097 * @param bool $drop True if site's database tables should be dropped. Default is false.
16098 */
16099 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
16100 $this->_logger->entrance();
16101
16102 $install = $this->get_install_by_blog_id( $context_blog_id );
16103
16104 if ( ! is_object( $install ) ) {
16105 // Site not connected.
16106 return;
16107 }
16108
16109 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16110
16111 if ( ! $this->is_registered() ) {
16112 return;
16113 }
16114
16115 $current_blog_id = get_current_blog_id();
16116
16117 $this->switch_to_blog( $context_blog_id );
16118
16119 if ( $drop ) {
16120 // Delete install if dropping site DB.
16121 $this->delete_account_event();
16122 } else {
16123 // Send deactivation event.
16124 $this->sync_install( array(
16125 'is_active' => false,
16126 ) );
16127 }
16128
16129 $this->switch_to_blog( $current_blog_id );
16130 }
16131
16132 /**
16133 * Executed after site deletion, called from wp_delete_site
16134 *
16135 * @author Dario Curvino (@dudo)
16136 * @since 2.5.0
16137 *
16138 * @param WP_Site $old_site
16139 */
16140 public function _after_wpsite_deleted_callback( WP_Site $old_site ) {
16141 $this->_logger->entrance();
16142
16143 $this->_after_site_deleted_callback( $old_site->blog_id, true );
16144 }
16145
16146 /**
16147 * Executed after site re-activation.
16148 *
16149 * @author Vova Feldman (@svovaf)
16150 * @since 2.0.0
16151 *
16152 * @param int $context_blog_id
16153 */
16154 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
16155 $this->_logger->entrance();
16156
16157 $install = $this->get_install_by_blog_id( $context_blog_id );
16158
16159 if ( ! is_object( $install ) ) {
16160 // Site not connected.
16161 return;
16162 }
16163
16164 if ( ! self::is_site_active( $context_blog_id ) ) {
16165 // Site not yet active (can be in spam mode, archived, deleted...).
16166 return;
16167 }
16168
16169 $current_blog_id = get_current_blog_id();
16170
16171 $this->switch_to_blog( $context_blog_id );
16172
16173 // Send re-activation event.
16174 $this->sync_install( array(
16175 'is_active' => true,
16176 ) );
16177
16178 $this->switch_to_blog( $current_blog_id );
16179 }
16180
16181 #endregion Multisite
16182
16183 /**
16184 * @author Leo Fajardo (@leorw)
16185 *
16186 * @param string $path
16187 * @param string $scheme
16188 * @param bool $network
16189 *
16190 * @return string
16191 */
16192 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
16193 return ( $this->_is_network_active && $network ) ?
16194 network_admin_url( $path, $scheme ) :
16195 admin_url( $path, $scheme );
16196 }
16197
16198 /**
16199 * Check if currently in a specified admin page.
16200 *
16201 * @author Vova Feldman (@svovaf)
16202 * @since 1.2.2.7
16203 *
16204 * @param string $page
16205 *
16206 * @return bool
16207 */
16208 function is_admin_page( $page ) {
16209 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
16210 }
16211
16212 /**
16213 * Check if currently in the product's main admin page.
16214 *
16215 * @author Vova Feldman (@svovaf)
16216 * @since 2.3.1
16217 *
16218 * @return bool
16219 */
16220 function is_main_admin_page() {
16221 return $this->is_admin_page( '' );
16222 }
16223
16224 /**
16225 * Get module's main admin setting page URL.
16226 *
16227 * @author Vova Feldman (@svovaf)
16228 * @since 1.2.2.7
16229 *
16230 * @return string
16231 */
16232 function main_menu_url() {
16233 return $this->_menu->main_menu_url();
16234 }
16235
16236 /**
16237 * Check if currently on the theme's setting page or
16238 * on any of the Freemius added pages (via tabs).
16239 *
16240 * @author Vova Feldman (@svovaf)
16241 * @since 1.2.2.7
16242 *
16243 * @return bool
16244 *
16245 * @deprecated Please use is_product_settings_page() instead;
16246 */
16247 function is_theme_settings_page() {
16248 return $this->is_product_settings_page();
16249 }
16250
16251 /**
16252 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
16253 *
16254 * @author Vova Feldman (@svovaf)
16255 * @since 1.2.2.7
16256 *
16257 * @return bool
16258 */
16259 function is_product_settings_page() {
16260 $page = fs_request_get( 'page', '', 'get' );
16261 $menu_slug = $this->_menu->get_slug();
16262
16263 if ( $page === $menu_slug ) {
16264 return true;
16265 }
16266
16267 return fs_starts_with(
16268 // e.g., {$menu_slug}-account, {$menu_slug}-affiliation, etc.
16269 $page,
16270 ( $menu_slug . '-' )
16271 );
16272 }
16273
16274 /**
16275 * Plugin's account page + sync license URL.
16276 *
16277 * @author Vova Feldman (@svovaf)
16278 * @since 1.1.9.1
16279 *
16280 * @param bool|number $plugin_id
16281 * @param bool $add_action_nonce
16282 * @param array $params
16283 *
16284 * @return string
16285 */
16286 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
16287 if ( is_numeric( $plugin_id ) ) {
16288 $params['plugin_id'] = $plugin_id;
16289 }
16290
16291 return $this->get_account_url(
16292 $this->get_unique_affix() . '_sync_license',
16293 $params,
16294 $add_action_nonce
16295 );
16296 }
16297
16298 /**
16299 * Plugin's account URL.
16300 *
16301 * @author Vova Feldman (@svovaf)
16302 * @since 1.0.4
16303 *
16304 * @param bool|string $action
16305 * @param array $params
16306 *
16307 * @param bool $add_action_nonce
16308 *
16309 * @return string
16310 */
16311 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
16312 if ( is_string( $action ) ) {
16313 $params['fs_action'] = $action;
16314 }
16315
16316 self::require_pluggable_essentials();
16317
16318 return ( $add_action_nonce && is_string( $action ) ) ?
16319 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
16320 $this->_get_admin_page_url( 'account', $params );
16321 }
16322
16323 /**
16324 * @author Vova Feldman (@svovaf)
16325 * @since 1.2.0
16326 *
16327 * @param string $tab
16328 * @param bool $action
16329 * @param array $params
16330 * @param bool $add_action_nonce
16331 *
16332 * @return string
16333 *
16334 * @uses get_account_url()
16335 */
16336 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
16337 $params['tab'] = $tab;
16338
16339 return $this->get_account_url( $action, $params, $add_action_nonce );
16340 }
16341
16342 /**
16343 * Plugin's account URL.
16344 *
16345 * @author Vova Feldman (@svovaf)
16346 * @since 1.0.4
16347 *
16348 * @param bool|string $topic
16349 * @param bool|string $message
16350 * @param bool|string $summary Since 2.5.1.
16351 *
16352 * @return string
16353 */
16354 function contact_url( $topic = false, $message = false, $summary = false ) {
16355 $params = array();
16356 if ( is_string( $topic ) ) {
16357 $params['topic'] = $topic;
16358 }
16359 if ( is_string( $message ) ) {
16360 $params['message'] = $message;
16361 }
16362
16363 if ( is_string( $summary ) ) {
16364 $params['summary'] = $summary;
16365 }
16366
16367 if ( $this->is_addon() ) {
16368 $params['addon_id'] = $this->get_id();
16369
16370 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
16371 } else {
16372 return $this->_get_admin_page_url( 'contact', $params );
16373 }
16374 }
16375
16376 /**
16377 * Add-on direct info URL.
16378 *
16379 * @author Vova Feldman (@svovaf)
16380 * @since 1.1.0
16381 *
16382 * @param string $slug
16383 *
16384 * @return string
16385 */
16386 function addon_url( $slug ) {
16387 return $this->_get_admin_page_url( 'addons', array(
16388 'slug' => $slug
16389 ) );
16390 }
16391
16392 /**
16393 * Add-ons URL.
16394 *
16395 * @author Vova Feldman (@svovaf)
16396 * @since 2.4.5
16397 *
16398 * @return string
16399 */
16400 function get_addons_url() {
16401 return $this->_get_admin_page_url( 'addons' );
16402 }
16403
16404 /* Logger
16405 ------------------------------------------------------------------------------------------------------------------*/
16406 /**
16407 * @param string $id
16408 * @param bool $prefix_slug
16409 *
16410 * @return FS_Logger
16411 */
16412 function get_logger( $id = '', $prefix_slug = true ) {
16413 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16414 }
16415
16416 /**
16417 * Note: This method is used externally so don't delete it.
16418 *
16419 * @param $id
16420 * @param bool $load_options
16421 * @param bool $prefix_slug
16422 *
16423 * @return FS_Option_Manager
16424 */
16425 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16426 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16427 }
16428
16429 /* Security
16430 ------------------------------------------------------------------------------------------------------------------*/
16431 private static function _encrypt( $str ) {
16432 if ( is_null( $str ) ) {
16433 return null;
16434 }
16435
16436 /**
16437 * The encrypt/decrypt functions are used to protect
16438 * the user from messing up with some of the sensitive
16439 * data stored for the module as a JSON in the database.
16440 *
16441 * I used the same suggested hack by the theme review team.
16442 * For more details, look at the function `Base64UrlDecode()`
16443 * in `./sdk/FreemiusBase.php`.
16444 *
16445 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16446 *
16447 * @author Vova Feldman (@svovaf)
16448 * @since 1.2.2
16449 */
16450 $fn = 'base64' . '_encode';
16451
16452 return $fn( $str );
16453 }
16454
16455 static function _decrypt( $str ) {
16456 if ( is_null( $str ) ) {
16457 return null;
16458 }
16459
16460 /**
16461 * The encrypt/decrypt functions are used to protect
16462 * the user from messing up with some of the sensitive
16463 * data stored for the module as a JSON in the database.
16464 *
16465 * I used the same suggested hack by the theme review team.
16466 * For more details, look at the function `Base64UrlDecode()`
16467 * in `./sdk/FreemiusBase.php`.
16468 *
16469 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16470 *
16471 * @author Vova Feldman (@svovaf)
16472 * @since 1.2.2
16473 */
16474 $fn = 'base64' . '_decode';
16475
16476 return $fn( $str );
16477 }
16478
16479 /**
16480 * @author Vova Feldman (@svovaf)
16481 * @since 1.0.5
16482 *
16483 * @param FS_Entity $entity
16484 *
16485 * @return FS_Entity Return an encrypted clone entity.
16486 */
16487 private static function _encrypt_entity( FS_Entity $entity ) {
16488 $clone = clone $entity;
16489 $props = get_object_vars( $entity );
16490
16491 foreach ( $props as $key => $val ) {
16492 $clone->{$key} = self::_encrypt( $val );
16493 }
16494
16495 return $clone;
16496 }
16497
16498 /**
16499 * @author Vova Feldman (@svovaf)
16500 * @since 1.0.5
16501 *
16502 * @param FS_Entity $entity
16503 *
16504 * @return FS_Entity Return an decrypted clone entity.
16505 */
16506 private static function decrypt_entity( FS_Entity $entity ) {
16507 $clone = clone $entity;
16508 $props = get_object_vars( $entity );
16509
16510 foreach ( $props as $key => $val ) {
16511 $clone->{$key} = self::_decrypt( $val );
16512 }
16513
16514 return $clone;
16515 }
16516
16517 /**
16518 * @author Vova Feldman (@svovaf)
16519 * @since 1.0.7
16520 *
16521 * @param string $email
16522 *
16523 * @return FS_User|false
16524 */
16525 public static function _get_user_by_email( $email ) {
16526 self::$_static_logger->entrance();
16527
16528 $email = trim( strtolower( $email ) );
16529
16530 $users = self::get_all_users();
16531
16532 if ( is_array( $users ) ) {
16533 foreach ( $users as $user ) {
16534 if ( $email === trim( strtolower( $user->email ) ) ) {
16535 return $user;
16536 }
16537 }
16538 }
16539
16540 return false;
16541 }
16542
16543 #----------------------------------------------------------------------------------
16544 #region Account (Loading, Updates & Activation)
16545 #----------------------------------------------------------------------------------
16546
16547 /***
16548 * Load account information (user + site).
16549 *
16550 * @author Vova Feldman (@svovaf)
16551 * @since 1.0.1
16552 */
16553 private function _load_account() {
16554 $this->_logger->entrance();
16555
16556 $this->do_action( 'before_account_load' );
16557
16558 $users = self::get_all_users();
16559 $plans = self::get_all_plans( $this->_module_type );
16560
16561 if ( $this->_logger->is_on() && is_admin() ) {
16562 $this->_logger->log( 'users = ' . var_export( $users, true ) );
16563 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
16564 }
16565
16566 $site = fs_is_network_admin() ?
16567 $this->get_network_install() :
16568 $this->get_install_by_blog_id();
16569
16570 if ( fs_is_network_admin() &&
16571 $this->is_network_active() &&
16572 ! is_object( $site ) &&
16573 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
16574 ) {
16575 $first_install = $this->find_first_install();
16576
16577 if ( is_null( $first_install ) ) {
16578 unset( $this->_storage->network_install_blog_id );
16579 } else {
16580 $site = $first_install['install'];
16581 $this->_storage->network_install_blog_id = $first_install['blog_id'];
16582 }
16583 }
16584
16585 if ( is_object( $site ) &&
16586 is_numeric( $site->id ) &&
16587 is_numeric( $site->user_id ) &&
16588 FS_Plugin_Plan::is_valid_id( $site->plan_id )
16589 ) {
16590 // Load site.
16591 $this->_site = $site;
16592 }
16593
16594 $user = null;
16595 if ( fs_is_network_admin() && $this->_is_network_active ) {
16596 $user = $this->get_network_user();
16597 }
16598
16599 if ( is_object( $user ) ) {
16600 $this->_user = clone $user;
16601 } else if ( $this->_site ) {
16602 $user = self::_get_user_by_id( $this->_site->user_id );
16603
16604 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
16605 /**
16606 * Try to load the previous owner. This recovery is used for the following use-case:
16607 * 1. Opt-in
16608 * 2. Cloning site1 to site2
16609 * 3. Ownership switch in site1 (same applies for site2)
16610 * 4. Install data sync on site2
16611 * 5. Now site2's install is associated with the new owner which does not exists locally.
16612 */
16613 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
16614 }
16615
16616 if ( ! is_object( $user ) ) {
16617 /**
16618 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
16619 */
16620 if (
16621 ! isset( $this->_storage->user_recovery_from_install_last_attempt_timestamp ) ||
16622 time() > ( $this->_storage->user_recovery_from_install_last_attempt_timestamp + FS_Clone_Manager::CLONE_RESOLUTION_MAX_EXECUTION_TIME )
16623 ) {
16624 $user = $this->sync_user_by_current_install();
16625 } else {
16626 return;
16627 }
16628
16629 if ( is_object( $user ) ) {
16630 $this->_storage->user_was_recovered_from_install = true;
16631 } else {
16632 $this->_storage->user_recovery_from_install_attempts = isset( $this->_storage->user_recovery_from_install_attempts ) ?
16633 ( $this->_storage->user_recovery_from_install_attempts + 1 ) :
16634 1;
16635
16636 if ( $this->_storage->user_recovery_from_install_attempts >= 3 ) {
16637 $this->delete_current_install( false );
16638 } else {
16639 $this->_storage->user_recovery_from_install_last_attempt_timestamp = time();
16640
16641 return;
16642 }
16643 }
16644 }
16645
16646 $this->_user = ( $user instanceof FS_User ) ?
16647 clone $user :
16648 null;
16649 }
16650
16651 if ( is_object( $this->_user ) ) {
16652 // Load licenses.
16653 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16654 }
16655
16656 if ( is_object( $this->_site ) ) {
16657 // Load plans.
16658 $this->_plans = isset( $plans[ $this->_slug ] ) ?
16659 $plans[ $this->_slug ] :
16660 array();
16661
16662 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
16663 $this->_sync_plans();
16664 } else {
16665 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16666 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
16667 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
16668 } else {
16669 unset( $this->_plans[ $i ] );
16670 }
16671 }
16672 }
16673
16674 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
16675
16676 if ( $this->_site->version != $this->get_plugin_version() ) {
16677 // If stored install version is different than current installed plugin version,
16678 // then update plugin version event.
16679 $this->update_plugin_version_event();
16680 }
16681 }
16682
16683 if ( true === $this->_storage->require_license_activation &&
16684 ! fs_request_get_bool( 'require_license', true )
16685 ) {
16686 $this->_storage->require_license_activation = false;
16687 }
16688
16689 if ( $this->is_theme() ) {
16690 $this->_register_account_hooks();
16691 }
16692
16693 if ( $this->is_user_in_admin() && $this->is_clone() ) {
16694 if ( empty( FS_Clone_Manager::instance()->get_clone_identification_timestamp() ) ) {
16695 FS_Clone_Manager::instance()->store_clone_identification_timestamp();
16696 }
16697
16698 FS_Clone_Manager::instance()->maybe_update_clone_resolution_support_flag( $this->_storage->sdk_last_version );
16699 $this->send_pending_clone_update_once();
16700 }
16701 }
16702
16703 /**
16704 * Special user recovery mechanism.
16705 *
16706 * @author Vova Feldman (@svovaf)
16707 * @since 2.0.0
16708 *
16709 * @param number|null $site_user_id
16710 *
16711 * @return \FS_User|mixed
16712 */
16713 private function sync_user_by_current_install( $site_user_id = null ) {
16714 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
16715 $site_user_id :
16716 $this->_site->user_id;
16717
16718 $api = $this->get_api_site_scope();
16719
16720 $uid = $this->get_anonymous_id();
16721 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
16722
16723 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
16724
16725 if ( $this->is_api_result_entity( $result ) ) {
16726 $user = new FS_User( $result );
16727 $this->_user = $user;
16728 $this->_store_user();
16729
16730 return $user;
16731 }
16732
16733 $error_code = FS_Api::get_error_code( $result );
16734
16735 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
16736 /**
16737 * Those API errors will continue coming and are not recoverable with the
16738 * current site's data. Therefore, extend the API call's cached result to 7 days.
16739 */
16740 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
16741 }
16742
16743 return $result;
16744 }
16745
16746 /**
16747 * @author Vova Feldman (@svovaf)
16748 * @since 1.0.1
16749 *
16750 * @param FS_User $user
16751 * @param FS_Site $site
16752 * @param bool|array $plans
16753 */
16754 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
16755 $site->user_id = $user->id;
16756
16757 $this->_site = $site;
16758 $this->_user = $user;
16759 if ( false !== $plans ) {
16760 $this->_plans = $plans;
16761 }
16762
16763 $this->send_install_update();
16764
16765 $this->_store_account();
16766
16767 }
16768
16769 /**
16770 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
16771 * Each version is trimmed after the 16th char.
16772 *
16773 * @author Vova Feldman (@svovaf)
16774 * @since 2.2.1
16775 *
16776 * @return array
16777 */
16778 private function get_versions() {
16779 $versions = array();
16780 $versions['sdk_version'] = $this->version;
16781
16782 // Collect these diagnostic information only if it's allowed.
16783 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
16784 $versions['platform_version'] = get_bloginfo( 'version' );
16785 $versions['programming_language_version'] = phpversion();
16786 }
16787
16788 foreach ( $versions as $k => $version ) {
16789 $versions[ $k ] = self::get_api_sanitized_property( $k, $version );
16790 }
16791
16792 return $versions;
16793 }
16794
16795 /**
16796 * Get sanitized site language.
16797 *
16798 * @param string $language
16799 * @param int $max_len
16800 *
16801 * @since 2.5.1
16802 * @author Vova Feldman (@svovaf)
16803 *
16804 * @return string
16805 */
16806 private static function get_sanitized_language( $language = '', $max_len = self::LANGUAGE_MAX_CHARS ) {
16807 if ( empty( $language ) ) {
16808 $language = get_bloginfo( 'language' );
16809 }
16810
16811 return substr( $language, 0, $max_len );
16812 }
16813
16814 /**
16815 * Get core version stripped from pre-release and build.
16816 *
16817 * @since 2.5.1
16818 * @author Vova Feldman (@svovaf)
16819 *
16820 * @param string $version
16821 * @param int $parts
16822 * @param int $max_len
16823 * @param bool $include_pre_release
16824 *
16825 * @return string
16826 */
16827 private static function get_core_version(
16828 $version,
16829 $parts = 3,
16830 $max_len = self::VERSION_MAX_CHARS,
16831 $include_pre_release = false
16832 ) {
16833 if ( empty( $version ) ) {
16834 // Version is empty.
16835 return '';
16836 }
16837
16838 if ( is_numeric( $version ) ) {
16839 $is_float_version = is_float( $version );
16840
16841 $version = (string) $version;
16842
16843 /**
16844 * 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.
16845 */
16846 if ( $is_float_version && false === strpos( $version, '.' ) ) {
16847 $version .= '.0';
16848 }
16849 }
16850
16851 if ( ! is_string( $version ) ) {
16852 return '';
16853 }
16854
16855 if ( $parts < 1 ) {
16856 return '';
16857 }
16858
16859 $pre_release_regex = $include_pre_release ?
16860 '(\-(alpha|beta|RC)([0-9]+)?)?' :
16861 '';
16862
16863 if ( 0 === preg_match( '/^([0-9]+(\.[0-9]+){0,' . ( $parts - 1 ) . '}' . $pre_release_regex . ')/i', $version, $matches ) ) {
16864 // Version is not starting with a digit.
16865 return '';
16866 }
16867
16868 return substr( $matches[1], 0, $max_len );
16869 }
16870
16871 /**
16872 * @param string $prop
16873 * @param mixed $val
16874 *
16875 * @return mixed
16876 *@author Vova Feldman (@svovaf)
16877 *
16878 * @since 2.5.1
16879 */
16880 private static function get_api_sanitized_property( $prop, $val ) {
16881 if ( ! is_string( $val ) || empty( $val ) ) {
16882 return $val;
16883 }
16884
16885 switch ( $prop ) {
16886 case 'programming_language_version':
16887 // Get core PHP version, which can have up to 3 parts (ignore pre-releases).
16888 return self::get_core_version( $val );
16889 case 'platform_version':
16890 // Get the exact WordPress version, which can have up to 3 parts (including pre-releases).
16891 return self::get_core_version( $val, 3, self::VERSION_MAX_CHARS, true );
16892 case 'sdk_version':
16893 // Get the exact SDK version, which can have up to 4 parts.
16894 return self::get_core_version( $val, 4 );
16895 case 'version':
16896 // Get the entire version but just limited in length.
16897 return substr( $val, 0, self::VERSION_MAX_CHARS );
16898 case 'language':
16899 return self::get_sanitized_language( $val );
16900 default:
16901 return $val;
16902 }
16903 }
16904
16905 /**
16906 * @author Leo Fajardo (@leorw)
16907 * @since 2.3.0
16908 *
16909 * @return bool
16910 */
16911 function has_beta_update() {
16912 return (
16913 ! empty( $this->_storage->beta_data ) &&
16914 ( true === $this->_storage->beta_data['is_beta'] ) &&
16915 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
16916 );
16917 }
16918
16919 /**
16920 * @author Leo Fajardo (@leorw)
16921 * @since 2.3.0
16922 *
16923 * @return bool
16924 */
16925 function is_beta() {
16926 return (
16927 ! empty( $this->_storage->beta_data ) &&
16928 ( true === $this->_storage->beta_data['is_beta'] ) &&
16929 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
16930 );
16931 }
16932
16933 /**
16934 * @author Vova Feldman (@svovaf)
16935 * @since 1.1.7.4
16936 *
16937 * @param array $override_with
16938 * @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.
16939 *
16940 * @return array
16941 */
16942 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
16943 $this->_logger->entrance();
16944
16945 $current_user = self::_get_current_wp_user();
16946
16947 $activation_action = $this->get_unique_affix() . '_activate_new';
16948 $return_url = $this->is_anonymous() ?
16949 // If skipped already, then return to the account page.
16950 $this->get_account_url( $activation_action, array(), false ) :
16951 // Return to the module's main page.
16952 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
16953
16954 $versions = $this->get_versions();
16955
16956 $params = array_merge( $versions, array(
16957 'user_firstname' => $current_user->user_firstname,
16958 'user_lastname' => $current_user->user_lastname,
16959 'user_email' => $current_user->user_email,
16960 'plugin_slug' => $this->_slug,
16961 'plugin_id' => $this->get_id(),
16962 'plugin_public_key' => $this->get_public_key(),
16963 'plugin_version' => $this->get_plugin_version(),
16964 'return_url' => fs_nonce_url( $return_url, $activation_action ),
16965 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
16966 'account',
16967 array( 'fs_action' => 'sync_user' )
16968 ), 'sync_user' ),
16969 'is_premium' => $this->is_premium(),
16970 'is_active' => true,
16971 'is_uninstalled' => false,
16972 'is_localhost' => WP_FS__IS_LOCALHOST,
16973 ) );
16974
16975 if ( $this->is_addon() ) {
16976 $parent_fs = $this->get_parent_instance();
16977
16978 $params['parent_plugin_slug'] = $parent_fs->_slug;
16979 $params['parent_plugin_id'] = $parent_fs->get_id();
16980 }
16981
16982 if ( true === $network_level_or_blog_id ) {
16983 if ( ! isset( $override_with['sites'] ) ) {
16984 $params['sites'] = $this->get_sites_for_network_level_optin();
16985 }
16986 } else {
16987 $site = is_numeric( $network_level_or_blog_id ) ?
16988 array( 'blog_id' => $network_level_or_blog_id ) :
16989 null;
16990
16991 $site = $this->get_site_info( $site );
16992
16993 $diagnostic_info = array();
16994 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
16995 $diagnostic_info = array(
16996 'site_name' => $site['title'],
16997 'language' => self::get_sanitized_language( $site['language'] ),
16998 );
16999 }
17000
17001 $params = array_merge( $params, $diagnostic_info, array(
17002 'site_uid' => $site['uid'],
17003 'site_url' => $site['url'],
17004 ) );
17005 }
17006
17007 if ( $this->is_pending_activation() &&
17008 ! empty( $this->_storage->pending_license_key )
17009 ) {
17010 $params['license_key'] = $this->_storage->pending_license_key;
17011 }
17012
17013 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
17014 // Even though rand() is known for its security issues,
17015 // the timestamp adds another layer of protection.
17016 // It would be very hard for an attacker to get the secret key form here.
17017 // Plus, this should never run in production since the secret should never
17018 // be included in the production version.
17019 $params['ts'] = WP_FS__SCRIPT_START_TIME;
17020 $params['salt'] = md5( uniqid( rand() ) );
17021 $params['secure'] = md5(
17022 $params['ts'] .
17023 $params['salt'] .
17024 $this->get_secret_key()
17025 );
17026 }
17027
17028 if ( is_multisite() && function_exists( 'get_network' ) ) {
17029 $params['network_uid'] = $this->get_anonymous_network_id();
17030 }
17031
17032 return array_merge( $params, $override_with );
17033 }
17034
17035 /**
17036 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
17037 * 2. If there was an API error, return the API result.
17038 *
17039 * @author Vova Feldman (@svovaf)
17040 * @since 1.1.7.4
17041 *
17042 * @param string|bool $email
17043 * @param string|bool $first
17044 * @param string|bool $last
17045 * @param string|bool $license_key
17046 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
17047 * In this case, the user and site info will be sent to the server but no
17048 * data will be saved to the WP installation's database.
17049 * @param number|bool $trial_plan_id
17050 * @param bool $is_disconnected Whether to opt in without tracking.
17051 * @param null|bool $is_marketing_allowed
17052 * @param array $sites If network-level opt-in, an array of containing details of sites.
17053 * @param bool $redirect
17054 *
17055 * @return string|object
17056 * @use WP_Error
17057 */
17058 function opt_in(
17059 $email = false,
17060 $first = false,
17061 $last = false,
17062 $license_key = false,
17063 $is_uninstall = false,
17064 $trial_plan_id = false,
17065 $is_disconnected = false,
17066 $is_marketing_allowed = null,
17067 $sites = array(),
17068 $redirect = true
17069 ) {
17070 $this->_logger->entrance();
17071
17072 if ( false === $email ) {
17073 $current_user = self::_get_current_wp_user();
17074 $email = $current_user->user_email;
17075 }
17076
17077 /**
17078 * @since 1.2.1 If activating with license key, ignore the context-user
17079 * since the user will be automatically loaded from the license.
17080 */
17081 if ( empty( $license_key ) ) {
17082 // Clean up pending license if opt-ing in again.
17083 $this->_storage->remove( 'pending_license_key' );
17084
17085 if ( ! $is_uninstall ) {
17086 $fs_user = Freemius::_get_user_by_email( $email );
17087 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
17088 return $this->install_with_user(
17089 $fs_user,
17090 false,
17091 $trial_plan_id,
17092 $redirect,
17093 true,
17094 $sites
17095 );
17096 }
17097 }
17098 }
17099
17100 $user_info = array();
17101 if ( ! empty( $email ) ) {
17102 $user_info['user_email'] = $email;
17103 }
17104 if ( ! empty( $first ) ) {
17105 $user_info['user_firstname'] = $first;
17106 }
17107 if ( ! empty( $last ) ) {
17108 $user_info['user_lastname'] = $last;
17109 }
17110
17111 if ( ! empty( $sites ) ) {
17112 $is_network = true;
17113
17114 $user_info['sites'] = $sites;
17115 } else {
17116 $is_network = false;
17117 }
17118
17119 $params = $this->get_opt_in_params( $user_info, $is_network );
17120
17121 $filtered_license_key = false;
17122 if ( is_string( $license_key ) ) {
17123 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
17124 $params['license_key'] = $filtered_license_key;
17125 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17126 $params['trial_plan_id'] = $trial_plan_id;
17127 }
17128
17129 if ( $is_uninstall ) {
17130 $params['uninstall_params'] = array(
17131 'reason_id' => $this->_storage->uninstall_reason->id,
17132 'reason_info' => $this->_storage->uninstall_reason->info
17133 );
17134 }
17135
17136 if ( isset( $params['license_key'] ) ) {
17137 $fs_user = Freemius::_get_user_by_email( $email );
17138
17139 if ( is_object( $fs_user ) ) {
17140 /**
17141 * If opting in with a context license and the context WP Admin user already opted in
17142 * before from the current site, add the user context security params to avoid the
17143 * unnecessary email activation when the context license is owned by the same context user.
17144 *
17145 * @author Leo Fajardo (@leorw)
17146 * @since 1.2.3
17147 */
17148 $params = array_merge( $params, FS_Security::instance()->get_context_params(
17149 $fs_user,
17150 false,
17151 'install_with_existing_user'
17152 ) );
17153 }
17154 }
17155
17156 if ( is_bool( $is_marketing_allowed ) ) {
17157 $params['is_marketing_allowed'] = $is_marketing_allowed;
17158 }
17159
17160 $params['is_disconnected'] = $is_disconnected;
17161 $params['format'] = 'json';
17162 $params['is_extensions_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed();
17163 $params['is_diagnostic_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed();
17164
17165 $request = array(
17166 'method' => 'POST',
17167 'body' => $params,
17168 'timeout' => 60,
17169 );
17170
17171 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
17172 $response = self::safe_remote_post( $url, $request );
17173
17174 if ( is_wp_error( $response ) ) {
17175 /**
17176 * @var WP_Error $response
17177 */
17178 $result = new stdClass();
17179
17180 $error_code = $response->get_error_code();
17181 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
17182
17183 $result->error = (object) array(
17184 'type' => $error_type,
17185 'message' => $response->get_error_message(),
17186 'code' => $error_code,
17187 'http' => 402
17188 );
17189
17190 $this->maybe_modify_api_curl_error_message( $result );
17191
17192 if ( FS_Api::is_blocked( $result ) ) {
17193 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
17194 }
17195
17196 $is_connected = null;
17197
17198 if ( empty( $license_key ) && $this->is_enable_anonymous() ) {
17199 $this->skip_connection( fs_is_network_admin() );
17200
17201 $is_connected = ( ! FS_Api::is_blocked( $result ) );
17202 }
17203
17204 $this->update_connectivity_info( $is_connected );
17205
17206 return $result;
17207 }
17208
17209 $this->update_connectivity_info( true );
17210
17211 // Module is being uninstalled, don't handle the returned data.
17212 if ( $is_uninstall ) {
17213 return true;
17214 }
17215
17216 /**
17217 * 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.
17218 *
17219 * @author Vova Feldman (@svovaf)
17220 * @since 1.2.3
17221 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
17222 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
17223 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
17224 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
17225 */
17226 $decoded = is_string( $response['body'] ) ?
17227 json_decode( $response['body'] ) :
17228 null;
17229
17230 if ( empty( $decoded ) ) {
17231 return false;
17232 }
17233
17234 if ( ! $this->is_api_result_object( $decoded ) ) {
17235 if ( ! empty( $params['license_key'] ) ) {
17236 // Pass the fully entered license key to the failure handler.
17237 $params['license_key'] = $license_key;
17238 }
17239
17240 return $is_uninstall ?
17241 $decoded :
17242 $this->apply_filters( 'after_install_failure', $decoded, $params );
17243 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
17244 if ( $is_network ) {
17245 $site_ids = array();
17246 foreach ( $sites as $site ) {
17247 $site_ids[] = $site['blog_id'];
17248 }
17249
17250 /**
17251 * Store the sites so that they can be installed once the user has clicked on the activation link
17252 * in the email.
17253 *
17254 * @author Leo Fajardo (@leorw)
17255 */
17256 $this->_storage->pending_sites_info = array(
17257 'blog_ids' => $site_ids,
17258 'license_key' => $license_key,
17259 'trial_plan_id' => $trial_plan_id
17260 );
17261 }
17262
17263 // Pending activation, add message.
17264 return $this->set_pending_confirmation(
17265 ( isset( $decoded->email ) ?
17266 $decoded->email :
17267 true ),
17268 false,
17269 $filtered_license_key,
17270 ! empty( $params['trial_plan_id'] ),
17271 isset( $decoded->is_suspicious_email ) && $decoded->is_suspicious_email
17272 );
17273 } else if ( isset( $decoded->install_secret_key ) ) {
17274 return $this->install_with_new_user(
17275 $decoded->user_id,
17276 $decoded->user_public_key,
17277 $decoded->user_secret_key,
17278 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17279 $decoded->is_marketing_allowed :
17280 null ),
17281 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17282 $decoded->is_extensions_tracking_allowed :
17283 null ),
17284 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17285 $decoded->is_diagnostic_tracking_allowed :
17286 null ),
17287 $decoded->install_id,
17288 $decoded->install_public_key,
17289 $decoded->install_secret_key,
17290 false
17291 );
17292 } else if ( is_array( $decoded->installs ) ) {
17293 return $this->install_many_with_new_user(
17294 $decoded->user_id,
17295 $decoded->user_public_key,
17296 $decoded->user_secret_key,
17297 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17298 $decoded->is_marketing_allowed :
17299 null ),
17300 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17301 $decoded->is_extensions_tracking_allowed :
17302 null ),
17303 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17304 $decoded->is_diagnostic_tracking_allowed :
17305 null ),
17306 $decoded->installs,
17307 false
17308 );
17309 }
17310
17311 return $decoded;
17312 }
17313
17314 /**
17315 * Set user and site identities.
17316 *
17317 * @author Vova Feldman (@svovaf)
17318 * @since 1.0.9
17319 *
17320 * @param FS_User $user
17321 * @param FS_Site $site
17322 * @param bool $redirect
17323 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
17324 * redirect (or return a URL) to the account page with a special parameter to
17325 * trigger the auto installation processes.
17326 *
17327 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17328 */
17329 function setup_account(
17330 FS_User $user,
17331 FS_Site $site,
17332 $redirect = true,
17333 $auto_install = false
17334 ) {
17335 return $this->setup_network_account(
17336 $user,
17337 array( $site ),
17338 $redirect,
17339 $auto_install,
17340 false
17341 );
17342 }
17343
17344 /**
17345 * Set user and site identities.
17346 *
17347 * @author Vova Feldman (@svovaf)
17348 * @since 2.0.0
17349 *
17350 * @param FS_User $user
17351 * @param FS_Site[] $installs
17352 * @param bool $redirect
17353 * @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.
17354 * @param bool $is_network_level_opt_in
17355 *
17356 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17357 */
17358 function setup_network_account(
17359 FS_User $user,
17360 array $installs,
17361 $redirect = true,
17362 $auto_install = false,
17363 $is_network_level_opt_in = true
17364 ) {
17365 $first_install = $installs[0];
17366
17367 $this->_user = $user;
17368 $this->_site = $first_install;
17369
17370 $this->_sync_plans();
17371
17372 if ( $this->_storage->handle_gdpr_admin_notice &&
17373 $this->should_handle_gdpr_admin_notice() &&
17374 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
17375 ) {
17376 /**
17377 * Clear user lock after an opt-in.
17378 */
17379 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
17380 FS_User_Lock::instance()->unlock();
17381 }
17382
17383 if ( 1 < count( $installs ) ) {
17384 // Only network level opt-in can have more than one install.
17385 $is_network_level_opt_in = true;
17386 }
17387
17388 $this->update_connectivity_info( true );
17389
17390 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
17391 // If Freemius was OFF before, turn it on.
17392 $this->turn_on();
17393
17394 $this->handle_account_connection(
17395 $installs,
17396 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
17397 );
17398
17399 if ( is_numeric( $first_install->license_id ) ) {
17400 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
17401 }
17402
17403 $this->_admin_notices->remove_sticky( 'connect_account' );
17404
17405 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
17406 $this->clear_pending_activation_mode();
17407
17408 if ( ! $this->is_paying_or_trial() ) {
17409 $this->_admin_notices->add_sticky(
17410 sprintf( $this->get_text_inline( '%s opt-in was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
17411 'activation_complete'
17412 );
17413 }
17414 }
17415
17416 if ( $this->is_paying_or_trial() ) {
17417 if ( ! $this->is_premium() ||
17418 ! $this->has_premium_version() ||
17419 ! $this->has_settings_menu()
17420 ) {
17421 if ( $this->is_paying() ) {
17422 $this->add_complete_upgrade_instructions_notice(
17423 sprintf(
17424 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
17425 $this->get_plan_title()
17426 ),
17427 'plan_upgraded'
17428 );
17429 } else {
17430 $trial_plan = $this->get_trial_plan();
17431
17432 $this->add_complete_upgrade_instructions_notice(
17433 sprintf(
17434 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
17435 '<i>' . $this->get_plugin_name() . '</i>'
17436 ),
17437 'trial_started',
17438 $trial_plan->title
17439 );
17440 }
17441 }
17442
17443 $this->_admin_notices->remove_sticky( array(
17444 'trial_promotion',
17445 ) );
17446 }
17447
17448 $plugin_id = fs_request_get( 'plugin_id', false );
17449
17450 // Store activation time ONLY for plugins & themes (not add-ons).
17451 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
17452 if ( empty( $this->_storage->activation_timestamp ) ) {
17453 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
17454 }
17455 }
17456
17457 $next_page = '';
17458
17459 $extra = array();
17460 if ( $auto_install ) {
17461 $extra['auto_install'] = 'true';
17462 }
17463
17464 if ( is_numeric( $plugin_id ) ) {
17465 /**
17466 * @author Leo Fajardo (@leorw)
17467 * @since 1.2.1.6
17468 *
17469 * Also sync the license after an anonymous user subscribes.
17470 */
17471 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
17472 // Add-on was installed - sync license right after install.
17473 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
17474 }
17475 } else {
17476 /**
17477 * @author Vova Feldman (@svovaf)
17478 * @since 1.1.9 If site installed with a valid license, sync license.
17479 */
17480 if ( $this->is_paying() ) {
17481 $this->_sync_plugin_license(
17482 true,
17483 // Installs data is already synced in the beginning of this method directly or via _set_account().
17484 false
17485 );
17486 }
17487
17488 // Reload the page with the keys.
17489 $next_page = $this->is_anonymous() ?
17490 // If user previously skipped, redirect to account page.
17491 $this->get_account_url( false, $extra ) :
17492 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
17493 }
17494
17495 if ( ! empty( $next_page ) && $redirect ) {
17496 fs_redirect( $next_page );
17497 }
17498
17499 return $next_page;
17500 }
17501
17502 /**
17503 * Install plugin with new user information after approval.
17504 *
17505 * @author Vova Feldman (@svovaf)
17506 * @since 1.0.7
17507 */
17508 function _install_with_new_user() {
17509 $this->_logger->entrance();
17510
17511 if ( $this->is_registered() ) {
17512 return;
17513 }
17514
17515 $has_pending_activation_confirmation_param = fs_request_has( 'pending_activation' );
17516
17517 $this->update_license_required_permissions_if_anonymous();
17518
17519 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17520 // @todo This logic should be improved because it's executed on every load of a theme.
17521 $this->is_theme()
17522 ) {
17523 // check_admin_referer( $this->_slug . '_activate_new' );
17524
17525 if ( fs_request_has( 'user_secret_key' ) ) {
17526 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17527 $pending_sites_info = $this->_storage->pending_sites_info;
17528
17529 $this->install_many_pending_with_user(
17530 fs_request_get( 'user_id' ),
17531 fs_request_get_raw( 'user_public_key' ),
17532 fs_request_get_raw( 'user_secret_key' ),
17533 fs_request_get_bool( 'is_marketing_allowed', null ),
17534 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17535 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17536 $pending_sites_info['blog_ids'],
17537 $pending_sites_info['license_key'],
17538 $pending_sites_info['trial_plan_id']
17539 );
17540 } else {
17541 $this->install_with_new_user(
17542 fs_request_get( 'user_id' ),
17543 fs_request_get_raw( 'user_public_key' ),
17544 fs_request_get_raw( 'user_secret_key' ),
17545 fs_request_get_bool( 'is_marketing_allowed', null ),
17546 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17547 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17548 fs_request_get( 'install_id' ),
17549 fs_request_get_raw( 'install_public_key' ),
17550 fs_request_get_raw( 'install_secret_key' ),
17551 true,
17552 fs_request_get_bool( 'auto_install' )
17553 );
17554 }
17555 } else if ( $has_pending_activation_confirmation_param ) {
17556 $this->set_pending_confirmation(
17557 fs_request_get( 'user_email' ),
17558 true,
17559 false,
17560 false,
17561 fs_request_get_bool( 'is_suspicious_email' ),
17562 fs_request_get_bool( 'has_upgrade_context' ),
17563 fs_request_get( 'support_email_address' )
17564 );
17565 }
17566 }
17567 }
17568
17569 /**
17570 * @author Vova Feldman (@svovaf)
17571 * @since 2.0.0
17572 *
17573 * @param number $id
17574 * @param string $public_key
17575 * @param string $secret_key
17576 *
17577 * @return \FS_User
17578 */
17579 private function setup_user( $id, $public_key, $secret_key ) {
17580 $user = self::_get_user_by_id( $id );
17581
17582 if ( is_object( $user ) ) {
17583 $this->_user = $user;
17584 } else {
17585 $user = new FS_User();
17586 $user->id = $id;
17587 $user->public_key = $public_key;
17588 $user->secret_key = $secret_key;
17589
17590 $this->_user = $user;
17591 $user_result = $this->get_api_user_scope()->get();
17592 $user = new FS_User( $user_result );
17593
17594 $this->_user = $user;
17595 $this->_store_user();
17596 }
17597
17598 return $user;
17599 }
17600
17601 /**
17602 * Install plugin with new user.
17603 *
17604 * @author Vova Feldman (@svovaf)
17605 * @since 1.1.7.4
17606 *
17607 * @param number $user_id
17608 * @param string $user_public_key
17609 * @param string $user_secret_key
17610 * @param bool|null $is_marketing_allowed
17611 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17612 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17613 * @param number $install_id
17614 * @param string $install_public_key
17615 * @param string $install_secret_key
17616 * @param bool $redirect
17617 * @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.
17618 *
17619 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17620 */
17621 private function install_with_new_user(
17622 $user_id,
17623 $user_public_key,
17624 $user_secret_key,
17625 $is_marketing_allowed,
17626 $is_extensions_tracking_allowed,
17627 $is_diagnostic_tracking_allowed,
17628 $install_id,
17629 $install_public_key,
17630 $install_secret_key,
17631 $redirect = true,
17632 $auto_install = false
17633 ) {
17634 /**
17635 * This method is also executed after opting in with a license key since the
17636 * license can be potentially associated with a different owner.
17637 *
17638 * @since 2.0.0
17639 */
17640 $user = self::_get_user_by_id( $user_id );
17641
17642 if ( ! is_object( $user ) ) {
17643 $user = new FS_User();
17644 $user->id = $user_id;
17645 $user->public_key = $user_public_key;
17646 $user->secret_key = $user_secret_key;
17647
17648 $this->_user = $user;
17649 $user_result = $this->get_api_user_scope()->get();
17650 $user = new FS_User( $user_result );
17651 }
17652
17653 $this->_user = $user;
17654
17655 $site = new FS_Site();
17656 $site->id = $install_id;
17657 $site->public_key = $install_public_key;
17658 $site->secret_key = $install_secret_key;
17659
17660 $this->_site = $site;
17661 $site_result = $this->get_api_site_scope( true )->get();
17662 $site = new FS_Site( $site_result );
17663 $this->_site = $site;
17664
17665 if ( ! is_null( $is_marketing_allowed ) ) {
17666 $this->disable_opt_in_notice_and_lock_user();
17667 }
17668
17669 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17670 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17671 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17672 ) );
17673
17674 return $this->setup_account(
17675 $this->_user,
17676 $this->_site,
17677 $redirect,
17678 $auto_install
17679 );
17680 }
17681
17682 /**
17683 * Install plugin with user.
17684 *
17685 * @author Leo Fajardo (@leorw)
17686 * @since 2.0.0
17687 *
17688 * @param number $user_id
17689 * @param string $user_public_key
17690 * @param string $user_secret_key
17691 * @param bool|null $is_marketing_allowed
17692 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17693 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17694 * @param array $site_ids
17695 * @param bool $license_key
17696 * @param bool $trial_plan_id
17697 * @param bool $redirect
17698 *
17699 * @return void
17700 */
17701 private function install_many_pending_with_user(
17702 $user_id,
17703 $user_public_key,
17704 $user_secret_key,
17705 $is_marketing_allowed,
17706 $is_extensions_tracking_allowed,
17707 $is_diagnostic_tracking_allowed,
17708 $site_ids,
17709 $license_key = false,
17710 $trial_plan_id = false,
17711 $redirect = true
17712 ) {
17713 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17714
17715 if ( ! is_null( $is_marketing_allowed ) ) {
17716 $this->disable_opt_in_notice_and_lock_user();
17717 }
17718
17719 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17720 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17721 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17722 ) );
17723
17724 $sites = array();
17725 foreach ( $site_ids as $site_id ) {
17726 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
17727 }
17728
17729 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17730 }
17731
17732 /**
17733 * Multi-site install with a new user.
17734 *
17735 * @author Vova Feldman (@svovaf)
17736 * @since 2.0.0
17737 *
17738 * @param number $user_id
17739 * @param string $user_public_key
17740 * @param string $user_secret_key
17741 * @param bool|null $is_marketing_allowed
17742 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17743 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17744 * @param object[] $installs
17745 * @param bool $redirect
17746 * @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.
17747 *
17748 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17749 */
17750 private function install_many_with_new_user(
17751 $user_id,
17752 $user_public_key,
17753 $user_secret_key,
17754 $is_marketing_allowed,
17755 $is_extensions_tracking_allowed,
17756 $is_diagnostic_tracking_allowed,
17757 array $installs,
17758 $redirect = true,
17759 $auto_install = false
17760 ) {
17761 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17762
17763 if ( ! is_null( $is_marketing_allowed ) ) {
17764 $this->disable_opt_in_notice_and_lock_user();
17765 }
17766
17767 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17768 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17769 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17770 ) );
17771
17772 $install_ids = array();
17773
17774 foreach ( $installs as $install ) {
17775 $install_ids[] = $install->id;
17776 }
17777
17778 $items_per_request = 25;
17779 $left = count( $install_ids );
17780 $offset = 0;
17781
17782 $installs = array();
17783 while ( $left > 0 ) {
17784 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, $items_per_request ) ) );
17785
17786 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
17787 // @todo Handle API error.
17788 }
17789
17790 $installs = array_merge( $installs, $result->installs );
17791
17792 $left -= $items_per_request;
17793 $offset += $items_per_request;
17794 }
17795
17796 foreach ( $installs as &$install ) {
17797 $install = new FS_Site( $install );
17798 }
17799
17800 return $this->setup_network_account(
17801 $this->_user,
17802 $installs,
17803 $redirect,
17804 $auto_install
17805 );
17806 }
17807
17808 /**
17809 * @author Vova Feldman (@svovaf)
17810 * @since 1.1.7.4
17811 *
17812 * @param string|bool $email
17813 * @param bool $redirect
17814 * @param string|bool $license_key Since 1.2.1.5
17815 * @param bool $is_pending_trial Since 1.2.1.5
17816 * @param bool $is_suspicious_email Since 2.5.0
17817 * @param bool $has_upgrade_context Since 2.5.3
17818 * @param bool|string $support_email_address Since 2.5.3
17819 *
17820 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
17821 */
17822 private function set_pending_confirmation(
17823 $email = false,
17824 $redirect = true,
17825 $license_key = false,
17826 $is_pending_trial = false,
17827 $is_suspicious_email = false,
17828 $has_upgrade_context = false,
17829 $support_email_address = false
17830 ) {
17831 $is_network_admin = fs_is_network_admin();
17832
17833 if ( $this->_ignore_pending_mode && ! $has_upgrade_context ) {
17834 /**
17835 * If explicitly asked to ignore pending mode, set to anonymous mode
17836 * 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).
17837 *
17838 * @author Vova Feldman
17839 * @since 1.2.1.6
17840 */
17841 $this->skip_connection( $is_network_admin );
17842 } else {
17843 // Install must be activated via email since
17844 // user with the same email already exist.
17845 $this->_storage->is_pending_activation = true;
17846 $this->_add_pending_activation_notice(
17847 $email,
17848 $is_pending_trial,
17849 $is_suspicious_email,
17850 $has_upgrade_context,
17851 $support_email_address
17852 );
17853 }
17854
17855 if ( ! empty( $license_key ) ) {
17856 $this->_storage->pending_license_key = $license_key;
17857 }
17858
17859 // Remove the opt-in sticky notice.
17860 $this->_admin_notices->remove_sticky( array(
17861 'connect_account',
17862 'trial_promotion',
17863 ) );
17864
17865 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
17866
17867 if ( $redirect ) {
17868 // Reload the page with a pending activation message.
17869 fs_redirect( $next_page );
17870 }
17871
17872 return $next_page;
17873 }
17874
17875 /**
17876 * Install plugin with current logged WP user info.
17877 *
17878 * @author Vova Feldman (@svovaf)
17879 * @since 1.0.7
17880 */
17881 function _install_with_current_user() {
17882 $this->_logger->entrance();
17883
17884 if ( $this->is_registered() ) {
17885 return;
17886 }
17887
17888 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
17889 check_admin_referer( $this->get_unique_affix() . '_activate_existing' );
17890
17891 /**
17892 * @author Vova Feldman (@svovaf)
17893 * @since 1.1.9 Add license key if given.
17894 */
17895 $license_key = fs_request_get_raw( 'license_secret_key' );
17896
17897 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17898 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17899 FS_Permission_Manager::PERMISSION_EXTENSIONS => fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17900 ) );
17901
17902 $this->install_with_current_user( $license_key );
17903 }
17904 }
17905
17906
17907 /**
17908 * @author Vova Feldman (@svovaf)
17909 * @since 1.1.7.4
17910 *
17911 * @param string|bool $license_key
17912 * @param number|bool $trial_plan_id
17913 * @param array $sites Since 2.0.0
17914 * @param bool $redirect
17915 *
17916 * @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.
17917 */
17918 function install_with_current_user(
17919 $license_key = false,
17920 $trial_plan_id = false,
17921 $sites = array(),
17922 $redirect = true
17923 ) {
17924 // Get current logged WP user.
17925 $current_user = self::_get_current_wp_user();
17926
17927 // Find the relevant FS user by the email.
17928 $user = self::_get_user_by_email( $current_user->user_email );
17929
17930 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17931 }
17932
17933 /**
17934 * @author Vova Feldman (@svovaf)
17935 * @since 2.0.0
17936 *
17937 * @param \FS_User $user
17938 * @param string|bool $license_key
17939 * @param number|bool $trial_plan_id
17940 * @param bool $redirect
17941 * @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.
17942 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
17943 *
17944 * @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.
17945 */
17946 function install_with_user(
17947 FS_User $user,
17948 $license_key = false,
17949 $trial_plan_id = false,
17950 $redirect = true,
17951 $setup_account = true,
17952 $sites = array()
17953 ) {
17954 // We have to set the user before getting user scope API handler.
17955 $this->_user = $user;
17956
17957 // Install the plugin.
17958 $result = $this->create_installs_with_user(
17959 $user,
17960 $license_key,
17961 $trial_plan_id,
17962 $sites,
17963 $redirect
17964 );
17965
17966 if ( ! $this->is_api_result_entity( $result ) &&
17967 ! $this->is_api_result_object( $result, 'installs' )
17968 ) {
17969 // @todo Handler potential API error of the $result
17970 }
17971
17972 if ( empty( $sites ) ) {
17973 $site = new FS_Site( $result );
17974 $this->_site = $site;
17975
17976 if ( ! $setup_account ) {
17977 $this->_store_site();
17978
17979 $this->sync_plan_if_not_exist( $site->plan_id );
17980
17981 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
17982 $this->sync_license_if_not_exist( $site->license_id, $license_key );
17983 }
17984
17985 $this->_admin_notices->remove_sticky( 'connect_account', false );
17986
17987 return $site;
17988 }
17989
17990 return $this->setup_account( $this->_user, $this->_site, $redirect );
17991 } else {
17992 $installs = array();
17993 foreach ( $result->installs as $install ) {
17994 $installs[] = new FS_Site( $install );
17995 }
17996
17997 return $this->setup_network_account(
17998 $user,
17999 $installs,
18000 $redirect
18001 );
18002 }
18003 }
18004
18005 /**
18006 * Initiate an API request to create a collection of installs.
18007 *
18008 * @author Vova Feldman (@svovaf)
18009 * @since 2.0.0
18010 *
18011 * @param \FS_User $user
18012 * @param bool $license_key
18013 * @param bool $trial_plan_id
18014 * @param array $sites
18015 * @param bool $redirect
18016 * @param bool $silent
18017 *
18018 * @return object|mixed
18019 */
18020 private function create_installs_with_user(
18021 FS_User $user,
18022 $license_key = false,
18023 $trial_plan_id = false,
18024 $sites = array(),
18025 $redirect = false,
18026 $silent = false
18027 ) {
18028 $extra_install_params = array(
18029 'uid' => $this->get_anonymous_id(),
18030 'is_disconnected' => false,
18031 );
18032
18033 if ( ! empty( $license_key ) ) {
18034 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
18035
18036 if ( $silent ) {
18037 $extra_install_params['ignore_license_owner'] = true;
18038 }
18039 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
18040 $extra_install_params['trial_plan_id'] = $trial_plan_id;
18041 }
18042
18043 if ( ! empty( $sites ) ) {
18044 $extra_install_params['sites'] = $sites;
18045 }
18046
18047 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
18048
18049 // Install the plugin.
18050 $result = $this->get_api_user_scope_by_user( $user )->call(
18051 "/plugins/{$this->get_id()}/installs.json",
18052 'post',
18053 $args
18054 );
18055
18056 if ( ! $this->is_api_result_entity( $result ) &&
18057 ! $this->is_api_result_object( $result, 'installs' )
18058 ) {
18059 if ( ! empty( $args['license_key'] ) ) {
18060 // Pass the fully entered license key to the failure handler.
18061 $args['license_key'] = $license_key;
18062 }
18063
18064 $result = $this->apply_filters( 'after_install_failure', $result, $args );
18065
18066 if ( ! $silent ) {
18067 $this->_admin_notices->add(
18068 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18069 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
18070 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18071 'error'
18072 );
18073 }
18074
18075 if ( $redirect ) {
18076 /**
18077 * We set the user before getting the user scope API handler, so the user became temporarily
18078 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
18079 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
18080 * activation page based on the return value of `is_registered()`. In addition, in case the
18081 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
18082 * misleading plugin activation errors will be shown on the `Plugins` page.
18083 *
18084 * @author Leo Fajardo (@leorw)
18085 */
18086 $user = $this->_user;
18087
18088 $this->_user = null;
18089
18090 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
18091
18092 /**
18093 * Restore the user after the redirect, this is relevant when there are cases where the redirect will choose not to do anything.
18094 */
18095 $this->_user = $user;
18096 }
18097 }
18098
18099 return $result;
18100 }
18101
18102 /**
18103 * Tries to activate add-on account based on parent plugin info.
18104 *
18105 * @author Vova Feldman (@svovaf)
18106 * @since 1.0.6
18107 *
18108 * @param Freemius $parent_fs
18109 * @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.
18110 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
18111 */
18112 private function _activate_addon_account(
18113 Freemius $parent_fs,
18114 $network_level_or_blog_id = null,
18115 FS_Plugin_License $bundle_license = null
18116 ) {
18117 if ( $this->is_registered() ) {
18118 // Already activated.
18119 return;
18120 }
18121
18122 $permission_ids = FS_Permission_Manager::get_all_permission_ids();
18123 $permissions = array();
18124 foreach ( $permission_ids as $permission_id ) {
18125 $permissions[ $permission_id ] = FS_Permission_Manager::instance( $parent_fs )->is_permission( $permission_id, true );
18126 }
18127
18128 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( $permissions );
18129
18130 /**
18131 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
18132 * already returns the data for the current blog.
18133 *
18134 * @author Leo Fajardo (@leorw)
18135 * @since 2.3.0
18136 */
18137 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
18138 array() :
18139 array( 'uid' => $this->get_anonymous_id() );
18140
18141 $params = $this->get_install_data_for_api(
18142 $uid_param_to_override,
18143 false,
18144 false,
18145 /**
18146 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
18147 * already includes the data for it.
18148 *
18149 * @author Leo Fajardo (@leorw)
18150 * @since 2.3.0
18151 */
18152 ( true !== $network_level_or_blog_id )
18153 );
18154
18155 if ( true === $network_level_or_blog_id ) {
18156 $params['sites'] = $this->get_sites_for_network_level_optin();
18157
18158 if ( empty( $params['sites'] ) ) {
18159 return;
18160 }
18161 }
18162
18163 if ( is_object( $bundle_license ) ) {
18164 $params['license_key'] = $bundle_license->secret_key;
18165 }
18166
18167 // Activate add-on with parent plugin credentials.
18168 $result = $parent_fs->get_api_site_scope()->call(
18169 "/addons/{$this->_plugin->id}/installs.json",
18170 'post',
18171 $params
18172 );
18173
18174 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18175 if ( is_object( $bundle_license ) ) {
18176 /**
18177 * 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.
18178 *
18179 * @author Leo Fajardo (@leorw)
18180 * @since 2.4.0
18181 */
18182 } else {
18183 $error_message = FS_Api::is_api_error_object( $result ) ?
18184 $result->error->message :
18185 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
18186
18187 $this->_admin_notices->add(
18188 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18189 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
18190 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18191 'error'
18192 );
18193 }
18194
18195 return;
18196 }
18197
18198 $addon_installs = $result->installs;
18199 foreach ( $addon_installs as $key => $addon_install ) {
18200 $addon_installs[ $key ] = new FS_Site( $addon_install );
18201 }
18202
18203 $first_install = $addon_installs[0];
18204
18205 // Get user information based on parent's plugin.
18206 $user = $parent_fs->get_user();
18207
18208 // First of all, set site and user info - otherwise we won't
18209 // be able to invoke API calls.
18210 $this->_site = $first_install;
18211 $this->_user = $user;
18212
18213 // Sync add-on plans.
18214 $this->_sync_plans();
18215
18216 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
18217
18218 // Get site's current plan.
18219 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
18220
18221 // Sync licenses.
18222 $this->_sync_licenses();
18223
18224 if ( ! fs_is_network_admin() ) {
18225 // Try to activate premium license.
18226 $this->_activate_license( true, $bundle_license );
18227
18228 if ( is_object( $bundle_license ) ) {
18229 $this->maybe_activate_bundle_license( $bundle_license );
18230 }
18231 } else {
18232 if ( is_object( $bundle_license ) ) {
18233 $premium_license = $bundle_license;
18234 } else {
18235 $license_id = fs_request_get( 'license_id' );
18236
18237 if ( is_object( $this->_site ) &&
18238 FS_Plugin_License::is_valid_id( $license_id ) &&
18239 $license_id == $this->_site->license_id
18240 ) {
18241 // License is already activated.
18242 return;
18243 }
18244
18245 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
18246 $this->_get_license_by_id( $license_id ) :
18247 $this->_get_available_premium_license();
18248 }
18249
18250 if ( is_object( $premium_license ) ) {
18251 $this->maybe_network_activate_addon_license( $premium_license );
18252 }
18253 }
18254 }
18255
18256 /**
18257 * @author Leo Fajardo (@leorw)
18258 * @since 2.3.0
18259 *
18260 * @param FS_Site[] $installs
18261 * @param bool $is_site_level
18262 */
18263 private function handle_account_connection( $installs, $is_site_level ) {
18264 $first_install = $installs[0];
18265
18266 if ( $is_site_level ) {
18267 $this->_set_account( $this->_user, $first_install );
18268
18269 $this->do_action( 'after_account_connection', $this->_user, $first_install );
18270 } else {
18271 $this->_store_user();
18272
18273 // Map site addresses to their blog IDs.
18274 $address_to_blog_map = $this->get_address_to_blog_map();
18275
18276 $first_blog_id = null;
18277 $blog_2_install_map = array();
18278 foreach ( $installs as $install ) {
18279 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
18280 $blog_id = $address_to_blog_map[ $address ];
18281
18282 $this->_store_site( true, $blog_id, $install );
18283
18284 if ( is_null( $first_blog_id ) ) {
18285 $first_blog_id = $blog_id;
18286 }
18287
18288 $blog_2_install_map[ $blog_id ] = $install;
18289 }
18290
18291 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
18292 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
18293 ) {
18294 // Store network user.
18295 $this->_storage->network_user_id = $this->_user->id;
18296 }
18297
18298 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
18299 $this->_storage->network_install_blog_id = $first_blog_id;
18300 }
18301
18302 if ( count( $installs ) === count( $address_to_blog_map ) ) {
18303 // Super admin opted in for all sites in the network.
18304 $this->_storage->is_network_connected = true;
18305 }
18306
18307 $this->_store_licenses( false );
18308
18309 self::$_accounts->store();
18310
18311 // Don't sync the installs data on network upgrade
18312 if ( ! $this->network_upgrade_mode_completed() ) {
18313 $this->send_installs_update();
18314 }
18315
18316 $current_blog = get_current_blog_id();
18317
18318 foreach ( $blog_2_install_map as $blog_id => $install ) {
18319 $this->switch_to_blog( $blog_id );
18320
18321 $this->do_action( 'after_account_connection', $this->_user, $install );
18322 }
18323
18324 // Switch install context back to the first install.
18325 $this->switch_to_blog(
18326 $current_blog,
18327 $first_install,
18328 ( $this->_site->id != $first_install->id )
18329 );
18330
18331 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
18332 }
18333 }
18334
18335 /**
18336 * Tries to activate parent account based on add-on's info.
18337 *
18338 * @author Vova Feldman (@svovaf)
18339 * @since 1.2.2.7
18340 *
18341 * @param Freemius $parent_fs
18342 */
18343 private function activate_parent_account( Freemius $parent_fs ) {
18344 if ( ! $this->is_addon() ) {
18345 // This is not an add-on.
18346 return;
18347 }
18348
18349 if ( $parent_fs->is_registered() ) {
18350 // Already activated.
18351 return;
18352 }
18353
18354 // Activate parent with add-on's user credentials.
18355 $parent_install = $this->get_api_user_scope()->call(
18356 "/plugins/{$parent_fs->_plugin->id}/installs.json",
18357 'post',
18358 $parent_fs->get_install_data_for_api( array(
18359 'uid' => $parent_fs->get_anonymous_id(),
18360 ), false, false )
18361 );
18362
18363 if ( isset( $parent_install->error ) ) {
18364 $this->_admin_notices->add(
18365 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18366 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
18367 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18368 'error'
18369 );
18370
18371 return;
18372 }
18373
18374 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
18375
18376 if ( $parent_fs->is_pending_activation() ) {
18377 $parent_fs->clear_pending_activation_mode();
18378 }
18379
18380 // Get user information based on parent's plugin.
18381 $user = $this->get_user();
18382
18383 // First of all, set site info - otherwise we won't
18384 // be able to invoke API calls.
18385 $parent_fs->_site = new FS_Site( $parent_install );
18386 $parent_fs->_user = $user;
18387
18388 // Sync add-on plans.
18389 $parent_fs->_sync_plans();
18390
18391 $parent_fs->update_license_required_permissions_if_anonymous();
18392
18393 $parent_fs->_set_account( $user, $parent_fs->_site );
18394 }
18395
18396 #endregion
18397
18398 #----------------------------------------------------------------------------------
18399 #region Admin Menu Items
18400 #----------------------------------------------------------------------------------
18401
18402 private $_menu_items = array();
18403
18404 /**
18405 * @author Vova Feldman (@svovaf)
18406 * @since 1.2.1.8
18407 *
18408 * @return array
18409 */
18410 function get_menu_items() {
18411 return $this->_menu_items;
18412 }
18413
18414 /**
18415 * @author Vova Feldman (@svovaf)
18416 * @since 1.0.7
18417 *
18418 * @return string
18419 */
18420 function get_menu_slug() {
18421 return $this->_menu->get_slug();
18422 }
18423
18424 /**
18425 * @author Vova Feldman (@svovaf)
18426 * @since 1.0.9
18427 */
18428 function _prepare_admin_menu() {
18429 // if ( ! $this->is_on() ) {
18430 // return;
18431 // }
18432
18433 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
18434 return;
18435 }
18436
18437 /**
18438 * When running from a site admin with a network activated module and the connection
18439 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
18440 * site level settings.
18441 *
18442 * @author Vova Feldman (@svovaf)
18443 * @since 2.0.0
18444 */
18445 $should_hide_site_admin_settings = (
18446 $this->_is_network_active &&
18447 ! fs_is_network_admin() &&
18448 ! $this->is_delegated_connection() &&
18449 ! $this->is_anonymous() &&
18450 ! $this->is_registered()
18451 );
18452
18453 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
18454
18455 if ( ( false === $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
18456 $should_hide_site_admin_settings
18457 ) {
18458 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
18459 } else {
18460 $this->do_action( fs_is_network_admin() ?
18461 'before_network_admin_menu_init' :
18462 'before_admin_menu_init'
18463 );
18464
18465 $this->add_menu_action();
18466
18467 $this->add_network_menu_when_missing();
18468
18469 $this->add_submenu_items();
18470 }
18471 }
18472
18473 /**
18474 * Admin dashboard menu items modifications.
18475 *
18476 * NOTE: admin_menu action executed before admin_init.
18477 *
18478 * @author Vova Feldman (@svovaf)
18479 * @since 1.0.7
18480 *
18481 */
18482 private function add_menu_action() {
18483 if ( $this->is_activation_mode() ) {
18484 if ( $this->show_opt_in_on_setting_page() ) {
18485 $this->override_plugin_menu_with_activation();
18486 } else {
18487 /**
18488 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
18489 */
18490 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18491 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
18492 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
18493 fs_request_get_bool( 'pending_activation' )
18494 ) {
18495 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
18496 }
18497 }
18498 } else {
18499 if ( ! $this->is_registered() ) {
18500 // If not registered try to install user.
18501 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18502 $this->_install_with_new_user();
18503 }
18504 } else if (
18505 fs_request_is_action( 'sync_user' ) &&
18506 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
18507 ) {
18508 $this->_handle_account_user_sync();
18509 }
18510 }
18511 }
18512
18513 /**
18514 * @author Vova Feldman (@svovaf)
18515 * @since 1.0.1
18516 */
18517 function _redirect_on_clicked_menu_link() {
18518 $this->_logger->entrance();
18519
18520 $page = fs_request_get('page');
18521 $page = is_string($page) ? strtolower($page) : '';
18522
18523 $this->_logger->log( 'page = ' . $page );
18524
18525 foreach ( $this->_menu_items as $priority => $items ) {
18526 foreach ( $items as $item ) {
18527 if ( isset( $item['url'] ) ) {
18528 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
18529 $this->_logger->log( 'Redirecting to ' . $item['url'] );
18530
18531 fs_redirect( $item['url'] );
18532 }
18533 }
18534 }
18535 }
18536 }
18537
18538 /**
18539 * Remove plugin's all admin menu items & pages, and replace with activation page.
18540 *
18541 * @author Vova Feldman (@svovaf)
18542 * @since 1.0.1
18543 */
18544 private function override_plugin_menu_with_activation() {
18545 $this->_logger->entrance();
18546
18547 $hook = false;
18548
18549 if ( ! $this->has_settings_menu() ) {
18550 // Add the opt-in page without a menu item.
18551 $hook = FS_Admin_Menu_Manager::add_subpage(
18552 '',
18553 $this->get_plugin_name(),
18554 $this->get_plugin_name(),
18555 'manage_options',
18556 $this->_slug,
18557 array( &$this, '_connect_page_render' )
18558 );
18559 } else if ( $this->_menu->is_top_level() ) {
18560 if ( $this->_menu->is_override_exact() ) {
18561 // Make sure the current page is matching the activation page.
18562 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18563 return;
18564 }
18565 }
18566
18567 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18568
18569 if ( false === $hook ) {
18570 // Create new menu item just for the opt-in.
18571 $hook = FS_Admin_Menu_Manager::add_page(
18572 $this->get_plugin_name(),
18573 $this->get_plugin_name(),
18574 'manage_options',
18575 $this->_menu->get_slug(),
18576 array( &$this, '_connect_page_render' )
18577 );
18578 }
18579 } else {
18580 $menus = array( $this->_menu->get_parent_slug() );
18581
18582 if ( $this->_menu->is_override_exact() ) {
18583 // Make sure the current page is matching the activation page.
18584 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18585 return;
18586 }
18587 }
18588
18589 foreach ( $menus as $parent_slug ) {
18590 $hook = $this->_menu->override_submenu_action(
18591 $parent_slug,
18592 $this->_menu->get_raw_slug(),
18593 array( &$this, '_connect_page_render' )
18594 );
18595
18596 if ( false !== $hook ) {
18597 // Found plugin's submenu item.
18598 break;
18599 }
18600 }
18601 }
18602
18603 if ( $this->is_activation_page() ) {
18604 // Clean admin page from distracting content.
18605 self::_clean_admin_content_section();
18606 }
18607
18608 if ( false !== $hook ) {
18609 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18610 $this->_install_with_current_user();
18611 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18612 $this->_install_with_new_user();
18613 }
18614 }
18615 }
18616
18617 /**
18618 * If a plugin was network activated and connected but don't have a network
18619 * level settings, then add an artificial menu item for the Account and other
18620 * Freemius settings.
18621 *
18622 * @author Vova Feldman (@svovaf)
18623 * @since 2.0.0
18624 */
18625 private function add_network_menu_when_missing() {
18626 $this->_logger->entrance();
18627
18628 if ( ! $this->_is_network_active ) {
18629 // Plugin wasn't activated on the network level.
18630 return;
18631 }
18632
18633 if ( ! fs_is_network_admin() ) {
18634 // The context is not the network admin.
18635 return;
18636 }
18637
18638 if ( $this->_menu->has_network_menu() ) {
18639 // Plugin already has a network level menu.
18640 return;
18641 }
18642
18643 if ( $this->is_network_activation_mode() ) {
18644 /**
18645 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
18646 * screen is being shown.
18647 *
18648 * @author Leo Fajardo (@leorw)
18649 */
18650 return;
18651 }
18652
18653 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
18654 if ( $this->is_network_delegated_connection() ) {
18655 // Super-admin delegated the connection to the site admins.
18656 return;
18657 }
18658 }
18659
18660 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
18661
18662 if ( $this->_menu->has_menu() ||
18663 ! $this->is_addon() ||
18664 $this->is_activation_mode()
18665 ) {
18666 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
18667 $this->get_plugin_name(),
18668 $this->get_plugin_name(),
18669 'manage_options',
18670 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
18671 );
18672 }
18673 } else {
18674 $this->_menu->add_subpage_and_update(
18675 $this->_menu->get_parent_slug(),
18676 $this->get_plugin_name(),
18677 $this->get_plugin_name(),
18678 'manage_options',
18679 $this->_menu->get_slug()
18680 );
18681 }
18682 }
18683
18684 /**
18685 * @author Leo Fajardo (@leorw)
18686 * @since 1.2.1
18687 *
18688 * return string
18689 */
18690 function get_top_level_menu_capability() {
18691 global $menu;
18692
18693 $top_level_menu_slug = $this->get_top_level_menu_slug();
18694
18695 foreach ( $menu as $menu_info ) {
18696 /**
18697 * The second element in the menu info array is the capability/role that has access to the menu and the
18698 * third element is the menu slug.
18699 */
18700 if ( $menu_info[2] === $top_level_menu_slug ) {
18701 return $menu_info[1];
18702 }
18703 }
18704
18705 return 'read';
18706 }
18707
18708 /**
18709 * @author Vova Feldman (@svovaf)
18710 * @since 1.0.0
18711 *
18712 * @return string
18713 */
18714 private function get_top_level_menu_slug() {
18715 return ( $this->is_addon() ?
18716 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
18717 $this->_menu->get_top_level_menu_slug() );
18718 }
18719
18720 /**
18721 * @author Vova Feldman (@svovaf)
18722 * @since 1.2.2.7
18723 *
18724 * @return string
18725 */
18726 function get_pricing_cta_label() {
18727 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
18728
18729 if ( $this->is_in_trial_promotion() &&
18730 ! $this->is_paying_or_trial()
18731 ) {
18732 // If running a trial promotion, modify the pricing to load the trial.
18733 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
18734 } else if ( $this->is_paying() ) {
18735 $label = $this->get_text_inline( 'Pricing', 'pricing' );
18736 }
18737
18738 return $label;
18739 }
18740
18741 /**
18742 * @author Vova Feldman (@svovaf)
18743 * @since 1.2.2.7
18744 *
18745 * @return bool
18746 */
18747 function is_pricing_page_visible() {
18748 return (
18749 // Has at least one paid plan.
18750 $this->has_paid_plan() &&
18751 // Didn't ask to hide the pricing page.
18752 $this->is_page_visible( 'pricing' ) &&
18753 // Don't have a valid active license or has more than one plan.
18754 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
18755 );
18756 }
18757
18758 /**
18759 * @author Leo Fajardo (@leorw)
18760 * @since 2.3.0
18761 *
18762 * @param bool $is_activation_mode
18763 *
18764 * @return bool
18765 */
18766 private function should_add_submenu_or_action_links( $is_activation_mode ) {
18767 if ( $this->is_addon() ) {
18768 // No submenu items or action links for add-ons.
18769 return false;
18770 }
18771
18772 if ( $this->show_opt_in_on_themes_page() ) {
18773 if ( ! fs_is_network_admin() ) {
18774 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
18775 return true;
18776 }
18777 } else if ( $is_activation_mode ) {
18778 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
18779 return false;
18780 }
18781
18782 if ( fs_is_network_admin() ) {
18783 /**
18784 * Add submenu items or action links to network level when plugin was network activated and the super
18785 * admin did NOT delegate the connection of all sites to site admins.
18786 */
18787 return (
18788 $this->_is_network_active &&
18789 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
18790 ! $this->is_network_delegated_connection() )
18791 );
18792 }
18793
18794 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
18795 }
18796
18797 /**
18798 * Add default Freemius menu items.
18799 *
18800 * @author Vova Feldman (@svovaf)
18801 * @since 1.0.0
18802 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
18803 */
18804 private function add_submenu_items() {
18805 $this->_logger->entrance();
18806
18807 $is_activation_mode = $this->is_activation_mode();
18808
18809 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
18810
18811 if ( $add_submenu_items ) {
18812 if ( $this->has_affiliate_program() ) {
18813 // Add affiliation page.
18814 $this->add_submenu_item(
18815 $this->get_text_inline( 'Affiliation', 'affiliation' ),
18816 array( &$this, '_affiliation_page_render' ),
18817 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
18818 'manage_options',
18819 'affiliation',
18820 'Freemius::_clean_admin_content_section',
18821 WP_FS__DEFAULT_PRIORITY,
18822 $this->is_submenu_item_visible( 'affiliation' )
18823 );
18824 }
18825 }
18826
18827 if ( $add_submenu_items ||
18828 ( $is_activation_mode &&
18829 $this->is_only_premium() &&
18830 $this->is_admin_page( 'account' ) &&
18831 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
18832 )
18833 ) {
18834 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
18835 $show_account = (
18836 $this->is_submenu_item_visible( 'account' ) &&
18837 /**
18838 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
18839 */
18840 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
18841 );
18842
18843 // Add user account page.
18844 $this->add_submenu_item(
18845 $this->get_text_inline( 'Account', 'account' ),
18846 array( &$this, '_account_page_render' ),
18847 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
18848 'manage_options',
18849 'account',
18850 array( &$this, '_account_page_load' ),
18851 WP_FS__DEFAULT_PRIORITY,
18852 ( $add_submenu_items && $show_account )
18853 );
18854 }
18855 }
18856
18857 if ( $add_submenu_items ) {
18858 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18859 // Add contact page.
18860 if ( $this->is_premium() ) {
18861 $this->add_submenu_item(
18862 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18863 array( &$this, '_contact_page_render' ),
18864 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
18865 'manage_options',
18866 'contact',
18867 'Freemius::_clean_admin_content_section',
18868 WP_FS__DEFAULT_PRIORITY,
18869 $this->is_submenu_item_visible( 'contact' )
18870 );
18871 } else {
18872 $this->add_submenu_link_item(
18873 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18874 FS_Contact_Form_Manager::instance()->get_standalone_link( $this ),
18875 'contact',
18876 'manage_options',
18877 WP_FS__DEFAULT_PRIORITY,
18878 $this->is_submenu_item_visible( 'contact' ),
18879 'fs_external_contact',
18880 true
18881 );
18882 }
18883 }
18884
18885 if ( $this->has_addons() ) {
18886 $this->add_submenu_item(
18887 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18888 array( &$this, '_addons_page_render' ),
18889 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18890 'manage_options',
18891 'addons',
18892 array( &$this, '_addons_page_load' ),
18893 WP_FS__LOWEST_PRIORITY - 1,
18894 $this->is_submenu_item_visible( 'addons' )
18895 );
18896 }
18897 }
18898
18899 if ( $add_submenu_items ||
18900 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
18901 ) {
18902 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18903 $show_pricing = (
18904 $this->is_submenu_item_visible( 'pricing' ) &&
18905 $this->is_pricing_page_visible()
18906 );
18907
18908 $pricing_cta_text = $this->get_pricing_cta_label();
18909 $pricing_class = 'upgrade-mode';
18910 if ( $show_pricing ) {
18911 if ( $this->is_in_trial_promotion() &&
18912 ! $this->is_paying_or_trial()
18913 ) {
18914 // If running a trial promotion, modify the pricing to load the trial.
18915 $pricing_class = 'trial-mode';
18916 } else if ( $this->is_paying() ) {
18917 $pricing_class = '';
18918 }
18919 }
18920
18921 $custom_pricing_url = $this->get_pricing_url_with_filter( null );
18922 $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' ) );
18923 $show_pricing_submenu_item = ( $add_submenu_items && $show_pricing );
18924
18925 // Add upgrade/pricing submenu item.
18926 if ( ! is_null( $custom_pricing_url ) ) {
18927 $this->add_submenu_link_item(
18928 $pricing_menu_title,
18929 $custom_pricing_url,
18930 'pricing',
18931 'manage_options',
18932 WP_FS__LOWEST_PRIORITY,
18933 $show_pricing_submenu_item,
18934 $pricing_class
18935 );
18936 } else {
18937 $this->add_submenu_item(
18938 $pricing_menu_title,
18939 array( &$this, '_pricing_page_render' ),
18940 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
18941 'manage_options',
18942 'pricing',
18943 'Freemius::_clean_admin_content_section',
18944 WP_FS__LOWEST_PRIORITY,
18945 $show_pricing_submenu_item,
18946 $pricing_class
18947 );
18948 }
18949 }
18950 }
18951
18952 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
18953 /**
18954 * Add the other menu items if there are any when not in activation mode or license activation is not
18955 * required (license activation is required for registered or anonymous users after activating the
18956 * premium version when the site is not in trial mode or there's no active valid license).
18957 *
18958 * @author Leo Fajardo (@leorw)
18959 * @since 2.2.1
18960 */
18961 if ( 0 < count( $this->_menu_items ) ) {
18962 if ( ! $this->_menu->is_top_level() ) {
18963 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
18964
18965 // Append submenu items right after the plugin's submenu item.
18966 $this->order_sub_submenu_items();
18967 } else {
18968 // Append submenu items.
18969 $this->embed_submenu_items();
18970 }
18971 }
18972 }
18973 }
18974
18975 /**
18976 * Moved the actual submenu item additions to a separated function,
18977 * in order to support sub-submenu items when the plugin's settings
18978 * only have a submenu and not top-level menu item.
18979 *
18980 * @author Vova Feldman (@svovaf)
18981 * @since 1.1.4
18982 */
18983 private function embed_submenu_items() {
18984 $item_classes = $this->_menu->is_top_level() ? 'fs-submenu-item' : 'fs-submenu-item fs-sub';
18985
18986 $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>';
18987
18988 $top_level_menu_capability = $this->get_top_level_menu_capability();
18989
18990 ksort( $this->_menu_items );
18991
18992 $is_first_submenu_item = true;
18993
18994 foreach ( $this->_menu_items as $priority => $items ) {
18995 foreach ( $items as $item ) {
18996 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
18997
18998 $menu_item = sprintf(
18999 $item_template,
19000 $this->get_unique_affix(),
19001 $item['menu_slug'],
19002 ! empty( $item['class'] ) ? $item['class'] : '',
19003 $item['menu_title'],
19004 esc_attr( isset( $item['url'] ) ? $item['url'] : '' ),
19005 esc_attr( isset( $item['new_tab'] ) ? 'true' : 'false' )
19006 );
19007
19008 $top_level_menu_slug = $this->get_top_level_menu_slug();
19009 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
19010
19011 if ( ! isset( $item['url'] ) ) {
19012 $hook = FS_Admin_Menu_Manager::add_subpage(
19013 $item['show_submenu'] ?
19014 $top_level_menu_slug :
19015 '',
19016 $item['page_title'],
19017 $menu_item,
19018 $capability,
19019 $menu_slug,
19020 $item['render_function']
19021 );
19022
19023 if ( false !== $item['before_render_function'] ) {
19024 add_action( "load-$hook", $item['before_render_function'] );
19025 }
19026 } else {
19027 FS_Admin_Menu_Manager::add_subpage(
19028 $item['show_submenu'] ?
19029 $top_level_menu_slug :
19030 '',
19031 $item['page_title'],
19032 $menu_item,
19033 $capability,
19034 $menu_slug,
19035 array( $this, '' )
19036 );
19037 }
19038
19039 if ( $item['show_submenu'] && $is_first_submenu_item ) {
19040 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
19041 /**
19042 * If the top-level menu has been dynamically created, remove the first submenu item that
19043 * WordPress automatically creates when there's no submenu item whose slug matches the
19044 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
19045 *
19046 * Awesome Plugin
19047 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
19048 *
19049 * @author Leo Fajardo (@leorw)
19050 */
19051 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
19052 }
19053
19054 $is_first_submenu_item = false;
19055 }
19056 }
19057 }
19058 }
19059
19060 /**
19061 * Re-order the submenu items so all Freemius added new submenu items
19062 * are added right after the plugin's settings submenu item.
19063 *
19064 * @author Vova Feldman (@svovaf)
19065 * @since 1.1.4
19066 */
19067 private function order_sub_submenu_items() {
19068 global $submenu;
19069
19070 $menu_slug = $this->_menu->get_top_level_menu_slug();
19071
19072 /**
19073 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
19074 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
19075 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
19076 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
19077 *
19078 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
19079 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
19080 * capability needed to access the parent menu as the capability for the submenus that we will add.
19081 */
19082 if ( empty( $submenu[ $menu_slug ] ) ) {
19083 return;
19084 }
19085
19086 $top_level_menu = &$submenu[ $menu_slug ];
19087
19088 $all_submenu_items_after = array();
19089
19090 $found_submenu_item = false;
19091
19092 foreach ( $top_level_menu as $submenu_id => $meta ) {
19093 if ( $found_submenu_item ) {
19094 // Remove all submenu items after the plugin's submenu item.
19095 $all_submenu_items_after[] = $meta;
19096 unset( $top_level_menu[ $submenu_id ] );
19097 }
19098
19099 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
19100 // Found the submenu item, put all below.
19101 $found_submenu_item = true;
19102 continue;
19103 }
19104 }
19105
19106 // Embed all plugin's new submenu items.
19107 $this->embed_submenu_items();
19108
19109 // Start with specially high number to make sure it's appended.
19110 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
19111 foreach ( $all_submenu_items_after as $meta ) {
19112 $top_level_menu[ $i ] = $meta;
19113 $i ++;
19114 }
19115
19116 // Sort submenu items.
19117 ksort( $top_level_menu );
19118 }
19119
19120 /**
19121 * Helper method to return the module's support forum URL.
19122 *
19123 * @author Vova Feldman (@svovaf)
19124 * @since 1.2.2.7
19125 *
19126 * @return string
19127 */
19128 function get_support_forum_url() {
19129 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
19130 }
19131
19132 /**
19133 * Displays the Support Forum link when enabled.
19134 *
19135 * Can be filtered like so:
19136 *
19137 * function _fs_show_support_menu( $is_visible, $menu_id ) {
19138 * if ( 'support' === $menu_id ) {
19139 * return _fs->is_registered();
19140 * }
19141 * return $is_visible;
19142 * }
19143 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
19144 *
19145 */
19146 function _add_default_submenu_items() {
19147 if ( ! $this->is_on() ) {
19148 return;
19149 }
19150
19151 if ( ! $this->is_activation_mode() &&
19152 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
19153 ( ! $this->_is_network_active && is_admin() ) )
19154 ) {
19155 $this->add_submenu_link_item(
19156 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
19157 $this->get_support_forum_url(),
19158 'wp-support-forum',
19159 null,
19160 50,
19161 $this->is_submenu_item_visible( 'support' ),
19162 '',
19163 true
19164 );
19165 }
19166 }
19167
19168 /**
19169 * @author Vova Feldman (@svovaf)
19170 * @since 1.0.1
19171 *
19172 * @param string $menu_title
19173 * @param callable $render_function
19174 * @param bool|string $page_title
19175 * @param string $capability
19176 * @param bool|string $menu_slug
19177 * @param bool|callable $before_render_function
19178 * @param int $priority
19179 * @param bool $show_submenu
19180 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
19181 */
19182 function add_submenu_item(
19183 $menu_title,
19184 $render_function,
19185 $page_title = false,
19186 $capability = 'manage_options',
19187 $menu_slug = false,
19188 $before_render_function = false,
19189 $priority = WP_FS__DEFAULT_PRIORITY,
19190 $show_submenu = true,
19191 $class = ''
19192 ) {
19193 $this->_logger->entrance( 'Title = ' . $menu_title );
19194
19195 if ( $this->is_addon() ) {
19196 $parent_fs = $this->get_parent_instance();
19197
19198 if ( is_object( $parent_fs ) ) {
19199 $parent_fs->add_submenu_item(
19200 $menu_title,
19201 $render_function,
19202 $page_title,
19203 $capability,
19204 $menu_slug,
19205 $before_render_function,
19206 $priority,
19207 $show_submenu,
19208 $class
19209 );
19210
19211 return;
19212 }
19213 }
19214
19215 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19216 $this->_menu_items[ $priority ] = array();
19217 }
19218
19219 $this->_menu_items[ $priority ][] = array(
19220 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
19221 'menu_title' => $menu_title,
19222 'capability' => $capability,
19223 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19224 'render_function' => $render_function,
19225 'before_render_function' => $before_render_function,
19226 'show_submenu' => $show_submenu,
19227 'class' => $class,
19228 );
19229 }
19230
19231 /**
19232 * @author Vova Feldman (@svovaf)
19233 * @since 1.0.1
19234 *
19235 * @param string $menu_title
19236 * @param string $url
19237 * @param bool $menu_slug
19238 * @param string $capability
19239 * @param int $priority
19240 * @param bool $show_submenu
19241 * @param string $class
19242 * @param bool $new_tab
19243 */
19244 function add_submenu_link_item(
19245 $menu_title,
19246 $url,
19247 $menu_slug = false,
19248 $capability = 'read',
19249 $priority = WP_FS__DEFAULT_PRIORITY,
19250 $show_submenu = true,
19251 $class = '',
19252 $new_tab = false
19253 ) {
19254 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
19255
19256 if ( $this->is_addon() ) {
19257 $parent_fs = $this->get_parent_instance();
19258
19259 if ( is_object( $parent_fs ) ) {
19260 $parent_fs->add_submenu_link_item(
19261 $menu_title,
19262 $url,
19263 $menu_slug,
19264 $capability,
19265 $priority,
19266 $show_submenu,
19267 $class,
19268 $new_tab
19269 );
19270
19271 return;
19272 }
19273 }
19274
19275 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19276 $this->_menu_items[ $priority ] = array();
19277 }
19278
19279 $this->_menu_items[ $priority ][] = array(
19280 'menu_title' => $menu_title,
19281 'capability' => $capability,
19282 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19283 'url' => $url,
19284 'page_title' => $menu_title,
19285 'render_function' => 'fs_dummy',
19286 'before_render_function' => '',
19287 'show_submenu' => $show_submenu,
19288 'class' => $class,
19289 'new_tab' => $new_tab,
19290 );
19291 }
19292
19293 #endregion ------------------------------------------------------------------
19294
19295 #--------------------------------------------------------------------------------
19296 #region Admin Notices
19297 #--------------------------------------------------------------------------------
19298
19299 /**
19300 * @author Vova Feldman (@svovaf)
19301 * @since 2.3.1
19302 *
19303 * @param string|string[] $ids
19304 * @param int|null $network_level_or_blog_id
19305 *
19306 * @uses FS_Admin_Notices::remove_sticky()
19307 */
19308 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
19309 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
19310 }
19311
19312 #endregion
19313
19314 #--------------------------------------------------------------------------------
19315 #region Actions / Hooks / Filters
19316 #--------------------------------------------------------------------------------
19317
19318 /**
19319 * @author Vova Feldman (@svovaf)
19320 * @since 1.1.7
19321 *
19322 * @param string $tag
19323 *
19324 * @return string
19325 */
19326 public function get_action_tag( $tag ) {
19327 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
19328 }
19329
19330 /**
19331 * @author Vova Feldman (@svovaf)
19332 * @since 1.2.1.6
19333 *
19334 * @param string $tag
19335 * @param string $slug
19336 * @param bool $is_plugin
19337 *
19338 * @return string
19339 */
19340 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
19341 $action = "fs_{$tag}";
19342
19343 if ( ! empty( $slug ) ) {
19344 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
19345 }
19346
19347 return $action;
19348 }
19349
19350 /**
19351 * Returns a string that can be used to generate a unique action name,
19352 * option name, HTML element ID, or HTML element class.
19353 *
19354 * @author Leo Fajardo (@leorw)
19355 * @since 1.2.2
19356 *
19357 * @return string
19358 */
19359 public function get_unique_affix() {
19360 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
19361 }
19362
19363 /**
19364 * Returns a string that can be used to generate a unique action name,
19365 * option name, HTML element ID, or HTML element class.
19366 *
19367 * @author Vova Feldman (@svovaf)
19368 * @since 1.2.2.5
19369 *
19370 * @param string $slug
19371 * @param bool $is_plugin
19372 *
19373 * @return string
19374 */
19375 static function get_module_unique_affix( $slug, $is_plugin = true ) {
19376 $affix = $slug;
19377
19378 if ( ! $is_plugin ) {
19379 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
19380 }
19381
19382 return $affix;
19383 }
19384
19385 /**
19386 * @author Vova Feldman (@svovaf)
19387 * @since 1.2.1
19388 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19389 * based on the slug for backward compatibility.
19390 *
19391 * @param string $tag
19392 *
19393 * @return string
19394 */
19395 function get_ajax_action( $tag ) {
19396 return self::get_ajax_action_static( $tag, $this->_module_id );
19397 }
19398
19399 /**
19400 * @author Vova Feldman (@svovaf)
19401 * @since 1.2.1.7
19402 *
19403 * @param string $tag
19404 *
19405 * @return string
19406 */
19407 function get_ajax_security( $tag ) {
19408 return wp_create_nonce( $this->get_ajax_action( $tag ) );
19409 }
19410
19411 /**
19412 * @author Vova Feldman (@svovaf)
19413 * @since 1.2.1.7
19414 *
19415 * @param string $tag
19416 */
19417 function check_ajax_referer( $tag ) {
19418 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
19419 }
19420
19421 /**
19422 * @author Vova Feldman (@svovaf)
19423 * @since 1.2.1.6
19424 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19425 * based on the slug for backward compatibility.
19426 *
19427 * @param string $tag
19428 * @param number|null $module_id
19429 *
19430 * @return string
19431 */
19432 static function get_ajax_action_static( $tag, $module_id = null ) {
19433 $action = "fs_{$tag}";
19434
19435 if ( ! empty( $module_id ) ) {
19436 $action .= "_{$module_id}";
19437 }
19438
19439 return $action;
19440 }
19441
19442 /**
19443 * Do action, specific for the current context plugin.
19444 *
19445 * @author Vova Feldman (@svovaf)
19446 * @since 1.0.1
19447 *
19448 * @param string $tag The name of the action to be executed.
19449 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
19450 * functions hooked to the action. Default empty.
19451 *
19452 * @uses do_action()
19453 */
19454 function do_action( $tag, $arg = '' ) {
19455 $args = func_get_args();
19456
19457 $this->_logger->entrance( $tag );
19458
19459 call_user_func_array( 'do_action', array_merge(
19460 array( $this->get_action_tag( $tag ) ),
19461 array_slice( $args, 1 ) )
19462 );
19463 }
19464
19465 /**
19466 * Add action, specific for the current context plugin.
19467 *
19468 * @author Vova Feldman (@svovaf)
19469 * @since 1.0.1
19470 *
19471 * @param string $tag
19472 * @param callable $function_to_add
19473 * @param int $priority
19474 * @param int $accepted_args
19475 *
19476 * @uses add_action()
19477 */
19478 function add_action(
19479 $tag,
19480 $function_to_add,
19481 $priority = WP_FS__DEFAULT_PRIORITY,
19482 $accepted_args = 1
19483 ) {
19484 $this->_logger->entrance( $tag );
19485
19486 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19487 }
19488
19489 /**
19490 * Add AJAX action, specific for the current context plugin.
19491 *
19492 * @author Vova Feldman (@svovaf)
19493 * @since 1.2.1
19494 *
19495 * @param string $tag
19496 * @param callable $function_to_add
19497 * @param int $priority
19498 *
19499 * @uses add_action()
19500 *
19501 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19502 */
19503 function add_ajax_action(
19504 $tag,
19505 $function_to_add,
19506 $priority = WP_FS__DEFAULT_PRIORITY
19507 ) {
19508 $this->_logger->entrance( $tag );
19509
19510 return self::add_ajax_action_static(
19511 $tag,
19512 $function_to_add,
19513 $priority,
19514 $this->_module_id
19515 );
19516 }
19517
19518 /**
19519 * Add AJAX action.
19520 *
19521 * @author Vova Feldman (@svovaf)
19522 * @since 1.2.1.6
19523 *
19524 * @param string $tag
19525 * @param callable $function_to_add
19526 * @param int $priority
19527 * @param number|null $module_id
19528 *
19529 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19530 * @uses add_action()
19531 *
19532 */
19533 static function add_ajax_action_static(
19534 $tag,
19535 $function_to_add,
19536 $priority = WP_FS__DEFAULT_PRIORITY,
19537 $module_id = null
19538 ) {
19539 self::$_static_logger->entrance( $tag );
19540
19541 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
19542 return false;
19543 }
19544
19545 add_action(
19546 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
19547 $function_to_add,
19548 $priority,
19549 0
19550 );
19551
19552 self::$_static_logger->info( "$tag AJAX callback action added." );
19553
19554 return true;
19555 }
19556
19557 /**
19558 * Send a JSON response back to an Ajax request.
19559 *
19560 * @author Vova Feldman (@svovaf)
19561 * @since 1.2.1.5
19562 *
19563 * @param mixed $response
19564 */
19565 static function shoot_ajax_response( $response ) {
19566 wp_send_json( $response );
19567 }
19568
19569 /**
19570 * Send a JSON response back to an Ajax request, indicating success.
19571 *
19572 * @author Vova Feldman (@svovaf)
19573 * @since 1.2.1.5
19574 *
19575 * @param mixed $data Data to encode as JSON, then print and exit.
19576 */
19577 static function shoot_ajax_success( $data = null ) {
19578 wp_send_json_success( $data );
19579 }
19580
19581 /**
19582 * Send a JSON response back to an Ajax request, indicating failure.
19583 *
19584 * @author Vova Feldman (@svovaf)
19585 * @since 1.2.1.5
19586 *
19587 * @param mixed $error Optional error message.
19588 */
19589 static function shoot_ajax_failure( $error = '' ) {
19590 $result = array( 'success' => false );
19591 if ( ! empty( $error ) ) {
19592 $result['error'] = $error;
19593 }
19594
19595 wp_send_json( $result );
19596 }
19597
19598 /**
19599 * Returns an AJAX URL with a special extra param to indicate whether the request was triggered from the network admin or blog admin.
19600 *
19601 * @author Vova Feldman (@svovaf)
19602 * @since 2.5.1
19603 *
19604 * @param string $wrap_with By default, returns the AJAX URL wrapped with single quotes.
19605 *
19606 * @return string
19607 */
19608 static function ajax_url( $wrap_with = "'") {
19609 if ( fs_is_network_admin() ) {
19610 $param_name = '_fs_network_admin';
19611 } else {
19612 $param_name = '_fs_blog_admin';
19613 }
19614
19615 $url = admin_url( 'admin-ajax.php', 'relative' );
19616 $url .= ( false === strpos( $url, '?' ) ) ? '?' : '&';
19617 $url .= "{$param_name}=true";
19618
19619 return "{$wrap_with}{$url}{$wrap_with}";
19620 }
19621
19622 /**
19623 * Apply filter, specific for the current context plugin.
19624 *
19625 * @author Vova Feldman (@svovaf)
19626 * @since 1.0.9
19627 *
19628 * @param string $tag The name of the filter hook.
19629 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
19630 *
19631 * @return mixed The filtered value after all hooked functions are applied to it.
19632 *
19633 * @uses apply_filters()
19634 */
19635 function apply_filters( $tag, $value ) {
19636 $args = func_get_args();
19637
19638 $this->_logger->entrance( $tag );
19639
19640 array_unshift( $args, $this->get_unique_affix() );
19641
19642 return call_user_func_array( 'fs_apply_filter', $args );
19643 }
19644
19645 /**
19646 * Add filter, specific for the current context plugin.
19647 *
19648 * @author Vova Feldman (@svovaf)
19649 * @since 1.0.9
19650 *
19651 * @param string $tag
19652 * @param callable $function_to_add
19653 * @param int $priority
19654 * @param int $accepted_args
19655 *
19656 * @uses add_filter()
19657 */
19658 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
19659 $this->_logger->entrance( $tag );
19660
19661 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19662 }
19663
19664 /**
19665 * Check if has filter.
19666 *
19667 * @author Vova Feldman (@svovaf)
19668 * @since 1.1.4
19669 *
19670 * @param string $tag
19671 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
19672 *
19673 * @return false|int
19674 *
19675 * @uses has_filter()
19676 */
19677 function has_filter( $tag, $function_to_check = false ) {
19678 $this->_logger->entrance( $tag );
19679
19680 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
19681 }
19682
19683 #endregion
19684
19685 /**
19686 * Override default i18n text phrases.
19687 *
19688 * @author Vova Feldman (@svovaf)
19689 * @since 1.1.6
19690 *
19691 * @param string[] string $key_value
19692 *
19693 * @uses fs_override_i18n()
19694 */
19695 function override_i18n( $key_value ) {
19696 fs_override_i18n( $key_value, $this->_slug );
19697 }
19698
19699 /* Account Page
19700 ------------------------------------------------------------------------------------------------------------------*/
19701 /**
19702 * Update site information.
19703 *
19704 * @author Vova Feldman (@svovaf)
19705 * @since 1.0.1
19706 *
19707 * @param bool $store Flush to Database if true.
19708 * @param null|int $network_level_or_blog_id Since 2.0.0
19709 * @param \FS_Site $site Since 2.0.0
19710 */
19711 private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null, $is_backup = false ) {
19712 $this->_logger->entrance();
19713
19714 if ( is_null( $site ) ) {
19715 $site = $this->_site;
19716 }
19717
19718 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
19719 $this->_logger->error( "Empty install ID, can't store site." );
19720
19721 return;
19722 }
19723
19724 $site_clone = clone $site;
19725
19726 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id, $is_backup );
19727
19728 if (
19729 ! $is_backup &&
19730 is_object( $this->_user ) && $this->_user->id != $site->user_id
19731 ) {
19732 $this->sync_user_by_current_install( $site->user_id );
19733
19734 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
19735
19736 if ( empty( $prev_stored_user_id ) &&
19737 is_object($this->_user) && $this->_user->id != $site->user_id
19738 ) {
19739 /**
19740 * Store the current user ID as the previous user ID so that the previous user can be used
19741 * as the install's owner while the new owner's details are not yet available.
19742 *
19743 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
19744 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
19745 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
19746 * into `replica`.
19747 *
19748 * @author Leo Fajardo (@leorw)
19749 */
19750 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
19751 }
19752 }
19753
19754 $sites[ $this->_slug ] = $site_clone;
19755
19756 $this->set_account_option(
19757 ( $is_backup ? 'prev_' : '' ) . 'sites',
19758 $sites,
19759 $store,
19760 $network_level_or_blog_id
19761 );
19762 }
19763
19764 /**
19765 * 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).
19766 *
19767 * @author Leo Fajardo (@leorw)
19768 * @since 2.5.0
19769 */
19770 private function back_up_site() {
19771 $this->_logger->entrance();
19772
19773 $site_clone = clone $this->_site;
19774
19775 $this->_store_site( true, null, $site_clone, true );
19776 }
19777
19778 /**
19779 * Update plugin's plans information.
19780 *
19781 * @author Vova Feldman (@svovaf)
19782 * @since 1.0.2
19783 *
19784 * @param bool $store Flush to Database if true.
19785 */
19786 private function _store_plans( $store = true ) {
19787 $this->_logger->entrance();
19788
19789 $plans = self::get_all_plans( $this->_module_type );
19790
19791 // Copy plans.
19792 $encrypted_plans = array();
19793 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
19794 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
19795 }
19796
19797 $plans[ $this->_slug ] = $encrypted_plans;
19798
19799 $this->set_account_option( 'plans', $plans, $store );
19800 }
19801
19802 /**
19803 * Update user's plugin licenses.
19804 *
19805 * @author Vova Feldman (@svovaf)
19806 * @since 1.0.5
19807 *
19808 * @param bool $store
19809 * @param number|bool $module_id
19810 * @param FS_Plugin_License[] $licenses
19811 */
19812 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
19813 $this->_logger->entrance();
19814
19815 $all_licenses = self::get_all_licenses();
19816
19817 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
19818 $module_id = $this->_module_id;
19819
19820 $user_licenses = is_array( $this->_licenses ) ?
19821 $this->_licenses :
19822 array();
19823
19824 if ( empty( $user_licenses ) ) {
19825 // If the context user doesn't have any license, don't update the licenses collection.
19826 return;
19827 }
19828
19829 $new_user_licenses_map = array();
19830 foreach ( $user_licenses as $user_license ) {
19831 $new_user_licenses_map[ $user_license->id ] = $user_license;
19832 }
19833
19834 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
19835
19836 // Update user licenses.
19837 $licenses_to_update_count = count( $new_user_licenses_map );
19838 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
19839 if ( 0 === $licenses_to_update_count ) {
19840 break;
19841 }
19842
19843 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
19844 // Update license.
19845 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
19846 unset( $new_user_licenses_map[ $license->id ] );
19847
19848 $licenses_to_update_count --;
19849 }
19850 }
19851
19852 if ( ! empty( $new_user_licenses_map ) ) {
19853 // Add new licenses.
19854 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
19855 }
19856
19857 $licenses = $all_licenses[ $module_id ];
19858 }
19859
19860 if ( ! isset( $all_licenses[ $module_id ] ) ) {
19861 $all_licenses[ $module_id ] = array();
19862 }
19863
19864 $all_licenses[ $module_id ] = $licenses;
19865
19866 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
19867 }
19868
19869 /**
19870 * Update user information.
19871 *
19872 * @author Vova Feldman (@svovaf)
19873 * @since 1.0.1
19874 *
19875 * @param bool $store Flush to Database if true.
19876 */
19877 private function _store_user( $store = true ) {
19878 $this->_logger->entrance();
19879
19880 if ( empty( $this->_user->id ) ) {
19881 $this->_logger->error( "Empty user ID, can't store user." );
19882
19883 return;
19884 }
19885
19886 $users = self::get_all_users();
19887 $users[ $this->_user->id ] = $this->_user;
19888 self::$_accounts->set_option( 'users', $users, $store );
19889 }
19890
19891 /**
19892 * Update new updates information.
19893 *
19894 * @author Vova Feldman (@svovaf)
19895 * @since 1.0.4
19896 *
19897 * @param FS_Plugin_Tag|null $update
19898 * @param bool $store Flush to Database if true.
19899 * @param bool|number $plugin_id
19900 */
19901 private function _store_update( $update, $store = true, $plugin_id = false ) {
19902 $this->_logger->entrance();
19903
19904 if ( $update instanceof FS_Plugin_Tag ) {
19905 $update->updated = time();
19906 }
19907
19908 if ( ! is_numeric( $plugin_id ) ) {
19909 $plugin_id = $this->_plugin->id;
19910 }
19911
19912 $updates = self::get_all_updates();
19913 $updates[ $plugin_id ] = $update;
19914 self::$_accounts->set_option( 'updates', $updates, $store );
19915 }
19916
19917 /**
19918 * Update new updates information.
19919 *
19920 * @author Vova Feldman (@svovaf)
19921 * @since 1.0.6
19922 *
19923 * @param FS_Plugin[] $plugin_addons
19924 * @param bool $store Flush to Database if true.
19925 */
19926 private function _store_addons( $plugin_addons, $store = true ) {
19927 $this->_logger->entrance();
19928
19929 $addons = self::get_all_addons();
19930 $addons[ $this->_plugin->id ] = $plugin_addons;
19931 self::$_accounts->set_option( 'addons', $addons, $store );
19932 }
19933
19934 /**
19935 * Delete plugin's associated add-ons.
19936 *
19937 * @author Vova Feldman (@svovaf)
19938 * @since 1.0.8
19939 *
19940 * @param bool $store
19941 *
19942 * @return bool
19943 */
19944 private function _delete_account_addons( $store = true ) {
19945 $all_addons = self::get_all_account_addons();
19946
19947 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
19948 return false;
19949 }
19950
19951 unset( $all_addons[ $this->_plugin->id ] );
19952
19953 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
19954
19955 return true;
19956 }
19957
19958 /**
19959 * Update account add-ons list.
19960 *
19961 * @author Vova Feldman (@svovaf)
19962 * @since 1.0.6
19963 *
19964 * @param FS_Plugin[] $addons
19965 * @param bool $store Flush to Database if true.
19966 */
19967 private function _store_account_addons( $addons, $store = true ) {
19968 $this->_logger->entrance();
19969
19970 $all_addons = self::get_all_account_addons();
19971 $all_addons[ $this->_plugin->id ] = $addons;
19972 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
19973 }
19974
19975 /**
19976 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
19977 * the valid user licenses will be fetched again and the account add-ons may be updated.
19978 *
19979 * @author Leo Fajardo (@leorw)
19980 * @since 2.2.4
19981 */
19982 private function purge_valid_user_licenses_cache() {
19983 if ( ! $this->is_registered() ) {
19984 return;
19985 }
19986
19987 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
19988 }
19989
19990 /**
19991 * @author Leo Fajardo (@leorw)
19992 * @since 2.3.0
19993 *
19994 * @param array $all_licenses
19995 * @param number|null $site_license_id
19996 * @param bool $include_parent_licenses
19997 *
19998 * @return array
19999 */
20000 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
20001 $foreign_licenses = array(
20002 'ids' => array(),
20003 'license_keys' => array()
20004 );
20005
20006 $parent_license_ids_map = array();
20007
20008 foreach ( $all_licenses as $license ) {
20009 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
20010 continue;
20011 }
20012
20013 $foreign_licenses['ids'][] = $license->id;
20014 $foreign_licenses['license_keys'][] = $license->secret_key;
20015
20016 if (
20017 $include_parent_licenses &&
20018 is_object( $this->_license ) &&
20019 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
20020 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
20021 ) {
20022 /**
20023 * Include the parent license's info only if it has not been included before since child licenses
20024 * can have the same parent license.
20025 */
20026 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
20027 $foreign_licenses['license_keys'][] = $license->secret_key;
20028
20029 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
20030 }
20031 }
20032
20033 if ( empty( $foreign_licenses['ids'] ) ) {
20034 $foreign_licenses = array();
20035 }
20036
20037 return $foreign_licenses;
20038 }
20039
20040 /**
20041 * @author Leo Fajardo (@leorw)
20042 * @since 2.3.0
20043 *
20044 * @return string
20045 */
20046 private function get_valid_user_licenses_endpoint() {
20047 $user_licenses_endpoint = '/licenses.json?type=active' .
20048 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
20049
20050 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
20051
20052 if ( ! empty ( $foreign_licenses ) ) {
20053 $foreign_licenses = array(
20054 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20055 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20056 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20057 );
20058
20059 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20060 }
20061
20062 return $user_licenses_endpoint;
20063 }
20064
20065 /**
20066 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
20067 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
20068 * method filters out non–add-on product IDs and stores the add-on IDs.
20069 *
20070 * @author Leo Fajardo (@leorw)
20071 * @since 2.2.4
20072 *
20073 * @return stdClass[] array
20074 */
20075 private function fetch_valid_user_licenses() {
20076 $this->_logger->entrance();
20077
20078 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
20079
20080 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
20081 ! is_array( $result->licenses )
20082 ) {
20083 return array();
20084 }
20085
20086 return $result->licenses;
20087 }
20088
20089 /**
20090 * @author Leo Fajardo (@leorw)
20091 * @since 2.2.4
20092 *
20093 * @return number[] Account add-on IDs.
20094 */
20095 function get_updated_account_addons() {
20096 $addons = $this->get_addons();
20097 if ( empty( $addons ) ) {
20098 return array();
20099 }
20100
20101 $account_addons = $this->get_account_addons();
20102 if ( ! is_array( $account_addons ) ) {
20103 $account_addons = array();
20104 }
20105
20106 $user_licenses = $this->is_registered() ?
20107 $this->fetch_valid_user_licenses() :
20108 array();
20109
20110 if ( empty( $user_licenses ) ) {
20111 return $account_addons;
20112 }
20113
20114 $addon_ids = array();
20115 foreach ( $addons as $addon ) {
20116 $addon_ids[] = $addon->id;
20117 }
20118
20119 $license_product_ids = array();
20120
20121 foreach ( $user_licenses as $license ) {
20122 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
20123 $license_product_ids = array_merge( $license_product_ids, $license->products );
20124 } else {
20125 $license_product_ids[] = $license->plugin_id;
20126 }
20127 }
20128
20129 // Filter out non–add-on IDs.
20130 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
20131 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
20132 $this->_store_account_addons( array_unique( $new_account_addons ) );
20133 }
20134
20135 return $new_account_addons;
20136 }
20137
20138 /**
20139 * Store account params in the Database.
20140 *
20141 * @author Vova Feldman (@svovaf)
20142 * @since 1.0.1
20143 *
20144 * @param null|int $blog_id Since 2.0.0
20145 */
20146 private function _store_account( $blog_id = null ) {
20147 $this->_logger->entrance();
20148
20149 $this->_store_site( false, $blog_id );
20150 $this->_store_user( false );
20151 $this->_store_plans( false );
20152 $this->_store_licenses( false );
20153
20154 self::$_accounts->store( $blog_id );
20155 }
20156
20157 /**
20158 * Sync user's information.
20159 *
20160 * @author Vova Feldman (@svovaf)
20161 * @since 1.0.3
20162 * @uses FS_Api
20163 */
20164 private function _handle_account_user_sync() {
20165 $this->_logger->entrance();
20166
20167 $api = $this->get_api_user_scope();
20168
20169 // Get user's information.
20170 $user = $api->get( '/', true );
20171
20172 if ( isset( $user->id ) ) {
20173 $this->_user->first = $user->first;
20174 $this->_user->last = $user->last;
20175 $this->_user->email = $user->email;
20176
20177 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
20178
20179 if ( $user->is_verified &&
20180 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
20181 ) {
20182 $this->_user->is_verified = true;
20183
20184 $this->do_action( 'account_email_verified', $user->email );
20185
20186 $this->_admin_notices->add(
20187 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
20188 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
20189 'success',
20190 // Make admin sticky if account menu item is invisible,
20191 // since the page will be auto redirected to the plugin's
20192 // main settings page, and the non-sticky message
20193 // will disappear.
20194 ! $is_menu_item_account_visible,
20195 'email_verified'
20196 );
20197 }
20198
20199 // Flush user details to DB.
20200 $this->_store_user();
20201
20202 $this->do_action( 'after_account_user_sync', $user );
20203
20204 /**
20205 * If account menu item is hidden, redirect to plugin's main settings page.
20206 *
20207 * @author Vova Feldman (@svovaf)
20208 * @since 1.1.6
20209 *
20210 * @link https://github.com/Freemius/wordpress-sdk/issues/6
20211 */
20212 if ( ! $is_menu_item_account_visible ) {
20213 fs_redirect( $this->_get_admin_page_url() );
20214 }
20215 }
20216 }
20217
20218 /**
20219 * @author Vova Feldman (@svovaf)
20220 * @since 1.0.9
20221 * @uses FS_Api
20222 *
20223 * @param number|bool $license_id
20224 *
20225 * @return FS_Subscription|object|bool
20226 */
20227 private function _fetch_site_license_subscription( $license_id = false ) {
20228 $this->_logger->entrance();
20229 $api = $this->get_api_site_scope();
20230
20231 if ( ! is_numeric( $license_id ) ) {
20232 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
20233 $this->_license->parent_license_id :
20234 $this->_license->id;
20235 }
20236
20237 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
20238
20239 return ! isset( $result->error ) ?
20240 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
20241 new FS_Subscription( $result->subscriptions[0] ) :
20242 false
20243 ) :
20244 $result;
20245 }
20246
20247 /**
20248 * @author Vova Feldman (@svovaf)
20249 * @since 1.0.4
20250 * @uses FS_Api
20251 *
20252 * @param number|bool $plan_id
20253 *
20254 * @return FS_Plugin_Plan|object
20255 */
20256 private function _fetch_site_plan( $plan_id = false ) {
20257 $this->_logger->entrance();
20258 $api = $this->get_api_site_scope();
20259
20260 if ( ! is_numeric( $plan_id ) ) {
20261 $plan_id = $this->_site->plan_id;
20262 }
20263
20264 $plan = $api->get( "/plans/{$plan_id}.json", true );
20265
20266 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
20267 }
20268
20269 /**
20270 * @author Vova Feldman (@svovaf)
20271 * @since 1.0.5
20272 * @uses FS_Api
20273 *
20274 * @return FS_Plugin_Plan[]|object
20275 */
20276 private function _fetch_plugin_plans() {
20277 $this->_logger->entrance();
20278 $api = $this->get_current_or_network_user_api_scope();
20279
20280 /**
20281 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
20282 */
20283 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
20284
20285 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
20286 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
20287 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
20288 }
20289
20290 $result = $result->plans;
20291 }
20292
20293 return $result;
20294 }
20295
20296 /**
20297 * @author Vova Feldman (@svovaf)
20298 * @since 2.0.0
20299 *
20300 * @param number $plan_id
20301 *
20302 * @return \FS_Plugin_Plan|object
20303 */
20304 private function fetch_plan_by_id( $plan_id ) {
20305 $this->_logger->entrance();
20306 $api = $this->get_current_or_network_user_api_scope();
20307
20308 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
20309
20310 return $this->is_api_result_entity( $result ) ?
20311 new FS_Plugin_Plan( $result ) :
20312 $result;
20313 }
20314
20315 /**
20316 * @author Vova Feldman (@svovaf)
20317 * @since 1.0.5
20318 * @uses FS_Api
20319 *
20320 * @param number|bool $plugin_id
20321 * @param number|bool $site_license_id
20322 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
20323 * @param number|null $blog_id
20324 *
20325 * @return FS_Plugin_License[]|object
20326 */
20327 private function _fetch_licenses(
20328 $plugin_id = false,
20329 $site_license_id = false,
20330 $foreign_licenses = array(),
20331 $blog_id = null
20332 ) {
20333 $this->_logger->entrance();
20334
20335 $api = $this->get_api_user_scope();
20336
20337 if ( ! is_numeric( $plugin_id ) ) {
20338 $plugin_id = $this->_plugin->id;
20339 }
20340
20341 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
20342 if ( ! empty ( $foreign_licenses ) ) {
20343 $foreign_licenses = array(
20344 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20345 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20346 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20347 );
20348
20349 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20350 }
20351
20352 $result = $api->get( $user_licenses_endpoint, true );
20353
20354 $is_site_license_synced = false;
20355
20356 $api_errors = array();
20357
20358 if ( $this->is_api_result_object( $result, 'licenses' ) &&
20359 is_array( $result->licenses )
20360 ) {
20361 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
20362 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
20363
20364 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
20365 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
20366 }
20367 }
20368
20369 $result = $result->licenses;
20370 } else {
20371 $api_errors[] = $result;
20372 $result = array();
20373 }
20374
20375 if ( ! $is_site_license_synced ) {
20376 if ( ! is_null( $blog_id ) ) {
20377 /**
20378 * If blog ID is not null, the request is for syncing of the license of a single site via the
20379 * network-level "Account" page.
20380 *
20381 * @author Leo Fajardo (@leorw)
20382 */
20383 $this->switch_to_blog( $blog_id );
20384 }
20385
20386 $api = $this->get_api_site_scope();
20387
20388 if ( is_numeric( $site_license_id ) ) {
20389 // Try to retrieve a foreign license that is linked to the install.
20390 $api_result = $api->call( '/licenses.json?is_enriched=true' );
20391
20392 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
20393 is_array( $api_result->licenses )
20394 ) {
20395 $licenses = $api_result->licenses;
20396
20397 if ( ! empty( $licenses ) ) {
20398 $result[] = new FS_Plugin_License( $licenses[0] );
20399 }
20400 } else {
20401 $api_errors[] = $api_result;
20402 }
20403 } else if (
20404 is_object( $this->_license ) &&
20405 /**
20406 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
20407 * the FS instance that does the syncing is the parent FS instance.
20408 *
20409 * @author Leo Fajardo (@leorw)
20410 * @since 2.3.0
20411 */
20412 $this->_license->plugin_id == $plugin_id
20413 ) {
20414 $is_license_in_result = false;
20415 if ( ! empty( $result ) ) {
20416 foreach ( $result as $license ) {
20417 if ( $license->id == $this->_license->id ) {
20418 $is_license_in_result = true;
20419 break;
20420 }
20421 }
20422 }
20423
20424 if ( ! $is_license_in_result ) {
20425 // Fetch foreign license by ID and license key.
20426 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
20427 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
20428
20429 if ( $this->is_api_result_entity( $license ) ) {
20430 $result[] = new FS_Plugin_License( $license );
20431 } else {
20432 $api_errors[] = $license;
20433 }
20434 }
20435 }
20436
20437 if ( ! is_null( $blog_id ) ) {
20438 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20439 }
20440 }
20441
20442 if ( is_array( $result ) && 0 < count( $result ) ) {
20443 // If found at least one license, return license collection even if there are errors.
20444 return $result;
20445 }
20446
20447 if ( ! empty( $api_errors ) ) {
20448 // If found any errors and no licenses, return first error.
20449 return $api_errors[0];
20450 }
20451
20452 // Fallback to empty licenses list.
20453 return $result;
20454 }
20455
20456 /**
20457 * @author Vova Feldman (@svovaf)
20458 * @since 2.0.0
20459 *
20460 * @param number $license_id
20461 * @param string $license_key
20462 *
20463 * @return \FS_Plugin_License|object
20464 */
20465 private function fetch_license_by_key( $license_id, $license_key ) {
20466 $this->_logger->entrance();
20467
20468 $api = $this->get_current_or_network_user_api_scope();
20469
20470 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
20471
20472 return $this->is_api_result_entity( $result ) ?
20473 new FS_Plugin_License( $result ) :
20474 $result;
20475 }
20476
20477 /**
20478 * @author Vova Feldman (@svovaf)
20479 * @since 1.2.0
20480 * @uses FS_Api
20481 *
20482 * @param number|bool $plugin_id
20483 * @param bool $flush
20484 *
20485 * @return FS_Payment[]|object
20486 */
20487 function _fetch_payments( $plugin_id = false, $flush = false ) {
20488 $this->_logger->entrance();
20489
20490 $api = $this->get_api_user_scope();
20491
20492 if ( ! is_numeric( $plugin_id ) ) {
20493 $plugin_id = $this->_plugin->id;
20494 }
20495
20496 $include_bundles = (
20497 is_object( $this->_plugin ) &&
20498 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
20499 );
20500
20501 $result = $api->get(
20502 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
20503 $flush
20504 );
20505
20506 if ( ! isset( $result->error ) ) {
20507 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
20508 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
20509 }
20510 $result = $result->payments;
20511 }
20512
20513 return $result;
20514 }
20515
20516 /**
20517 * @author Vova Feldman (@svovaf)
20518 * @since 1.2.1.5
20519 * @uses FS_Api
20520 *
20521 * @param bool $flush
20522 *
20523 * @return \FS_Billing|mixed
20524 */
20525 function _fetch_billing( $flush = false ) {
20526 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
20527
20528 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
20529
20530 if ( $this->is_api_result_entity( $billing ) ) {
20531 $billing = new FS_Billing( $billing );
20532 }
20533
20534 return $billing;
20535 }
20536
20537 /**
20538 * @author Vova Feldman (@svovaf)
20539 * @since 1.0.5
20540 *
20541 * @param FS_Plugin_License[] $licenses
20542 * @param number $module_id
20543 */
20544 private function _update_licenses( $licenses, $module_id ) {
20545 $this->_logger->entrance();
20546
20547 if ( is_array( $licenses ) ) {
20548 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
20549 $licenses[ $i ]->updated = time();
20550 }
20551 }
20552
20553 $this->_store_licenses( true, $module_id, $licenses );
20554 }
20555
20556 /**
20557 * @author Vova Feldman (@svovaf)
20558 * @since 1.0.4
20559 *
20560 * @param bool|number $plugin_id
20561 * @param bool $flush Since 1.1.7.3
20562 * @param int $expiration Since 1.2.2.7
20563 * @param bool|string $newer_than Since 2.2.1
20564 *
20565 * @return object|false New plugin tag info if exist.
20566 */
20567 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
20568 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
20569
20570 if ( ! is_object( $latest_tag ) ) {
20571 return false;
20572 }
20573
20574 $plugin_version = $this->get_plugin_version();
20575
20576 // Check if version is actually newer.
20577 $has_new_version =
20578 // If it's an non-installed add-on then always return latest.
20579 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
20580 // Compare versions.
20581 version_compare( $plugin_version, $latest_tag->version, '<' );
20582
20583 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
20584
20585 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
20586
20587 $this->_storage->beta_data = array(
20588 'is_beta' => $is_latest_version_beta,
20589 'version' => $latest_tag->version
20590 );
20591
20592 return $has_new_version ? $latest_tag : false;
20593 }
20594
20595 /**
20596 * @author Vova Feldman (@svovaf)
20597 * @since 1.0.5
20598 *
20599 * @param bool|number $plugin_id
20600 * @param bool $flush Since 1.1.7.3
20601 * @param int $expiration Since 1.2.2.7
20602 * @param bool|string $newer_than Since 2.2.1
20603 *
20604 * @return bool|FS_Plugin_Tag
20605 */
20606 function get_update( $plugin_id = false, $flush = true, $expiration = FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION, $newer_than = false ) {
20607 $this->_logger->entrance();
20608
20609 if ( ! is_numeric( $plugin_id ) ) {
20610 $plugin_id = $this->_plugin->id;
20611 }
20612
20613 $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than );
20614 $updates = $this->get_all_updates();
20615
20616 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
20617 }
20618
20619 /**
20620 * Check if site assigned with active license.
20621 *
20622 * @author Vova Feldman (@svovaf)
20623 * @since 1.0.6
20624 *
20625 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
20626 */
20627 function has_active_license() {
20628 return (
20629 is_object( $this->_license ) &&
20630 is_numeric( $this->_license->id ) &&
20631 ! $this->_license->is_expired()
20632 );
20633 }
20634
20635 /**
20636 * Check if site assigned with active & valid (not expired) license.
20637 *
20638 * @author Vova Feldman (@svovaf)
20639 * @since 1.2.1
20640 *
20641 * @param bool $check_expiration
20642 */
20643 function has_active_valid_license( $check_expiration = true ) {
20644 return self::is_active_valid_license( $this->_license, $check_expiration );
20645 }
20646
20647 /**
20648 * @author Leo Fajardo (@leorw)
20649 * @since 2.3.1
20650 */
20651 function is_data_debug_mode() {
20652 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
20653 return false;
20654 }
20655
20656 $fs = $this->is_addon() ?
20657 $this->get_parent_instance() :
20658 $this;
20659
20660 if ( $fs->is_network_active() && fs_is_network_admin() ) {
20661 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
20662 } else {
20663 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
20664 }
20665
20666 return ( 'true' === $is_developer_license_debug_mode );
20667 }
20668
20669 /**
20670 * @author Leo Fajardo (@leorw)
20671 * @since 2.3.1
20672 */
20673 function _set_data_debug_mode() {
20674 if ( ! $this->is_whitelabeled( true ) ) {
20675 return;
20676 }
20677
20678 $license_or_user_key = fs_request_get_raw( 'license_or_user_key' );
20679
20680 $transient_value = ( ! empty( $license_or_user_key ) ) ?
20681 'true' :
20682 'false';
20683
20684 if ( 'true' === $transient_value ) {
20685 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
20686 'last_license_key' :
20687 'last_license_user_key'
20688 );
20689
20690 if ( md5( $license_or_user_key ) !== $stored_key ) {
20691 $this->shoot_ajax_failure( sprintf(
20692 '%s... %s',
20693 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
20694 $this->get_text_inline(
20695 'seems like the key you entered doesn\'t match our records.',
20696 'developer-or-license-not-found'
20697 )
20698 ) );
20699 }
20700 }
20701
20702 if ( $this->is_network_active() && fs_is_network_admin() ) {
20703 set_site_transient(
20704 "fs_{$this->get_id()}_data_debug_mode",
20705 $transient_value,
20706 WP_FS__TIME_24_HOURS_IN_SEC / 24
20707 );
20708 } else {
20709 set_transient(
20710 "fs_{$this->get_id()}_data_debug_mode",
20711 $transient_value,
20712 WP_FS__TIME_24_HOURS_IN_SEC / 24
20713 );
20714 }
20715
20716 if ( 'true' === $transient_value ) {
20717 $this->_admin_notices->add_sticky(
20718 $this->get_text_inline(
20719 '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.',
20720 'data_debug_mode_enabled'
20721 ),
20722 'data_debug_mode_enabled'
20723 );
20724 }
20725
20726 $this->shoot_ajax_success();
20727 }
20728
20729 /**
20730 * Check if a given license is active & valid (not expired).
20731 *
20732 * @author Vova Feldman (@svovaf)
20733 * @since 2.1.3
20734 *
20735 * @param FS_Plugin_License $license
20736 * @param bool $check_expiration
20737 *
20738 * @return bool
20739 */
20740 private static function is_active_valid_license( $license, $check_expiration = true ) {
20741 return (
20742 is_object( $license ) &&
20743 FS_Plugin_License::is_valid_id( $license->id ) &&
20744 $license->is_active() &&
20745 ( ! $check_expiration || $license->is_valid() )
20746 );
20747 }
20748
20749 /**
20750 * Checks if there's any site that is associated with an active & valid license.
20751 * This logic is used to determine if the admin can download the premium code base from a network level admin.
20752 *
20753 * @author Vova Feldman (@svovaf)
20754 * @since 2.1.3
20755 *
20756 * @return bool
20757 */
20758 function has_any_active_valid_license() {
20759 if ( ! fs_is_network_admin() ) {
20760 return $this->has_active_valid_license();
20761 }
20762
20763 $installs = $this->get_blog_install_map();
20764 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
20765
20766 foreach ( $installs as $blog_id => $install ) {
20767 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20768 continue;
20769 }
20770
20771 foreach ( $all_plugin_licenses as $license ) {
20772 if ( $license->id == $install->license_id ) {
20773 if ( self::is_active_valid_license( $license ) ) {
20774 return true;
20775 }
20776 }
20777 }
20778 }
20779
20780 return false;
20781 }
20782
20783 /**
20784 * Check if site assigned with license with enabled features.
20785 *
20786 * @author Vova Feldman (@svovaf)
20787 * @since 1.0.6
20788 *
20789 * @return bool
20790 */
20791 function has_features_enabled_license() {
20792 return (
20793 is_object( $this->_license ) &&
20794 is_numeric( $this->_license->id ) &&
20795 $this->_license->is_features_enabled()
20796 );
20797 }
20798
20799 /**
20800 * Checks if the product is activated with a bundle license.
20801 *
20802 * @author Leo Fajardo (@leorw)
20803 * @since 2.4.0
20804 *
20805 * @return bool
20806 */
20807 function is_activated_with_bundle_license() {
20808 if ( ! $this->has_features_enabled_license() ) {
20809 return false;
20810 }
20811
20812 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
20813 }
20814
20815 /**
20816 * Check if user is a trial or have feature enabled license.
20817 *
20818 * @author Vova Feldman (@svovaf)
20819 * @since 1.1.7
20820 *
20821 * @return bool
20822 */
20823 function can_use_premium_code() {
20824 return $this->is_trial() || $this->has_features_enabled_license();
20825 }
20826
20827 /**
20828 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
20829 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
20830 * the context user is authenticated.
20831 *
20832 * @author Leo Fajardo (@leorw)
20833 * @since 1.2.2
20834 *
20835 * @return bool
20836 */
20837 function is_user_admin() {
20838 /**
20839 * Require a super-admin when network activated, running from the network level OR if
20840 * running from the site level but not delegated the opt-in.
20841 *
20842 * @author Vova Feldman (@svovaf)
20843 * @since 2.0.0
20844 */
20845 if ( $this->_is_network_active &&
20846 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
20847 ) {
20848 return is_super_admin();
20849 }
20850
20851 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
20852 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
20853 }
20854
20855 /**
20856 * Sync site's plan.
20857 *
20858 * @author Vova Feldman (@svovaf)
20859 * @since 1.0.3
20860 *
20861 * @uses FS_Api
20862 *
20863 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
20864 * the admin.
20865 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
20866 * network-level "Account" page.
20867 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
20868 * `_sync_plugin_license` method in order to switch to the previous blog when sending
20869 * updates for a single site in case `execute_cron` has switched to a different blog.
20870 */
20871 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
20872 $this->_logger->entrance();
20873
20874 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
20875
20876 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
20877
20878 if ( $is_addon_sync ) {
20879 $this->_sync_addon_license( $plugin_id, $background );
20880 } else {
20881 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
20882 }
20883
20884 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
20885 }
20886
20887 /**
20888 * Sync plugin's add-on license.
20889 *
20890 * @author Vova Feldman (@svovaf)
20891 * @since 1.0.6
20892 * @uses FS_Api
20893 *
20894 * @param number $addon_id
20895 * @param bool $background
20896 */
20897 private function _sync_addon_license( $addon_id, $background ) {
20898 $this->_logger->entrance();
20899
20900 if ( $this->is_addon_activated( $addon_id ) ) {
20901 // If already installed, use add-on sync.
20902 $fs_addon = self::get_instance_by_id( $addon_id );
20903
20904 if (
20905 // Add-on is network activated and network integrated.
20906 $fs_addon->is_network_active() ||
20907 // Background sync cron.
20908 self::is_cron() ||
20909 // Add-on is not network activated or not network integrated.
20910 ! fs_is_network_admin()
20911 ) {
20912 $fs_addon->_sync_license( $background );
20913
20914 return;
20915 }
20916 }
20917
20918 // Validate add-on exists.
20919 $addon = $this->get_addon( $addon_id );
20920
20921 if ( ! is_object( $addon ) ) {
20922 return;
20923 }
20924
20925 // Add add-on into account add-ons.
20926 $account_addons = $this->get_account_addons();
20927 if ( ! is_array( $account_addons ) ) {
20928 $account_addons = array();
20929 }
20930 $account_addons[] = $addon->id;
20931 $account_addons = array_unique( $account_addons );
20932 $this->_store_account_addons( $account_addons );
20933
20934 // Load add-on licenses.
20935 $licenses = $this->_fetch_licenses( $addon->id );
20936
20937 // Sync add-on licenses.
20938 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
20939 $this->_update_licenses( $licenses, $addon->id );
20940
20941 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
20942 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
20943
20944 if ( ! isset( $plans_result->error ) ) {
20945 $plans = array();
20946 foreach ( $plans_result->plans as $plan ) {
20947 $plans[] = new FS_Plugin_Plan( $plan );
20948 }
20949
20950 $this->_admin_notices->add_sticky(
20951 sprintf(
20952 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
20953 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
20954 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
20955 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
20956 $addon->title
20957 ) . ' ' . $this->get_latest_download_link(
20958 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
20959 $addon_id
20960 ),
20961 'addon_plan_upgraded_' . $addon->slug,
20962 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20963 );
20964 }
20965 }
20966 }
20967 }
20968
20969 /**
20970 * Sync site's plugin plan.
20971 *
20972 * @author Vova Feldman (@svovaf)
20973 * @since 1.0.6
20974 * @uses FS_Api
20975 *
20976 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
20977 * @param bool $send_installs_update Since 2.0.0
20978 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
20979 * syncing its license from the network-level "Account" page (e.g.: after
20980 * activating a license only for the single install).
20981 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
20982 * can be used here to switch to the previous blog in case `execute_cron`
20983 * has switched to a different blog.
20984 */
20985 private function _sync_plugin_license(
20986 $background = false,
20987 $send_installs_update = true,
20988 $is_context_single_site = false,
20989 $current_blog_id = null
20990 ) {
20991 $this->_logger->entrance();
20992
20993 $plan_change = 'none';
20994
20995 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
20996
20997 if ( ! $send_installs_update ) {
20998 $site = $this->_site;
20999 } else {
21000 /**
21001 * Sync site info.
21002 *
21003 * @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.
21004 */
21005 if ( $is_site_level_sync ) {
21006 /**
21007 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
21008 *
21009 * @author Leo Fajardo (@leorw)
21010 * @since 2.2.3
21011 */
21012 if ( is_numeric( $current_blog_id ) ) {
21013 $this->switch_to_blog( $current_blog_id );
21014 }
21015
21016 $result = $this->send_install_update( array(), true, true );
21017 $is_valid = $this->is_api_result_entity( $result );
21018 } else {
21019 $result = $this->send_installs_update( array(), true, true );
21020 $is_valid = $this->is_api_result_object( $result, 'installs' );
21021 }
21022
21023 if ( ! $is_valid ) {
21024 if ( $is_context_single_site ) {
21025 // Switch back to the main blog so that the following logic will have the right entities.
21026 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21027 }
21028
21029 // Show API message only if not background sync or if paying customer.
21030 if ( ! $background || $this->is_paying() ) {
21031 // Try to ping API to see if not blocked.
21032 if ( FS_Api::is_blocked( $result ) ) {
21033 /**
21034 * @author Vova Feldman (@svovaf)
21035 * @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.
21036 */
21037 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
21038 // Add notice immediately if not a background sync.
21039 $add_notice = ( ! $background );
21040
21041 if ( ! $add_notice ) {
21042 $counter = (int) get_transient( '_fs_api_connection_retry_counter' );
21043
21044 // We only want to add the notice after 3 consecutive failures.
21045 $add_notice = ( 3 <= $counter );
21046
21047 if ( ! $add_notice ) {
21048 /**
21049 * 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.
21050 *
21051 * 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.
21052 */
21053 set_transient( '_fs_api_connection_retry_counter', $counter + 1, WP_FS__TIME_WEEK_IN_SEC );
21054 }
21055 }
21056
21057 // Add notice instantly for not-background sync and only after 3 failed attempts for background sync.
21058 if ( $add_notice ) {
21059 self::$_global_admin_notices->add(
21060 $this->generate_api_blocked_notice_message_from_result( $result ),
21061 '',
21062 'error',
21063 $background,
21064 'api_blocked'
21065 );
21066
21067 add_action( 'admin_footer', array( 'Freemius', '_add_api_connectivity_notice_handler_js' ) );
21068
21069 // Notice was just shown, reset connectivity counter.
21070 delete_transient( '_fs_api_connection_retry_counter' );
21071 }
21072 }
21073 } else if ( is_object( $result ) ) {
21074 // Authentication params are broken.
21075 $this->_admin_notices->add(
21076 $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 ),
21077 '',
21078 'error'
21079 );
21080 }
21081 }
21082
21083 // No reason to continue with license sync while there are API issues.
21084 return;
21085 }
21086
21087 // API is working now. Delete the transient and start afresh.
21088 delete_transient('_fs_api_connection_retry_counter');
21089
21090 if ( $is_site_level_sync ) {
21091 $site = new FS_Site( $result );
21092 } else {
21093 // Map site addresses to their blog IDs.
21094 $address_to_blog_map = $this->get_address_to_blog_map();
21095
21096 // Find the current context install.
21097 $site = null;
21098 foreach ( $result->installs as $install ) {
21099 if ( $install->id == $this->_site->id ) {
21100 $site = new FS_Site( $install );
21101 } else {
21102 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
21103 $blog_id = $address_to_blog_map[ $address ];
21104
21105 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
21106 }
21107 }
21108 }
21109
21110 // Sync plans.
21111 $this->_sync_plans();
21112 }
21113
21114 // Remove sticky API connectivity message.
21115 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
21116
21117 if ( ! $this->has_paid_plan() ) {
21118 $this->_site = $site;
21119 $this->_store_site(
21120 true,
21121 $is_site_level_sync ?
21122 null :
21123 $this->get_network_install_blog_id()
21124 );
21125 } else {
21126 $context_blog_id = 0;
21127
21128 if ( $is_context_single_site ) {
21129 $context_blog_id = get_current_blog_id();
21130
21131 // Switch back to the main blog in order to properly sync the license.
21132 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21133 }
21134
21135 /**
21136 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
21137 * associated with that ID is not included in the user's licenses collection.
21138 * Save previous value to manage remote license renewals.
21139 */
21140 $was_license_expired_before_sync = is_object( $this->_license ) && $this->_license->is_expired();
21141 $this->_sync_licenses(
21142 $site->license_id,
21143 ( $is_context_single_site ?
21144 $context_blog_id :
21145 null
21146 )
21147 );
21148
21149 if ( $is_context_single_site ) {
21150 $this->switch_to_blog( $context_blog_id );
21151 }
21152
21153 // Check if plan / license changed.
21154 if ( $site->plan_id != $this->_site->plan_id ||
21155 // Check if trial started.
21156 $site->trial_plan_id != $this->_site->trial_plan_id ||
21157 $site->trial_ends != $this->_site->trial_ends ||
21158 // Check if license changed.
21159 $site->license_id != $this->_site->license_id
21160 ) {
21161 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
21162 // New trial started.
21163 $this->_site = $site;
21164 $plan_change = 'trial_started';
21165
21166 // For trial with subscription use-case.
21167 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
21168
21169 if ( is_object( $new_license ) && $new_license->is_valid() ) {
21170 $this->_site = $site;
21171 $this->_update_site_license( $new_license );
21172 $this->_store_licenses();
21173
21174 $this->_sync_site_subscription( $this->_license );
21175 }
21176 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
21177 // Was in trial, but now trial expired and no license ID.
21178 // New trial started.
21179 $this->_site = $site;
21180 $plan_change = 'trial_expired';
21181 } else {
21182 $is_free = $this->is_free_plan();
21183
21184 // Make sure license exist and not expired.
21185 $new_license = is_null( $site->license_id ) ?
21186 null :
21187 $this->_get_license_by_id( $site->license_id );
21188
21189 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
21190 // License cancelled.
21191 $this->_site = $site;
21192 $this->_update_site_license( $new_license );
21193 $this->_store_licenses();
21194
21195 $plan_change = 'cancelled';
21196 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
21197 // The license is expired, so ignore upgrade method.
21198 $this->_site = $site;
21199 } else {
21200 // License changed.
21201 $this->_site = $site;
21202
21203 /**
21204 * IMPORTANT:
21205 * 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.
21206 *
21207 * @author Vova Feldman (@svovaf)
21208 * @since 2.0.0
21209 */
21210 $this->_update_site_license( $new_license );
21211
21212 if ( ! $is_context_single_site &&
21213 fs_is_network_admin() &&
21214 $this->_is_network_active &&
21215 $new_license->quota > 1 &&
21216 get_blog_count() > 1
21217 ) {
21218 // See if license can activated on all sites.
21219 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
21220 if ( ! fs_request_get_bool( 'auto_install' ) ) {
21221 // Open the license activation dialog box on the account page.
21222 add_action( 'admin_footer', array(
21223 &$this,
21224 '_open_license_activation_dialog_box'
21225 ) );
21226 }
21227 }
21228 }
21229
21230 $this->_store_licenses();
21231
21232 $plan_change = $is_free ?
21233 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
21234 ( is_object( $new_license ) ?
21235 'changed' :
21236 'downgraded' );
21237 }
21238 }
21239
21240 // Store updated site info.
21241 $this->_store_site(
21242 true,
21243 $is_site_level_sync ?
21244 null :
21245 $this->get_network_install_blog_id()
21246 );
21247 } else {
21248 if ( ! is_object( $this->_license ) ) {
21249 $this->maybe_update_whitelabel_flag(
21250 FS_Plugin_License::is_valid_id( $site->license_id ) ?
21251 $this->get_license_by_id( $site->license_id ) :
21252 null
21253 );
21254 } else {
21255 $this->maybe_update_whitelabel_flag( $this->_license );
21256
21257 if ( $this->_license->is_expired() ) {
21258 if ( ! $this->has_features_enabled_license() ) {
21259 $this->_deactivate_license();
21260 $plan_change = 'downgraded';
21261 } else {
21262 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
21263
21264 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
21265 /**
21266 * Show the expired license notice every 14 days.
21267 *
21268 * @author Leo Fajardo (@leorw)
21269 * @since 2.3.1
21270 */
21271 $plan_change = 'expired';
21272 }
21273 }
21274 } else if ( $was_license_expired_before_sync ) {
21275 /**
21276 * If license was expired but it is not anymore.
21277 *
21278 *
21279 * @author Daniele Alessandra (@danielealessandra)
21280 */
21281 $plan_change = 'extended';
21282 }
21283 }
21284
21285 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
21286 $this->_sync_site_subscription( $this->_license );
21287 }
21288 }
21289
21290 if ( ! $this->is_addon() &&
21291 $this->_site->is_beta() !== $site->is_beta()
21292 ) {
21293 // Beta flag updated.
21294 $this->_site = $site;
21295
21296 $this->_store_site(
21297 true,
21298 $is_site_level_sync ?
21299 null :
21300 $this->get_network_install_blog_id()
21301 );
21302 }
21303
21304 if ( $this->is_addon() || $this->has_addons() ) {
21305 /**
21306 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21307 * an updated valid user licenses collection will be fetched from the server which is used to also
21308 * update the account add-ons (add-ons the user has licenses for).
21309 *
21310 * @author Leo Fajardo (@leorw)
21311 * @since 2.2.4
21312 */
21313 $this->purge_valid_user_licenses_cache();
21314 }
21315 }
21316
21317 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21318
21319 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
21320 switch ( $plan_change ) {
21321 case 'none':
21322 if ( ! $background && is_admin() ) {
21323 $plan = $this->is_trial() ?
21324 $this->get_trial_plan() :
21325 $this->get_plan();
21326
21327 if ( $plan->is_free() ) {
21328 $this->_admin_notices->add(
21329 sprintf(
21330 $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' ),
21331 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
21332 ) . ' ' . sprintf(
21333 '<a href="%s">%s</a>',
21334 $this->contact_url(
21335 'bug',
21336 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' ),
21337 strtoupper( $plan->name )
21338 )
21339 ),
21340 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21341 ),
21342 $hmm_text
21343 );
21344 }
21345 }
21346 break;
21347 case 'upgraded':
21348 case 'activated':
21349 $this->add_after_plan_activation_or_upgrade_instructions_notice( 'upgraded' === $plan_change );
21350
21351 $this->_admin_notices->remove_sticky( array(
21352 'trial_started',
21353 'trial_promotion',
21354 'trial_expired',
21355 'activation_complete',
21356 'license_expired',
21357 ) );
21358 break;
21359 case 'extended':
21360 $this->_admin_notices->remove_sticky( array(
21361 'trial_expired',
21362 'license_expired',
21363 ) );
21364 break;
21365 case 'changed':
21366 $this->_admin_notices->add_sticky(
21367 sprintf(
21368 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
21369 $this->get_plan_title()
21370 ),
21371 'plan_changed'
21372 );
21373
21374 $this->_admin_notices->remove_sticky( array(
21375 'trial_started',
21376 'trial_promotion',
21377 'trial_expired',
21378 'activation_complete',
21379 ) );
21380 break;
21381 case 'downgraded':
21382 $this->_admin_notices->add_sticky(
21383 ($this->has_free_plan() ?
21384 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 ) :
21385 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21386 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) ) ),
21387 'license_expired',
21388 $hmm_text
21389 );
21390 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21391 break;
21392 case 'cancelled':
21393 $this->_admin_notices->add(
21394 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
21395 sprintf(
21396 '<a href="%s">%s</a>',
21397 $this->contact_url( 'bug' ),
21398 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21399 ),
21400 $hmm_text,
21401 'error'
21402 );
21403 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21404 break;
21405 case 'expired':
21406 $this->_admin_notices->add_sticky(
21407 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 ),
21408 'license_expired',
21409 $hmm_text
21410 );
21411
21412 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
21413
21414 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21415 break;
21416 case 'trial_started':
21417 $this->add_complete_upgrade_instructions_notice(
21418 sprintf(
21419 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
21420 '<i>' . $this->get_plugin_name() . '</i>'
21421 ),
21422 'trial_started',
21423 $this->get_trial_plan()->title
21424 );
21425
21426 $this->_admin_notices->remove_sticky( array(
21427 'trial_promotion',
21428 ) );
21429 break;
21430 case 'trial_expired':
21431 $this->_admin_notices->add_sticky(
21432 ($this->has_free_plan() ?
21433 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
21434 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21435 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))),
21436 'trial_expired',
21437 $hmm_text
21438 );
21439 $this->_admin_notices->remove_sticky( array(
21440 'trial_started',
21441 'trial_promotion',
21442 'plan_upgraded',
21443 ) );
21444 break;
21445 }
21446 }
21447
21448 if ( 'none' !== $plan_change ) {
21449 if (
21450 ! is_object( $this->_license ) ||
21451 ! $this->_license->is_whitelabeled
21452 ) {
21453 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
21454 }
21455
21456 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
21457 }
21458 }
21459
21460 /**
21461 * @author Leo Fajardo (@leorw)
21462 * @since 2.5.4
21463 *
21464 * @param mixed $result
21465 *
21466 * @return string
21467 */
21468 private function generate_api_blocked_notice_message_from_result( $result ) {
21469 $api_domains = $this->apply_filters( 'api_domains', array(
21470 'api.freemius.com',
21471 'wp.freemius.com',
21472 ) );
21473
21474 $api_domains_list_items = '';
21475
21476 foreach( $api_domains as $api_domain ) {
21477 $api_domains_list_items .= "<li>{$api_domain}</li>";
21478 }
21479
21480 $error_message = sprintf(
21481 $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' ),
21482 $this->get_plugin_name(),
21483 "<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>"
21484 );
21485
21486 $error_message =
21487 "<div>{$error_message}</div>" .
21488 '<div class="fs-api-request-error-details" style="display: none">' .
21489 '<strong>' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . '</strong><br>' .
21490 $result->error->message .
21491 '</div>';
21492
21493 return $error_message;
21494 }
21495
21496 /**
21497 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
21498 *
21499 * @author Vova Feldman (@svovaf)
21500 * @since 2.0.0
21501 */
21502 public function _open_license_activation_dialog_box() {
21503 $vars = array( 'license_id' => $this->_site->license_id );
21504 fs_require_once_template( 'js/open-license-activation.php', $vars );
21505 }
21506
21507 /**
21508 * @author Vova Feldman (@svovaf)
21509 * @since 1.0.5
21510 *
21511 * @param bool $background
21512 * @param FS_Plugin_License|null $premium_license
21513 */
21514 protected function _activate_license( $background = false, $premium_license = null ) {
21515 $this->_logger->entrance();
21516
21517 if ( is_null( $premium_license ) ) {
21518 $license_id = fs_request_get( 'license_id' );
21519
21520 if ( is_object( $this->_site ) &&
21521 FS_Plugin_License::is_valid_id( $license_id ) &&
21522 $license_id == $this->_site->license_id
21523 ) {
21524 // License is already activated.
21525 return;
21526 }
21527
21528 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
21529 $this->_get_license_by_id( $license_id ) :
21530 $this->_get_available_premium_license();
21531 }
21532
21533 if ( ! is_object( $premium_license ) ) {
21534 return;
21535 }
21536
21537 if ( ! is_object( $this->_site ) ) {
21538 // Not yet opted-in.
21539 $user = $this->get_current_or_network_user();
21540 if ( ! is_object( $user ) ) {
21541 $user = self::_get_user_by_id( $premium_license->user_id );
21542 }
21543
21544 if ( is_object( $user ) ) {
21545 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
21546 } else {
21547 $this->opt_in(
21548 false,
21549 false,
21550 false,
21551 $premium_license->secret_key
21552 );
21553
21554 return;
21555 }
21556 }
21557
21558
21559 /**
21560 * If the premium license is already associated with the install, just
21561 * update the license reference (activation is not required).
21562 *
21563 * @since 1.1.9
21564 */
21565 if ( $premium_license->id == $this->_site->license_id ) {
21566 // License is already activated.
21567 $this->_update_site_license( $premium_license );
21568 $this->_store_account();
21569
21570 return;
21571 }
21572
21573 if ( $this->_site->user_id != $premium_license->user_id ) {
21574 $api_request_params = array( 'license_key' => $premium_license->secret_key );
21575 } else {
21576 $api_request_params = array();
21577 }
21578
21579 $api = $this->get_api_site_scope();
21580 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
21581
21582 if ( ! $this->is_api_result_entity( $license ) ) {
21583 if ( ! $background ) {
21584 $this->_admin_notices->add( sprintf(
21585 '%s %s',
21586 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
21587 ( is_object( $license ) && isset( $license->error ) ?
21588 $license->error->message :
21589 sprintf( '%s<br><code>%s</code>',
21590 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
21591 var_export( $license, true )
21592 )
21593 )
21594 ),
21595 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
21596 'error'
21597 );
21598 }
21599
21600 return;
21601 }
21602
21603 $premium_license = new FS_Plugin_License( $license );
21604
21605 // Updated site plan.
21606 $site = $this->get_api_site_scope()->get( '/', true );
21607 if ( $this->is_api_result_entity( $site ) ) {
21608 $this->_site = new FS_Site( $site );
21609 }
21610 $this->_update_site_license( $premium_license );
21611
21612 $this->_store_account();
21613
21614 if ( $this->is_addon() || $this->has_addons() ) {
21615 /**
21616 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21617 * an updated valid user licenses collection will be fetched from the server which is used to also
21618 * update the account add-ons (add-ons the user has licenses for).
21619 *
21620 * @author Leo Fajardo (@leorw)
21621 * @since 2.2.4
21622 */
21623 $this->purge_valid_user_licenses_cache();
21624 }
21625
21626 if ( ! $background ) {
21627 $this->add_complete_upgrade_instructions_notice(
21628 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ),
21629 'license_activated'
21630 );
21631 }
21632
21633 $this->_admin_notices->remove_sticky( array(
21634 'trial_promotion',
21635 'license_expired',
21636 ) );
21637 }
21638
21639 /**
21640 * @author Vova Feldman (@svovaf)
21641 * @since 1.0.5
21642 *
21643 * @param bool $show_notice
21644 */
21645 protected function _deactivate_license( $show_notice = true ) {
21646 $this->_logger->entrance();
21647
21648 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21649
21650 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
21651 $this->_admin_notices->add(
21652 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() ),
21653 $hmm_text
21654 );
21655
21656 return;
21657 }
21658
21659 $api = $this->get_api_site_scope();
21660 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
21661
21662 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
21663 }
21664
21665 /**
21666 * @author Leo Fajardo (@leorw)
21667 * @since 2.2.1
21668 *
21669 * @param FS_Plugin_License $license
21670 * @param bool|string $hmm_text
21671 * @param bool $show_notice
21672 */
21673 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
21674 if ( isset( $license->error ) ) {
21675 $this->_admin_notices->add(
21676 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
21677 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
21678 $hmm_text,
21679 'error'
21680 );
21681
21682 return;
21683 }
21684
21685 // Update license cache.
21686 if ( is_array( $this->_licenses ) ) {
21687 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
21688 if ( $license->id == $this->_licenses[ $i ]->id ) {
21689 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
21690 }
21691 }
21692 }
21693
21694 // Update site plan to default.
21695 $this->_sync_plans();
21696 $this->_site->plan_id = $this->_plans[0]->id;
21697 // Unlink license from site.
21698 $this->_update_site_license( null );
21699
21700 $this->_store_account();
21701
21702 if ( $show_notice ) {
21703 $this->_admin_notices->add(
21704 sprintf( $this->is_only_premium() ?
21705 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
21706 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
21707 $this->get_plan_title()
21708 ),
21709 $this->get_text_inline( 'O.K', 'ok' )
21710 );
21711 }
21712
21713 $this->_admin_notices->remove_sticky( array(
21714 'plan_upgraded',
21715 'license_activated',
21716 ) );
21717 }
21718
21719 /**
21720 * Site plan downgrade.
21721 *
21722 * @author Vova Feldman (@svovaf)
21723 * @since 1.0.4
21724 *
21725 * @return object
21726 *
21727 * @uses FS_Api
21728 */
21729 private function _downgrade_site() {
21730 $this->_logger->entrance();
21731
21732 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
21733
21734 $api = $this->get_api_site_scope();
21735 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
21736
21737 $plan_downgraded = false;
21738 $plan = false;
21739 if ( $this->is_api_result_entity( $site ) ) {
21740 $prev_plan_id = $this->_site->plan_id;
21741
21742 // Update new site plan id.
21743 $this->_site->plan_id = $site->plan_id;
21744
21745 $plan = $this->get_plan();
21746 $subscription = $this->_sync_site_subscription( $this->_license );
21747
21748 // Plan downgraded if plan was changed or subscription was cancelled.
21749 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
21750 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
21751 } else {
21752 // handle different error cases.
21753 $this->handle_license_deactivation_result(
21754 $site,
21755 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
21756 );
21757 }
21758
21759 if ( ! $plan_downgraded ) {
21760 return (object) array(
21761 'error' => (object) array(
21762 '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' )
21763 )
21764 );
21765 }
21766
21767 // Remove previous sticky message about upgrade (if exist).
21768 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21769
21770 $this->_admin_notices->add(
21771 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
21772 $plan->title,
21773 human_time_diff( time(), strtotime( $this->_license->expiration ) )
21774 )
21775 );
21776
21777 // Store site updates.
21778 $this->_store_site();
21779
21780 if ( $deactivate_license &&
21781 ! FS_Plugin_License::is_valid_id( $site->license_id )
21782 ) {
21783 if ( $this->_site->is_localhost() ) {
21784 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
21785 } else {
21786 $this->_license->activated = max( 0, $this->_license->activated - 1 );
21787 }
21788
21789 // Handle successful license deactivation result.
21790 $this->handle_license_deactivation_result( $this->_license );
21791 }
21792
21793 return $site;
21794 }
21795
21796 /**
21797 * @author Vova Feldman (@svovaf)
21798 * @since 1.1.8.1
21799 *
21800 * @param bool|string $plan_name
21801 * @param bool $add_sticky_notice
21802 *
21803 * @return bool If trial was successfully started.
21804 */
21805 function start_trial( $plan_name = false, $add_sticky_notice = false ) {
21806 $this->_logger->entrance();
21807
21808 // Alias.
21809 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
21810
21811 if ( $this->is_trial() ) {
21812 // Already in trial mode.
21813 $this->_admin_notices->add(
21814 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
21815 $oops_text,
21816 'error',
21817 $add_sticky_notice
21818 );
21819
21820 return false;
21821 }
21822
21823 if ( $this->_site->is_trial_utilized() && ! $this->is_payments_sandbox() ) {
21824 // Trial was already utilized.
21825 $this->_admin_notices->add(
21826 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
21827 $oops_text,
21828 'error',
21829 $add_sticky_notice
21830 );
21831
21832 return false;
21833 }
21834
21835 if ( false !== $plan_name ) {
21836 $plan = $this->get_plan_by_name( $plan_name );
21837
21838 if ( false === $plan ) {
21839 // Plan doesn't exist.
21840 $this->_admin_notices->add(
21841 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
21842 $oops_text,
21843 'error',
21844 $add_sticky_notice
21845 );
21846
21847 return false;
21848 }
21849
21850 if ( ! $plan->has_trial() ) {
21851 // Plan doesn't exist.
21852 $this->_admin_notices->add(
21853 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
21854 $oops_text,
21855 'error',
21856 $add_sticky_notice
21857 );
21858
21859 return false;
21860 }
21861 } else {
21862 if ( ! $this->has_trial_plan() ) {
21863 // None of the plans have a trial.
21864 $this->_admin_notices->add(
21865 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
21866 $oops_text,
21867 'error',
21868 $add_sticky_notice
21869 );
21870
21871 return false;
21872 }
21873
21874 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
21875
21876 $plan = $plans_with_trial[0];
21877 }
21878
21879 $trial_params = array();
21880
21881 if ( $this->is_payments_sandbox() ) {
21882 $trial_params['trial_timestamp'] = time();
21883 $trial_params['trial_token'] = FS_Security::instance()->get_trial_token(
21884 $this->get_plugin(),
21885 $plan,
21886 $trial_params['trial_timestamp']
21887 );
21888 }
21889
21890 $api = $this->get_api_site_scope();
21891 $trial = $api->call( "plans/{$plan->id}/trials.json", 'post', $trial_params );
21892
21893 if ( ! $this->is_api_result_entity( $trial ) ) {
21894 // Some API error while trying to start the trial.
21895 $this->_admin_notices->add(
21896 $this->get_api_error_message( $trial ),
21897 $oops_text,
21898 'error',
21899 $add_sticky_notice
21900 );
21901
21902 return false;
21903 }
21904
21905 // Sync license.
21906 $this->_sync_license();
21907
21908 return $this->is_trial();
21909 }
21910
21911 /**
21912 * Cancel site trial.
21913 *
21914 * @author Vova Feldman (@svovaf)
21915 * @since 1.0.9
21916 *
21917 * @return object
21918 *
21919 * @uses FS_Api
21920 */
21921 private function _cancel_trial() {
21922 $this->_logger->entrance();
21923
21924 if ( ! $this->is_trial() ) {
21925 return (object) array(
21926 'error' => (object) array(
21927 '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' )
21928 )
21929 );
21930 }
21931
21932 $trial_plan = $this->get_trial_plan();
21933
21934 $api = $this->get_api_site_scope();
21935 $site = $api->call( 'trials.json', 'delete' );
21936
21937 $trial_cancelled = false;
21938
21939 if ( $this->is_api_result_entity( $site ) ) {
21940 $prev_trial_ends = $this->_site->trial_ends;
21941
21942 if ( $this->is_paid_trial() ) {
21943 $this->_license->expiration = $site->trial_ends;
21944 $this->_license->is_cancelled = true;
21945 $this->_update_site_license( $this->_license );
21946 $this->_store_licenses();
21947
21948 // Clear subscription reference.
21949 $this->_sync_site_subscription( null );
21950 }
21951
21952 // Update site info.
21953 $this->_site = new FS_Site( $site );
21954
21955 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
21956 } else {
21957 // @todo handle different error cases.
21958 }
21959
21960 if ( ! $trial_cancelled ) {
21961 return (object) array(
21962 'error' => (object) array(
21963 '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' )
21964 )
21965 );
21966 }
21967
21968 // Remove previous sticky messages about upgrade or trial (if exist).
21969 $this->_admin_notices->remove_sticky( array(
21970 'trial_started',
21971 'trial_promotion',
21972 'plan_upgraded',
21973 ) );
21974
21975 // Store site updates.
21976 $this->_store_site();
21977
21978 if ( ! $this->is_addon() ||
21979 ! $this->deactivate_premium_only_addon_without_license( true )
21980 ) {
21981 $this->_admin_notices->add(
21982 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
21983 );
21984 }
21985
21986 return $site;
21987 }
21988
21989 /**
21990 * @author Vova Feldman (@svovaf)
21991 * @since 1.0.6
21992 *
21993 * @param bool|number $plugin_id
21994 *
21995 * @return bool
21996 */
21997 private function _is_addon_id( $plugin_id ) {
21998 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
21999 }
22000
22001 /**
22002 * Check if user eligible to download premium version updates.
22003 *
22004 * @author Vova Feldman (@svovaf)
22005 * @since 1.0.6
22006 *
22007 * @return bool
22008 */
22009 private function _can_download_premium() {
22010 return $this->has_any_active_valid_license() ||
22011 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
22012 }
22013
22014 /**
22015 *
22016 * @author Vova Feldman (@svovaf)
22017 * @since 1.0.6
22018 *
22019 * @param bool|number $addon_id
22020 * @param string $type "json" or "zip"
22021 *
22022 * @return string
22023 */
22024 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
22025
22026 $is_addon = $this->_is_addon_id( $addon_id );
22027
22028 $is_premium = null;
22029 if ( ! $is_addon ) {
22030 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
22031 } else if ( $this->is_addon_activated( $addon_id ) ) {
22032 $fs_addon = self::get_instance_by_id( $addon_id );
22033 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
22034 }
22035
22036 // If add-on, then append add-on ID.
22037 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
22038 '/updates/latest.' . $type;
22039
22040 // If add-on and not yet activated, try to fetch based on server licensing.
22041 if ( is_bool( $is_premium ) ) {
22042 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
22043 }
22044
22045 if ( $this->has_secret_key() ) {
22046 $endpoint = add_query_arg( 'type', 'all', $endpoint );
22047 } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) {
22048 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
22049 }
22050
22051 return $endpoint;
22052 }
22053
22054 /**
22055 * @author Vova Feldman (@svovaf)
22056 * @since 1.0.4
22057 *
22058 * @param bool|number $addon_id
22059 * @param bool $flush Since 1.1.7.3
22060 * @param int $expiration Since 1.2.2.7
22061 * @param bool|string $newer_than Since 2.2.1
22062 * @param bool|string $fetch_readme Since 2.2.1
22063 *
22064 * @return object|false Plugin latest tag info.
22065 */
22066 function _fetch_latest_version(
22067 $addon_id = false,
22068 $flush = true,
22069 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22070 $newer_than = false,
22071 $fetch_readme = true
22072 ) {
22073 $this->_logger->entrance();
22074
22075 if ( $this->is_unresolved_clone( true ) ) {
22076 return false;
22077 }
22078
22079 $switch_to_blog_id = null;
22080
22081 /**
22082 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
22083 * @since 1.1.7.4 Also check updates for add-ons.
22084 */
22085 if (
22086 ( ! $this->is_registered() || ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed() ) &&
22087 ! $this->_is_addon_id( $addon_id )
22088 ) {
22089 if ( ! is_multisite() ) {
22090 return false;
22091 }
22092
22093 $installs_map = $this->get_blog_install_map();
22094
22095 foreach ( $installs_map as $blog_id => $install ) {
22096 if ( ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed( $blog_id ) ) {
22097 continue;
22098 }
22099
22100 /**
22101 * @var FS_Site $install
22102 */
22103 if ( $install->is_trial() ) {
22104 $switch_to_blog_id = $blog_id;
22105 break;
22106 }
22107
22108 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
22109 $license = $this->get_license_by_id( $install->license_id );
22110
22111 if ( is_object( $license ) && $license->is_features_enabled() ) {
22112 $switch_to_blog_id = $blog_id;
22113 break;
22114 }
22115 }
22116 }
22117
22118 if ( is_null( $switch_to_blog_id ) ) {
22119 return false;
22120 }
22121 }
22122
22123 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
22124 get_current_blog_id() :
22125 0;
22126
22127 if ( is_numeric( $switch_to_blog_id ) ) {
22128 $this->switch_to_blog( $switch_to_blog_id );
22129 }
22130
22131 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
22132
22133 if ( ! empty( $newer_than ) ) {
22134 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
22135 }
22136
22137 if ( true === $fetch_readme ) {
22138 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
22139
22140 // Don't cache the API response when fetching readme information.
22141 $expiration = null;
22142 }
22143
22144 $tag = $this->get_api_site_or_plugin_scope()->get(
22145 $latest_version_endpoint,
22146 $flush,
22147 $expiration
22148 );
22149
22150 if ( is_numeric( $switch_to_blog_id ) ) {
22151 $this->switch_to_blog( $current_blog_id );
22152 }
22153
22154 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
22155
22156 $this->_logger->departure( 'Latest version ' . $latest_version );
22157
22158 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
22159 }
22160
22161 #----------------------------------------------------------------------------------
22162 #region Download Plugin
22163 #----------------------------------------------------------------------------------
22164
22165 /**
22166 * Download latest plugin version, based on plan.
22167 *
22168 * Not like _download_latest(), this will redirect the page
22169 * to secure download url to prevent dual download (from FS to WP server,
22170 * and then from WP server to the client / browser).
22171 *
22172 * @author Vova Feldman (@svovaf)
22173 * @since 1.0.9
22174 *
22175 * @param bool|number $plugin_id
22176 *
22177 * @uses FS_Api
22178 * @uses wp_redirect()
22179 */
22180 private function download_latest_directly( $plugin_id = false ) {
22181 $this->_logger->entrance();
22182
22183 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
22184 }
22185
22186 /**
22187 * Get latest plugin FS API download URL.
22188 *
22189 * @author Vova Feldman (@svovaf)
22190 * @since 1.0.9
22191 *
22192 * @param bool|number $plugin_id
22193 *
22194 * @return string
22195 */
22196 private function get_latest_download_api_url( $plugin_id = false ) {
22197 $this->_logger->entrance();
22198
22199 $download_api_url = $this->get_api_site_scope()->get_signed_url(
22200 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
22201 );
22202
22203 return str_replace( 'http:', 'https:', $download_api_url );
22204 }
22205
22206 /**
22207 * Get payment invoice URL.
22208 *
22209 * @author Vova Feldman (@svovaf)
22210 * @since 1.2.0
22211 *
22212 * @param bool|number $payment_id
22213 *
22214 * @return string
22215 */
22216 function _get_invoice_api_url( $payment_id = false ) {
22217 $this->_logger->entrance();
22218
22219 $url = $this->get_api_user_scope()->get_signed_url(
22220 "/payments/{$payment_id}/invoice.pdf"
22221 );
22222
22223 if ( ! fs_starts_with( $url, 'https://' ) ) {
22224 // Always use HTTPS for invoices.
22225 $url = 'https' . substr( $url, 4 );
22226 }
22227
22228 return $url;
22229 }
22230
22231 /**
22232 * Get latest plugin download link.
22233 *
22234 * @author Vova Feldman (@svovaf)
22235 * @since 1.0.9
22236 *
22237 * @param string $label
22238 * @param bool|number $plugin_id
22239 *
22240 * @return string
22241 */
22242 private function get_latest_download_link( $label, $plugin_id = false ) {
22243 return sprintf(
22244 '<a target="_blank" rel="noopener" href="%s">%s</a>',
22245 $this->_get_latest_download_local_url( $plugin_id ),
22246 $label
22247 );
22248 }
22249
22250 /**
22251 * Get latest plugin download local URL.
22252 *
22253 * @author Vova Feldman (@svovaf)
22254 * @since 1.0.9
22255 *
22256 * @param bool|number $plugin_id
22257 *
22258 * @return string
22259 */
22260 function _get_latest_download_local_url( $plugin_id = false ) {
22261 // Add timestamp to protect from caching.
22262 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
22263
22264 if ( ! empty( $plugin_id ) ) {
22265 $params['plugin_id'] = $plugin_id;
22266 } else if ( $this->is_addon() ) {
22267 $params['plugin_id'] = $this->get_id();
22268 }
22269
22270 $fs = $this->is_addon() ?
22271 $this->get_parent_instance() :
22272 $this;
22273
22274 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
22275 }
22276
22277 #endregion Download Plugin ------------------------------------------------------------------
22278
22279 /**
22280 * @author Vova Feldman (@svovaf)
22281 * @since 1.0.4
22282 *
22283 * @uses FS_Api
22284 *
22285 * @param bool $background Hints the method if it's a background updates check. If false, it means that
22286 * was initiated by the admin.
22287 * @param bool|number $plugin_id
22288 * @param bool $flush Since 1.1.7.3
22289 * @param int $expiration Since 1.2.2.7
22290 * @param bool|string $newer_than Since 2.2.1
22291 */
22292 private function check_updates(
22293 $background = false,
22294 $plugin_id = false,
22295 $flush = true,
22296 $expiration = FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION,
22297 $newer_than = false
22298 ) {
22299 $this->_logger->entrance();
22300
22301 // Check if there's a newer version for download.
22302 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than );
22303
22304 $update = null;
22305 if ( is_object( $new_version ) ) {
22306 $update = new FS_Plugin_Tag( $new_version );
22307
22308 if ( ! $background ) {
22309 $this->_admin_notices->add(
22310 sprintf(
22311 /* translators: %s: Numeric version number (e.g. '2.1.9' */
22312 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
22313 $update->version,
22314 sprintf(
22315 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22316 $this->get_account_url( 'download_latest' ),
22317 sprintf(
22318 /* translators: %s: plan name (e.g. latest "Professional" version) */
22319 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
22320 $this->get_plan_title()
22321 )
22322 )
22323 ),
22324 $this->get_text_inline( 'New', 'new' ) . '!'
22325 );
22326 }
22327 } else if ( false === $new_version && ! $background ) {
22328 $this->_admin_notices->add(
22329 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
22330 $this->get_text_inline( 'You are all good!', 'you-are-good' )
22331 );
22332 }
22333
22334 $this->_store_update( $update, true, $plugin_id );
22335 }
22336
22337 /**
22338 * @author Vova Feldman (@svovaf)
22339 * @since 1.0.4
22340 *
22341 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
22342 *
22343 * @return FS_Plugin[]
22344 *
22345 * @uses FS_Api
22346 */
22347 private function sync_addons( $flush = false ) {
22348 $this->_logger->entrance();
22349
22350 $api = $this->get_api_site_or_plugin_scope();
22351
22352 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
22353
22354 /**
22355 * @since 1.2.1
22356 *
22357 * If there's a cached version of the add-ons and not asking
22358 * for a flush, just use the currently stored add-ons.
22359 */
22360 if ( ! $flush && $api->is_cached( $path ) ) {
22361 $addons = self::get_all_addons();
22362
22363 return isset( $addons[ $this->_plugin->id ] ) ?
22364 $addons[ $this->_plugin->id ] :
22365 array();
22366 }
22367
22368 $result = $api->get( $path, $flush );
22369
22370 $addons = array();
22371 if ( $this->is_api_result_object( $result, 'plugins' ) &&
22372 is_array( $result->plugins )
22373 ) {
22374 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
22375 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
22376 }
22377
22378 $this->_store_addons( $addons, true );
22379 }
22380
22381 return $addons;
22382 }
22383
22384 /**
22385 * Handle user email update.
22386 *
22387 * @author Vova Feldman (@svovaf)
22388 * @since 1.0.3
22389 * @uses FS_Api
22390 *
22391 * @param string $new_email
22392 *
22393 * @return object
22394 */
22395 private function update_email( $new_email ) {
22396 $this->_logger->entrance();
22397
22398 $api = $this->get_api_user_scope();
22399 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
22400 'email' => $new_email,
22401 'after_email_confirm_url' => $this->_get_admin_page_url(
22402 'account',
22403 array( 'fs_action' => 'sync_user' )
22404 ),
22405 ) );
22406
22407 if ( ! isset( $user->error ) ) {
22408 $this->_user->email = $user->email;
22409 $this->_user->is_verified = $user->is_verified;
22410 $this->_store_user();
22411 } else {
22412 // handle different error cases.
22413 }
22414
22415 return $user;
22416 }
22417
22418 #----------------------------------------------------------------------------------
22419 #region API Error Handling
22420 #----------------------------------------------------------------------------------
22421
22422 /**
22423 * @author Vova Feldman (@svovaf)
22424 * @since 1.1.1
22425 *
22426 * @param mixed $result
22427 *
22428 * @return bool Is API result contains an error.
22429 */
22430 private function is_api_error( $result ) {
22431 return FS_Api::is_api_error( $result );
22432 }
22433
22434 /**
22435 * Checks if given API result is a non-empty and not an error object.
22436 *
22437 * @author Vova Feldman (@svovaf)
22438 * @since 1.2.1.5
22439 *
22440 * @param mixed $result
22441 * @param string|null $required_property Optional property we want to verify that is set.
22442 *
22443 * @return bool
22444 */
22445 function is_api_result_object( $result, $required_property = null ) {
22446 return FS_Api::is_api_result_object( $result, $required_property );
22447 }
22448
22449 /**
22450 * Checks if given API result is a non-empty entity object with non-empty ID.
22451 *
22452 * @author Vova Feldman (@svovaf)
22453 * @since 1.2.1.5
22454 *
22455 * @param mixed $result
22456 *
22457 * @return bool
22458 */
22459 private function is_api_result_entity( $result ) {
22460 return FS_Api::is_api_result_entity( $result );
22461 }
22462
22463 #endregion
22464
22465 /**
22466 * Make sure a given argument is an array of a specific type.
22467 *
22468 * @author Vova Feldman (@svovaf)
22469 * @since 1.2.1.5
22470 *
22471 * @param mixed $array
22472 * @param string $class
22473 *
22474 * @return bool
22475 */
22476 private function is_array_instanceof( $array, $class ) {
22477 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
22478 }
22479
22480 /**
22481 * Start install ownership change.
22482 *
22483 * @author Vova Feldman (@svovaf)
22484 * @since 1.1.1
22485 * @uses FS_Api
22486 *
22487 * @param string $candidate_email
22488 * @param string $transfer_type
22489 *
22490 * @return bool Is ownership change successfully initiated.
22491 */
22492 private function init_change_owner( $candidate_email, $transfer_type ) {
22493 $this->_logger->entrance();
22494
22495 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
22496 $install_ids = array();
22497
22498 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
22499 $install = $install_info['install'];
22500
22501 if ( $this->_user->id != $install->user_id ) {
22502 // Skip add-on installs that are not owned by the parent product's install's owner.
22503 continue;
22504 }
22505
22506 $install_ids[ $slug ] = $install->id;
22507 }
22508
22509 $api = $this->get_api_site_scope();
22510 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
22511 'email' => $candidate_email,
22512 'transfer_type' => $transfer_type,
22513 'install_ids' => implode( ',', array_values( $install_ids ) ),
22514 'after_confirm_url' => $this->_get_admin_page_url(
22515 'account',
22516 array( 'fs_action' => 'change_owner' )
22517 ),
22518 ) );
22519
22520 return ! $this->is_api_error( $result );
22521 }
22522
22523 /**
22524 * Handle install ownership change.
22525 *
22526 * @author Vova Feldman (@svovaf)
22527 * @since 1.1.1
22528 * @uses FS_Api
22529 *
22530 * @return bool Was ownership change successfully complete.
22531 */
22532 private function complete_change_owner() {
22533 $this->_logger->entrance();
22534
22535 $install_ids = fs_request_get( 'install_ids' );
22536
22537 if ( ! empty( $install_ids ) ) {
22538 $install_ids = explode( ',', $install_ids );
22539
22540 foreach ( $install_ids as $key => $install_id ) {
22541 if ( ! FS_Site::is_valid_id( $install_id ) ) {
22542 unset( $install_ids[ $key ] );
22543 }
22544 }
22545 }
22546
22547 if ( ! is_array( $install_ids ) ) {
22548 $install_ids = array();
22549 }
22550
22551 $user = new FS_User();
22552 $user->id = fs_request_get( 'user_id' );
22553 $user->public_key = fs_request_get_raw( 'user_public_key' );
22554 $user->secret_key = fs_request_get_raw( 'user_secret_key' );
22555
22556 $prev_user = $this->_user;
22557 $this->_user = $user;
22558
22559 $result = $this->get_api_user_scope( true )->get(
22560 "/installs.json?install_ids=" . implode( ',', $install_ids )
22561 );
22562
22563 $current_blog_sites = self::get_all_sites( $this->get_module_type() );
22564
22565 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22566 $site_id_slug_map = array();
22567
22568 foreach ( $current_blog_sites as $slug => $site ) {
22569 $site_id_slug_map[ $site->id ] = $slug;
22570 }
22571
22572 foreach ( $result->installs as $install ) {
22573 $site = new FS_Site( $install );
22574
22575 if ( ! isset( $site_id_slug_map[ $install->id ] ) ) {
22576 continue;
22577 }
22578
22579 $current_blog_sites[ $site_id_slug_map[ $install->id ] ] = clone $site;
22580
22581 if ( $this->_site->id == $site->id ) {
22582 $this->_site = $site;
22583 }
22584 }
22585 }
22586
22587 // Validate install's user and given user.
22588 if ( $user->id != $this->_site->user_id ) {
22589 $this->_user = $prev_user;
22590
22591 return false;
22592 }
22593
22594 $this->set_account_option( 'sites', $current_blog_sites, true );
22595
22596 // Fetch new user information.
22597 $user_result = $this->get_api_user_scope( true )->get();
22598 $user = new FS_User( $user_result );
22599 $this->_user = $user;
22600
22601 $this->_set_account( $user, $this->_site );
22602
22603 $remove_user = true;
22604 $all_modules_sites = FS_DebugManager::get_all_modules_sites();
22605
22606 foreach ( $all_modules_sites as $sites_by_module_type ) {
22607 foreach ( $sites_by_module_type as $sites_by_slug ) {
22608 foreach ( $sites_by_slug as $site ) {
22609 if ( $prev_user->id == $site->user_id ) {
22610 $remove_user = false;
22611 break;
22612 }
22613 }
22614
22615 if ( ! $remove_user ) {
22616 break;
22617 }
22618 }
22619
22620 if ( ! $remove_user ) {
22621 break;
22622 }
22623 }
22624
22625 if ( $remove_user ) {
22626 $users = self::get_all_users();
22627
22628 if ( isset( $users[ $prev_user->id ] ) ) {
22629 unset( $users[ $prev_user->id ] );
22630 } else {
22631 // If the prev user wasn't found by the key, iterate over the users collection.
22632 foreach ( $users as $key => $user ) {
22633 if ( $user->id == $prev_user->id ) {
22634 unset( $users[ $key ] );
22635 break;
22636 }
22637 }
22638 }
22639
22640 $this->set_account_option( 'users', $users, true );
22641 }
22642
22643 return true;
22644 }
22645
22646 /**
22647 * Completes ownership change by license.
22648 *
22649 * @author Leo Fajardo (@leorw)
22650 * @since 2.3.2
22651 *
22652 * @param number $user_id
22653 * @param array[string]number $install_ids_by_slug_map
22654 *
22655 */
22656 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
22657 $this->_logger->entrance();
22658
22659 $this->sync_user_by_current_install( $user_id );
22660
22661 $result = $this->get_api_user_scope( true )->get(
22662 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
22663 );
22664
22665 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22666 $sites = self::get_all_sites( $this->get_module_type() );
22667 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
22668
22669 foreach ( $result->installs as $install ) {
22670 $site = new FS_Site( $install );
22671
22672 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
22673 }
22674
22675 $this->set_account_option( 'sites', $sites, true );
22676 }
22677 }
22678
22679 /**
22680 * Handle user name update.
22681 *
22682 * @author Vova Feldman (@svovaf)
22683 * @since 1.0.9
22684 * @uses FS_Api
22685 *
22686 * @return object
22687 */
22688 private function update_user_name() {
22689 $this->_logger->entrance();
22690 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
22691
22692 $api = $this->get_api_user_scope();
22693 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
22694 'name' => $name,
22695 ) );
22696
22697 if ( ! isset( $user->error ) ) {
22698 $this->_user->first = $user->first;
22699 $this->_user->last = $user->last;
22700 $this->_store_user();
22701 } else {
22702 // handle different error cases.
22703
22704 }
22705
22706 return $user;
22707 }
22708
22709 /**
22710 * Verify user email.
22711 *
22712 * @author Vova Feldman (@svovaf)
22713 * @since 1.0.3
22714 * @uses FS_Api
22715 */
22716 private function verify_email() {
22717 $this->_handle_account_user_sync();
22718
22719 if ( $this->_user->is_verified() ) {
22720 return;
22721 }
22722
22723 $api = $this->get_api_site_scope();
22724 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
22725 'after_email_confirm_url' => $this->_get_admin_page_url(
22726 'account',
22727 array( 'fs_action' => 'sync_user' )
22728 )
22729 ) );
22730
22731 if ( ! isset( $result->error ) ) {
22732 $this->_admin_notices->add( sprintf(
22733 $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' ),
22734 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
22735 ) );
22736 } else {
22737 // handle different error cases.
22738
22739 }
22740 }
22741
22742 /**
22743 * @author Vova Feldman (@svovaf)
22744 * @since 1.1.2
22745 *
22746 * @param array $params
22747 * @param bool|null $network
22748 *
22749 * @return string
22750 */
22751 function get_activation_url( $params = array(), $network = null ) {
22752 if ( $this->is_addon() && $this->has_free_plan() ) {
22753 /**
22754 * @author Vova Feldman (@svovaf)
22755 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
22756 */
22757 return $this->get_parent_instance()->get_activation_url( $params );
22758 }
22759
22760 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
22761 }
22762
22763 /**
22764 * @author Vova Feldman (@svovaf)
22765 * @since 1.2.1.5
22766 *
22767 * @param array $params
22768 *
22769 * @return string
22770 */
22771 function get_reconnect_url( $params = array() ) {
22772 $params['fs_action'] = 'reset_anonymous_mode';
22773 $params['fs_unique_affix'] = $this->get_unique_affix();
22774
22775 return $this->get_activation_url( $params );
22776 }
22777
22778 /**
22779 * Get the URL of the page that should be loaded after the user connect
22780 * or skip in the opt-in screen.
22781 *
22782 * @author Vova Feldman (@svovaf)
22783 * @since 1.1.3
22784 *
22785 * @param string $filter Filter name.
22786 * @param array $params Since 1.2.2.7
22787 * @param bool|null $network
22788 *
22789 * @return string
22790 */
22791 function get_after_activation_url( $filter, $params = array(), $network = null ) {
22792 if ( $this->show_opt_in_on_themes_page() &&
22793 ( fs_request_has( 'pending_activation' ) ||
22794 // For cases when the first time path is set, even though it's a WP.org theme.
22795 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
22796 ) {
22797 $first_time_path = '';
22798 } else {
22799 $first_time_path = $this->_menu->get_first_time_path(
22800 fs_is_network_admin() && $this->_is_network_active
22801 );
22802 }
22803
22804 if ( $this->_is_network_active &&
22805 fs_is_network_admin() &&
22806 ! $this->_menu->has_network_menu() &&
22807 $this->is_network_registered()
22808 ) {
22809 $target_url = $this->get_account_url();
22810 } else {
22811 // Default plugin's page.
22812 $target_url = $this->_get_admin_page_url( '', array(), $network );
22813 }
22814
22815 return add_query_arg( $params, $this->apply_filters(
22816 $filter,
22817 empty( $first_time_path ) ?
22818 $target_url :
22819 $first_time_path
22820 ) );
22821 }
22822
22823 /**
22824 * Handle account page updates / edits / actions.
22825 *
22826 * @author Vova Feldman (@svovaf)
22827 * @since 1.0.2
22828 *
22829 */
22830 private function _handle_account_edits() {
22831 if ( ! $this->is_user_admin() ) {
22832 return;
22833 }
22834
22835 $action = fs_get_action();
22836
22837 if ( empty( $action ) ) {
22838 return;
22839 }
22840
22841 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
22842 $install_id = fs_request_get( 'install_id', '' );
22843
22844 // Alias.
22845 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
22846
22847 $is_network_action = $this->is_network_level_action();
22848 $blog_id = $this->is_network_level_site_specific_action();
22849 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
22850
22851 if ( is_numeric( $blog_id ) ) {
22852 $this->switch_to_blog( $blog_id );
22853 } else {
22854 $blog_id = '';
22855 }
22856
22857 switch ( $action ) {
22858 case 'opt_in':
22859 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22860
22861 if ( $is_parent_plugin_action ) {
22862 if ( $is_network_action && ! empty( $blog_id ) ) {
22863 if ( ! $this->is_registered() ) {
22864 $this->install_with_user(
22865 $this->get_network_user(),
22866 false,
22867 false,
22868 false,
22869 false
22870 );
22871
22872 $this->_admin_notices->add(
22873 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
22874 $this->get_text_inline( 'Awesome', 'awesome' )
22875 );
22876 }
22877 }
22878 }
22879 break;
22880
22881 case 'toggle_tracking':
22882 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22883
22884 if ( $is_parent_plugin_action ) {
22885 if ( $is_network_action && ! empty( $blog_id ) ) {
22886 if ( $this->is_registered( true ) ) {
22887 if ( $this->is_tracking_prohibited( $blog_id ) ) {
22888 if ( $this->toggle_site_tracking( true, $blog_id ) ) {
22889 $this->_admin_notices->add(
22890 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>" ),
22891 $this->get_text_inline( 'Thank you!', 'thank-you' )
22892 );
22893 }
22894 } else {
22895 if ( $this->toggle_site_tracking( false, $blog_id ) ) {
22896 $install = $this->get_install_by_blog_id( $blog_id );
22897
22898 $this->_admin_notices->add(
22899 sprintf(
22900 $this->get_text_inline( 'Diagnostic data will no longer be sent from %s to %s.', 'opted-out-successfully' ),
22901 self::get_unfiltered_site_url( $blog_id, true ),
22902 "<b>{$this->get_plugin_title()}</b>"
22903 )
22904 );
22905 }
22906 }
22907 }
22908 }
22909 }
22910
22911 break;
22912
22913 case 'delete_account':
22914 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22915
22916 $is_network_deletion = $is_network_action && empty( $blog_id );
22917
22918 if ( $is_parent_plugin_action ) {
22919 // Delete add-on installs if have any.
22920 $installed_addons = $this->get_installed_addons();
22921 foreach ( $installed_addons as $fs_addon ) {
22922 if ( $is_network_deletion ) {
22923 $fs_addon->delete_network_account_event();
22924 } else {
22925 $fs_addon->delete_account_event();
22926 }
22927 }
22928
22929 if ( $is_network_deletion ) {
22930 $this->delete_network_account_event();
22931 } else {
22932 $this->delete_account_event();
22933 }
22934
22935 // Clear user and site.
22936 $this->_site = null;
22937 $this->_user = null;
22938
22939 $this->maybe_set_slug_and_network_menu_exists_flag();
22940
22941 fs_redirect( $this->get_activation_url() );
22942 } else {
22943 if ( $this->is_addon_activated( $plugin_id ) ) {
22944 $fs_addon = self::get_instance_by_id( $plugin_id );
22945
22946 if ( $is_network_deletion ) {
22947 $fs_addon->delete_network_account_event();
22948 } else {
22949 $fs_addon->delete_account_event();
22950 }
22951
22952 fs_redirect( $this->_get_admin_page_url( 'account' ) );
22953 }
22954 }
22955
22956 return;
22957
22958 case 'downgrade_account':
22959 if ( is_numeric( $blog_id ) ) {
22960 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22961 } else {
22962 check_admin_referer( $action );
22963 }
22964
22965 $switch_to_network_install_blog_after_cancellation = (
22966 is_numeric( $blog_id ) &&
22967 $plugin_id == $this->get_id() &&
22968 ! $this->is_trial()
22969 );
22970
22971 $result = $this->cancel_subscription_or_trial( $plugin_id );
22972 if ( $this->is_api_error( $result ) ) {
22973 $this->_admin_notices->add(
22974 $result->error->message,
22975 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
22976 'error'
22977 );
22978 }
22979
22980 if ( $switch_to_network_install_blog_after_cancellation ) {
22981 $this->switch_to_blog( $this->_storage->network_install_blog_id );
22982 }
22983
22984 return;
22985
22986 case 'activate_license':
22987 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22988
22989 $fs = $this;
22990 if ( $plugin_id != $this->get_id() ) {
22991 $fs = $this->is_addon_activated( $plugin_id ) ?
22992 self::get_instance_by_id( $plugin_id ) :
22993 null;
22994 }
22995
22996 if ( is_object( $fs ) ) {
22997 $fs->_activate_license();
22998
22999 /**
23000 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
23001 *
23002 * @author Leo Fajardo (@leorw)
23003 * @since 2.4.0
23004 */
23005 unset( $_REQUEST['plugin_id'] );
23006
23007 if ( $this->is_bundle_license_auto_activation_enabled() ) {
23008 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
23009 }
23010 }
23011
23012 return;
23013
23014 case 'deactivate_license':
23015 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23016
23017 if ( $plugin_id == $this->get_id() ) {
23018 $this->_deactivate_license();
23019
23020 if ( $this->is_only_premium() ) {
23021 // Clear user and site.
23022 $this->_site = null;
23023 $this->_user = null;
23024
23025 if ( ! $is_network_action ) {
23026 fs_redirect( $this->get_activation_url() );
23027 } else if ( is_numeric( $blog_id ) ) {
23028 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23029 }
23030 }
23031 } else {
23032 if ( $this->is_addon_activated( $plugin_id ) ) {
23033 $fs_addon = self::get_instance_by_id( $plugin_id );
23034 $fs_addon->_deactivate_license();
23035 }
23036 }
23037
23038 return;
23039
23040 case 'check_updates':
23041 check_admin_referer( $action );
23042 $this->check_updates();
23043
23044 return;
23045
23046 case 'change_owner':
23047 $state = fs_request_get( 'state', 'init' );
23048 switch ( $state ) {
23049 case 'init':
23050 // The nonce is injected by the error handler in `_email_address_update_ajax_handler` function.
23051 check_admin_referer( 'change_owner' );
23052
23053 $candidate_email = fs_request_get( 'candidate_email' );
23054 $transfer_type = fs_request_get( 'transfer_type' );
23055
23056 if ( $this->init_change_owner( $candidate_email, $transfer_type ) ) {
23057 if ( 'transfer' === $transfer_type ) {
23058 $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>' ) );
23059 } else {
23060 $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>' ) );
23061 }
23062 }
23063 break;
23064 case 'owner_confirmed':
23065 // We cannot (or need not to) check the nonce and referer here, because the link comes from the email sent by our API.
23066 $candidate_email = fs_request_get( 'candidate_email', '' );
23067
23068 if ( ! is_email($candidate_email ) ) {
23069 return;
23070 }
23071
23072 $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>' ) );
23073 break;
23074 case 'candidate_confirmed':
23075 // We do not need to validate the authenticity of this request here, because the `complete_change_owner` does that for us through API calls.
23076 if ( $this->complete_change_owner() ) {
23077 $this->_admin_notices->add_sticky(
23078 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
23079 'ownership_changed',
23080 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
23081 );
23082 } else {
23083 // @todo Handle failed ownership change message.
23084 }
23085 break;
23086 }
23087
23088 return;
23089
23090 case 'update_user_name':
23091 check_admin_referer( 'update_user_name' );
23092
23093 $result = $this->update_user_name();
23094
23095 if ( isset( $result->error ) ) {
23096 $this->_admin_notices->add(
23097 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
23098 $oops_text,
23099 'error'
23100 );
23101 } else {
23102 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
23103 }
23104
23105 return;
23106
23107 #region Actions that might be called from external links (e.g. email)
23108
23109 /**
23110 * !!IMPORTANT!!: We cannot check for a valid nonce in this region, because the links could be coming from emails.
23111 */
23112
23113 case 'cancel_trial':
23114 $result = $this->cancel_subscription_or_trial( $plugin_id );
23115 if ( $this->is_api_error( $result ) ) {
23116 $this->_admin_notices->add(
23117 $result->error->message,
23118 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23119 'error'
23120 );
23121 }
23122
23123 return;
23124
23125 case 'verify_email':
23126 $this->verify_email();
23127
23128 return;
23129
23130 case 'sync_user':
23131 $this->_handle_account_user_sync();
23132
23133 return;
23134
23135 case $this->get_unique_affix() . '_sync_license':
23136 $this->_sync_license();
23137
23138 return;
23139
23140 case 'download_latest':
23141 $this->download_latest_directly( $plugin_id );
23142
23143 return;
23144
23145 #endregion
23146 }
23147
23148 if ( WP_FS__IS_POST_REQUEST ) {
23149 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
23150 foreach ( $properties as $p ) {
23151 if ( 'update_' . $p === $action ) {
23152 check_admin_referer( $action );
23153
23154 $this->_logger->log( $action );
23155
23156 $site_property = substr( $p, strlen( 'site_' ) );
23157 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
23158 $this->get_site()->{$site_property} = $site_property_value;
23159
23160 // Store account after modification.
23161 $this->_store_site();
23162
23163 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
23164
23165 $this->_admin_notices->add( sprintf(
23166 /* translators: %s: User's account property (e.g. email address, name) */
23167 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
23168 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
23169 ) );
23170
23171 return;
23172 }
23173 }
23174 }
23175 }
23176
23177 /**
23178 * Adds CSS classes for the body tag in the admin.
23179 *
23180 * @param string $classes Space-separated string of class names.
23181 *
23182 * @return string $classes FS Admin body tag class names.
23183 */
23184 public function fs_addons_body_class( $classes ) {
23185 $classes .= ' plugins-php';
23186 return $classes;
23187 }
23188
23189 /**
23190 * Account page resources load.
23191 *
23192 * @author Vova Feldman (@svovaf)
23193 * @since 1.0.6
23194 */
23195 function _account_page_load() {
23196 $this->_logger->entrance();
23197
23198 $this->_logger->info( var_export( $_REQUEST, true ) );
23199
23200 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
23201
23202 if ( $this->has_addons() ) {
23203 wp_enqueue_script( 'plugin-install' );
23204 add_thickbox();
23205 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23206 }
23207
23208 if ( $this->has_paid_plan() &&
23209 ! $this->has_any_license() &&
23210 ! $this->is_sync_executed() &&
23211 $this->is_tracking_allowed()
23212 ) {
23213 /**
23214 * If no licenses found and no sync job was executed during the last 24 hours,
23215 * just execute the sync job right away (blocking execution).
23216 *
23217 * @since 1.1.7.3
23218 */
23219 $this->run_manual_sync();
23220 }
23221
23222 $this->_handle_account_edits();
23223
23224 if (
23225 is_object( $this->_license ) &&
23226 $this->_license->user_id == $this->_user->id &&
23227 ! $this->is_whitelabeled( true )
23228 ) {
23229 $this->_admin_notices->add(
23230 sprintf(
23231 $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' ),
23232 sprintf(
23233 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
23234 $this->get_text_inline( 'Click here', 'click-here' )
23235 )
23236 ),
23237 '',
23238 'success',
23239 false,
23240 'license_not_whitelabeled'
23241 );
23242 }
23243
23244 $this->do_action( 'account_page_load_before_departure' );
23245 }
23246
23247 /**
23248 * Renders the "Affiliation" page.
23249 *
23250 * @author Leo Fajardo (@leorw)
23251 * @since 1.2.3
23252 */
23253 function _affiliation_page_render() {
23254 $this->_logger->entrance();
23255
23256 $this->fetch_affiliate_and_terms();
23257
23258 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
23259
23260 $is_bundle_context = $this->has_bundle_context();
23261
23262 $plugin_title = $this->get_plugin_title();
23263
23264 if ( $is_bundle_context ) {
23265 $plugin_title = $this->plugin_affiliate_terms->plugin_title;
23266
23267 // Add the suffix "Bundle" only if the word is not present in the title itself.
23268 if ( false === mb_stripos( $plugin_title, fs_text_inline( 'Bundle', 'bundle' ) ) ) {
23269 $plugin_title = $this->apply_filters(
23270 'formatted_bundle_title',
23271 $plugin_title . ' ' . fs_text_inline( 'Bundle', 'bundle' )
23272 );
23273 }
23274 }
23275
23276 $vars = array(
23277 'id' => $this->_module_id,
23278 'plugin_title' => $plugin_title,
23279 );
23280 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
23281 }
23282
23283
23284 /**
23285 * Render account page.
23286 *
23287 * @author Vova Feldman (@svovaf)
23288 * @since 1.0.0
23289 */
23290 function _account_page_render() {
23291 $this->_logger->entrance();
23292
23293 $template = 'account.php';
23294 $vars = array( 'id' => $this->_module_id );
23295
23296 /**
23297 * Added filter to the template to allow developers wrapping the template
23298 * in custom HTML (e.g. within a wizard/tabs).
23299 *
23300 * @author Vova Feldman (@svovaf)
23301 * @since 1.2.1.6
23302 */
23303 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
23304 }
23305
23306 /**
23307 * Render account connect page.
23308 *
23309 * @author Vova Feldman (@svovaf)
23310 * @since 1.0.7
23311 */
23312 function _connect_page_render() {
23313 $this->_logger->entrance();
23314
23315 $vars = array( 'id' => $this->_module_id );
23316
23317 /**
23318 * Added filter to the template to allow developers wrapping the template
23319 * in custom HTML (e.g. within a wizard/tabs).
23320 *
23321 * @author Vova Feldman (@svovaf)
23322 * @since 1.2.1.6
23323 */
23324 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
23325 }
23326
23327 /**
23328 * Load required resources before add-ons page render.
23329 *
23330 * @author Vova Feldman (@svovaf)
23331 * @since 1.0.6
23332 */
23333 function _addons_page_load() {
23334 $this->_logger->entrance();
23335
23336 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
23337
23338 wp_enqueue_script( 'plugin-install' );
23339 add_thickbox();
23340 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23341
23342 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
23343 $this->_admin_notices->add(
23344 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>' ),
23345 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
23346 'update-nag'
23347 );
23348 }
23349 }
23350
23351 /**
23352 * Render add-ons page.
23353 *
23354 * @author Vova Feldman (@svovaf)
23355 * @since 1.0.6
23356 */
23357 function _addons_page_render() {
23358 $this->_logger->entrance();
23359
23360 $vars = array( 'id' => $this->_module_id );
23361
23362 /**
23363 * Added filter to the template to allow developers wrapping the template
23364 * in custom HTML (e.g. within a wizard/tabs).
23365 *
23366 * @author Vova Feldman (@svovaf)
23367 * @since 1.2.1.6
23368 */
23369 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
23370 }
23371
23372 /* Pricing & Upgrade
23373 ------------------------------------------------------------------------------------------------------------------*/
23374 /**
23375 * Render pricing page.
23376 *
23377 * @author Vova Feldman (@svovaf)
23378 * @since 1.0.0
23379 */
23380 function _pricing_page_render() {
23381 $this->_logger->entrance();
23382
23383 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
23384 fs_enqueue_local_style( 'fs_checkout', '/admin/checkout.css' );
23385
23386 $vars = array( 'id' => $this->_module_id );
23387
23388 if ( 'true' === fs_request_get( 'checkout', false ) ) {
23389 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
23390 } else {
23391 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
23392 }
23393 }
23394
23395 /**
23396 * @author Leo Fajardo (@leorw)
23397 * @since 2.3.1
23398 */
23399 function _add_pricing_ajax_handler() {
23400 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
23401 }
23402
23403 /**
23404 * @author Leo Fajardo (@leorw)
23405 * @since 2.3.1
23406 */
23407 function _fs_pricing_ajax_action_handler() {
23408 $this->check_ajax_referer( 'pricing_ajax_action' );
23409
23410 $result = null;
23411 $pricing_action = fs_request_get( 'pricing_action' );
23412
23413 switch ( $pricing_action ) {
23414 case 'fetch_pricing_data':
23415 $params = array(
23416 'is_enriched' => true,
23417 'trial' => fs_request_get_bool( 'trial' ),
23418 'sandbox' => fs_request_get_raw( 'sandbox' ),
23419 's_ctx_type' => fs_request_get_raw( 's_ctx_type' ),
23420 's_ctx_id' => fs_request_get_raw( 's_ctx_id' ),
23421 's_ctx_ts' => fs_request_get_raw( 's_ctx_ts' ),
23422 's_ctx_secure' => fs_request_get_raw( 's_ctx_secure' ),
23423 );
23424
23425 $bundle_id = $this->get_bundle_id();
23426 $bundle_public_key = $this->get_bundle_public_key();
23427
23428 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
23429
23430 if ( ! $has_bundle_context ) {
23431 $api = $this->get_api_plugin_scope();
23432 } else {
23433 $api = FS_Api::instance(
23434 $bundle_id,
23435 'plugin',
23436 $bundle_id,
23437 $bundle_public_key,
23438 ! $this->is_live(),
23439 false,
23440 $this->get_sdk_version()
23441 );
23442
23443 $params['plugin_id'] = $this->get_id();
23444 $params['plugin_public_key'] = $this->get_public_key();
23445 }
23446
23447 $result = $api->get( 'pricing.json?' . http_build_query( $params ) );
23448 break;
23449 case 'start_trial':
23450 $trial_plan_id = fs_request_get( 'plan_id' );
23451
23452 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
23453 $plan = $this->_get_plan_by_id( $trial_plan_id );
23454
23455 if ( ! $plan ) {
23456 $this->shoot_ajax_failure( 'Invalid plan ID.' );
23457 return;
23458 }
23459
23460 $result = $this->start_trial( $plan->name, true );
23461 } else {
23462 // @todo - This fails for sandbox trial at the moment if the trial was already utilized.
23463 $result = $this->opt_in(
23464 false,
23465 false,
23466 false,
23467 false,
23468 false,
23469 $trial_plan_id
23470 );
23471 }
23472 }
23473
23474 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
23475 $this->_logger->api_error( $result );
23476
23477 self::shoot_ajax_failure(
23478 isset( $result->error ) ?
23479 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
23480 var_export( $result, true )
23481 );
23482 }
23483
23484 $this->shoot_ajax_success( $result );
23485 }
23486
23487 #----------------------------------------------------------------------------------
23488 #region Contact Us
23489 #----------------------------------------------------------------------------------
23490
23491 /**
23492 * Render contact-us page.
23493 *
23494 * @author Vova Feldman (@svovaf)
23495 * @since 1.0.3
23496 */
23497 function _contact_page_render() {
23498 $this->_logger->entrance();
23499
23500 $vars = array( 'id' => $this->_module_id );
23501
23502 /**
23503 * Added filter to the template to allow developers wrapping the template
23504 * in custom HTML (e.g. within a wizard/tabs).
23505 *
23506 * @author Vova Feldman (@svovaf)
23507 * @since 2.1.3
23508 */
23509 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
23510 }
23511
23512 #endregion ------------------------------------------------------------------------
23513
23514 /**
23515 * Hide all admin notices to prevent distractions.
23516 *
23517 * @author Vova Feldman (@svovaf)
23518 * @since 1.0.3
23519 *
23520 * @uses remove_all_actions()
23521 */
23522 private static function _hide_admin_notices() {
23523 remove_all_actions( 'admin_notices' );
23524 remove_all_actions( 'network_admin_notices' );
23525 remove_all_actions( 'all_admin_notices' );
23526 remove_all_actions( 'user_admin_notices' );
23527 }
23528
23529 static function _clean_admin_content_section_hook() {
23530 $hide_admin_notices = true;
23531
23532 if ( fs_request_is_action( 'allow_clone_resolution_notice' ) ) {
23533 check_admin_referer( 'fs_allow_clone_resolution_notice' );
23534
23535 $hide_admin_notices = false;
23536 }
23537
23538 if ( $hide_admin_notices ) {
23539 self::_hide_admin_notices();
23540 }
23541
23542 // Hide footer.
23543 echo '<style>#wpfooter { display: none !important; }</style>';
23544 }
23545
23546 /**
23547 * Attach to admin_head hook to hide all admin notices.
23548 *
23549 * @author Vova Feldman (@svovaf)
23550 * @since 1.0.3
23551 */
23552 static function _clean_admin_content_section() {
23553 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
23554 }
23555
23556 /* CSS & JavaScript
23557 ------------------------------------------------------------------------------------------------------------------*/
23558 /* function _enqueue_script($handle, $src) {
23559 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
23560
23561 $this->_logger->entrance( 'script = ' . $url );
23562
23563 wp_enqueue_script( $handle, $url );
23564 }*/
23565
23566 /* SDK
23567 ------------------------------------------------------------------------------------------------------------------*/
23568 private $_user_api;
23569
23570 /**
23571 *
23572 * @author Vova Feldman (@svovaf)
23573 * @since 1.0.2
23574 *
23575 * @param bool $flush
23576 *
23577 * @return FS_Api
23578 */
23579 function get_api_user_scope( $flush = false ) {
23580 if ( ! isset( $this->_user_api ) || $flush ) {
23581 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
23582 }
23583
23584 return $this->_user_api;
23585 }
23586
23587 /**
23588 * @author Vova Feldman (@svovaf)
23589 * @since 2.0.0
23590 *
23591 * @param \FS_User $user
23592 *
23593 * @return \FS_Api
23594 */
23595 private function get_api_user_scope_by_user( FS_User $user ) {
23596 return FS_Api::instance(
23597 $this->_module_id,
23598 'user',
23599 $user->id,
23600 $user->public_key,
23601 ! $this->is_live(),
23602 $user->secret_key,
23603 $this->get_sdk_version()
23604 );
23605 }
23606
23607 /**
23608 *
23609 * @author Leo Fajardo (@leorw)
23610 * @since 2.0.0
23611 *
23612 * @param bool $flush
23613 *
23614 * @return FS_Api
23615 */
23616 private function get_current_or_network_user_api_scope( $flush = false ) {
23617 if ( ! $this->_is_network_active ||
23618 ( isset( $this->_user ) && $this->_user instanceof FS_User )
23619 ) {
23620 return $this->get_api_user_scope( $flush );
23621 }
23622
23623 $user = $this->get_current_or_network_user();
23624
23625 $this->_user_api = FS_Api::instance(
23626 $this->_module_id,
23627 'user',
23628 $user->id,
23629 $user->public_key,
23630 ! $this->is_live(),
23631 $user->secret_key,
23632 $this->get_sdk_version()
23633 );
23634
23635 return $this->_user_api;
23636 }
23637
23638 private $_site_api;
23639
23640 /**
23641 *
23642 * @author Vova Feldman (@svovaf)
23643 * @since 1.0.2
23644 *
23645 * @param bool $flush
23646 *
23647 * @return FS_Api
23648 */
23649 private function get_api_site_scope( $flush = false ) {
23650 if ( ! isset( $this->_site_api ) || $flush ) {
23651 $this->_site_api = FS_Api::instance(
23652 $this->_module_id,
23653 'install',
23654 $this->_site->id,
23655 $this->_site->public_key,
23656 ! $this->is_live(),
23657 $this->_site->secret_key,
23658 $this->get_sdk_version(),
23659 self::get_unfiltered_site_url()
23660 );
23661 }
23662
23663 return $this->_site_api;
23664 }
23665
23666 /**
23667 * @author Leo Fajardo (@leorw)
23668 * @since 2.5.0
23669 *
23670 * @param string $path
23671 * @param string $method
23672 * @param array $params
23673 * @param bool $flush_instance
23674 *
23675 * @return array|mixed|string|void
23676 * @throws Freemius_Exception
23677 */
23678 private function api_site_call( $path, $method = 'GET', $params = array(), $flush_instance = false ) {
23679 $result = $this->get_api_site_scope( $flush_instance )->call( $path, $method, $params );
23680
23681 /**
23682 * 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.
23683 *
23684 * @author Leo Fajardo (@leorw)
23685 * @since 2.5.0
23686 */
23687 if (
23688 $this->is_registered() &&
23689 FS_Api::is_api_result_entity( $result ) &&
23690 isset( $result->url )
23691 ) {
23692 $stored_local_url = trailingslashit( $this->_site->url );
23693 $stored_remote_url = trailingslashit( $result->url );
23694
23695 if ( $stored_local_url !== $stored_remote_url ) {
23696 $this->_site->url = $result->url;
23697 $this->_store_site();
23698 }
23699
23700 if ( fs_strip_url_protocol( $stored_remote_url ) !== self::get_unfiltered_site_url( null, true, true ) ) {
23701 FS_Clone_Manager::instance()->maybe_run_clone_resolution();
23702 }
23703 }
23704
23705 return $result;
23706 }
23707
23708 private $_plugin_api;
23709
23710 /**
23711 * Get plugin public API scope.
23712 *
23713 * @author Vova Feldman (@svovaf)
23714 * @since 1.0.7
23715 *
23716 * @return FS_Api
23717 */
23718 function get_api_plugin_scope() {
23719 if ( ! isset( $this->_plugin_api ) ) {
23720 $this->_plugin_api = FS_Api::instance(
23721 $this->_module_id,
23722 'plugin',
23723 $this->_plugin->id,
23724 $this->_plugin->public_key,
23725 ! $this->is_live(),
23726 false,
23727 $this->get_sdk_version()
23728 );
23729 }
23730
23731 return $this->_plugin_api;
23732 }
23733
23734 /**
23735 * Get bundle public API scope.
23736 *
23737 * @author Vova Feldman (@svovaf)
23738 * @since 2.3.1
23739 *
23740 * @return FS_Api
23741 */
23742 function get_api_bundle_scope() {
23743 return FS_Api::instance(
23744 $this->get_bundle_id(),
23745 'plugin',
23746 $this->get_bundle_id(),
23747 $this->get_bundle_public_key(),
23748 ! $this->is_live(),
23749 false,
23750 $this->get_sdk_version()
23751 );
23752 }
23753
23754 /**
23755 * Get site API scope object (fallback to public plugin scope when not registered).
23756 *
23757 * @author Vova Feldman (@svovaf)
23758 * @since 1.0.7
23759 *
23760 * @return FS_Api
23761 */
23762 function get_api_site_or_plugin_scope() {
23763 return $this->is_registered() ?
23764 $this->get_api_site_scope() :
23765 $this->get_api_plugin_scope();
23766 }
23767
23768 /**
23769 * @author Leo Fajardo (@leorw)
23770 * @since 2.2.3.1
23771 *
23772 * @param object $result
23773 */
23774 private function maybe_modify_api_curl_error_message( $result ) {
23775 if (
23776 'cUrlMissing' !== $result->error->type &&
23777 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
23778 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
23779 ) {
23780 return;
23781 }
23782
23783 $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' ) .
23784 ' ' .
23785 $this->esc_html_inline(
23786 sprintf(
23787 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
23788 implode(
23789 ', ',
23790 $this->apply_filters( 'api_domains', array(
23791 'api.freemius.com',
23792 'wp.freemius.com'
23793 ) )
23794 )
23795 ),
23796 'connectivity-whitelist'
23797 ) .
23798 ' ' .
23799 sprintf(
23800 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
23801 $this->get_module_type()
23802 );
23803 }
23804
23805 /**
23806 * Show trial promotional notice (if any trial exist).
23807 *
23808 * @author Vova Feldman (@svovaf)
23809 * @since 1.0.9
23810 *
23811 * @param FS_Plugin_Plan[] $plans
23812 */
23813 function _check_for_trial_plans( $plans ) {
23814 /**
23815 * 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.
23816 *
23817 * @author Vova Feldman (@svovaf)
23818 * @since 2.1.2
23819 */
23820 if ( ! is_array( $plans ) && is_object( $plans ) ) {
23821 $plans = array( $plans );
23822 }
23823
23824 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
23825 $plans = array();
23826 }
23827
23828 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
23829 }
23830
23831 /**
23832 * During trial promotion the "upgrade" submenu item turns to
23833 * "start trial" to encourage the trial. Since we want to keep
23834 * the same menu item handler and there's no robust way to
23835 * add new arguments to the menu item link's querystring,
23836 * use JavaScript to find the menu item and update the href of
23837 * the link.
23838 *
23839 * @author Vova Feldman (@svovaf)
23840 * @since 1.2.1.5
23841 */
23842 function _fix_start_trial_menu_item_url() {
23843 $template_args = array( 'id' => $this->_module_id );
23844 fs_require_template( 'add-trial-to-pricing.php', $template_args );
23845 }
23846
23847 /**
23848 * Check if module is currently in a trial promotion mode.
23849 *
23850 * @author Vova Feldman (@svovaf)
23851 * @since 1.2.2.7
23852 *
23853 * @return bool
23854 */
23855 function is_in_trial_promotion() {
23856 return $this->_admin_notices->has_sticky( 'trial_promotion' );
23857 }
23858
23859 /**
23860 * Show trial promotional notice (if any trial exist).
23861 *
23862 * @author Vova Feldman (@svovaf)
23863 * @since 1.0.9
23864 *
23865 * @return bool If trial notice added.
23866 */
23867 function _add_trial_notice() {
23868 if ( ! $this->is_user_admin() ) {
23869 return false;
23870 }
23871
23872 if ( ! $this->is_user_in_admin() ) {
23873 return false;
23874 }
23875
23876 if ( $this->_is_network_active ) {
23877 if ( fs_is_network_admin() ) {
23878 // Network level trial is disabled at the moment.
23879 return false;
23880 }
23881
23882 if ( ! $this->is_delegated_connection() ) {
23883 // Only delegated sites should support trials.
23884 return false;
23885 }
23886 }
23887
23888 // Check if trial message is already shown.
23889 if ( $this->is_in_trial_promotion() ) {
23890 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
23891
23892 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
23893
23894 return false;
23895 }
23896
23897 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
23898 // Don't show trial if running the premium code, unless running in DEV mode.
23899 return false;
23900 }
23901
23902 if ( ! $this->has_trial_plan() ) {
23903 // No plans with trial.
23904 return false;
23905 }
23906
23907 if ( ! $this->apply_filters( 'show_trial', true ) ) {
23908 // Developer explicitly asked not to show the trial promo.
23909 return false;
23910 }
23911
23912 if ( $this->is_registered() ) {
23913 // Check if trial already utilized.
23914 if ( $this->_site->is_trial_utilized() ) {
23915 return false;
23916 }
23917
23918 if ( $this->is_paying_or_trial() ) {
23919 // Don't show trial if paying or already in trial.
23920 return false;
23921 }
23922 }
23923
23924 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
23925 // If not yet opted-in/skipped, or pending activation, don't show trial.
23926 return false;
23927 }
23928
23929 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
23930 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
23931
23932 // Show promotion if never shown before and 24 hours after initial activation with FS.
23933 if ( ! $was_promotion_shown_before &&
23934 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
23935 ) {
23936 return false;
23937 }
23938
23939 // OR if promotion was shown before, try showing it every 30 days.
23940 if ( $was_promotion_shown_before &&
23941 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
23942 ) {
23943 return false;
23944 }
23945
23946 $trial_period = $this->_trial_days;
23947 $require_payment = $this->_is_trial_require_payment;
23948 $trial_url = $this->get_trial_url();
23949 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
23950
23951 if ( $this->is_registered() ) {
23952 // If opted-in, override trial with up to date data from API.
23953 $trial_plans = FS_Plan_Manager::instance()->get_visible_trial_plans( $this->_plans );
23954 $trial_plans_count = count( $trial_plans );
23955
23956 if ( 0 === $trial_plans_count ) {
23957 // If there's no plans with a trial just exit.
23958 return false;
23959 }
23960
23961 /**
23962 * @var FS_Plugin_Plan $paid_plan
23963 */
23964 $paid_plan = $trial_plans[0];
23965 $require_payment = $paid_plan->is_require_subscription;
23966 $trial_period = $paid_plan->trial_period;
23967
23968 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
23969
23970 if ( $total_paid_plans !== $trial_plans_count ) {
23971 // Not all paid plans have a trial - generate a string of those that have it.
23972 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
23973 $plans_string .= sprintf(
23974 ' <a href="%s">%s</a>',
23975 $trial_url,
23976 $trial_plans[ $i ]->title
23977 );
23978
23979 if ( $i < $trial_plans_count - 2 ) {
23980 $plans_string .= ', ';
23981 } else if ( $i == $trial_plans_count - 2 ) {
23982 $plans_string .= ' and ';
23983 }
23984 }
23985 }
23986 }
23987
23988 $message = sprintf(
23989 $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' ),
23990 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
23991 $plans_string,
23992 $trial_period
23993 );
23994
23995 // "No Credit-Card Required" or "No Commitment for N Days".
23996 $cc_string = $require_payment ?
23997 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
23998 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
23999
24000
24001 // Start trial button.
24002 $button = ' ' . sprintf(
24003 '<div><a class="button button-primary" href="%s">%s &nbsp;&#10140;</a></div>',
24004 $trial_url,
24005 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
24006 );
24007
24008 $message_text = $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string}" );
24009
24010 $this->_admin_notices->add_sticky(
24011 "<div class=\"fs-trial-message-container\"><div>{$message_text}</div> {$button}</div>",
24012 'trial_promotion',
24013 '',
24014 'promotion'
24015 );
24016
24017 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
24018
24019 return true;
24020 }
24021
24022 /**
24023 * Lets users/customers know that the product has an affiliate program.
24024 *
24025 * @author Leo Fajardo (@leorw)
24026 * @since 1.2.2.11
24027 *
24028 * @return bool Returns true if the notice has been added.
24029 */
24030 function _add_affiliate_program_notice() {
24031 if ( ! $this->is_user_admin() ) {
24032 return false;
24033 }
24034
24035 if ( ! $this->is_user_in_admin() ) {
24036 return false;
24037 }
24038
24039 // Check if the notice is already shown.
24040 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
24041 return false;
24042 }
24043
24044 if (
24045 // Product has no affiliate program.
24046 ! $this->has_affiliate_program() ||
24047 // User has applied for an affiliate account.
24048 ! empty( $this->_storage->affiliate_application_data )
24049 ) {
24050 return false;
24051 }
24052
24053 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
24054 // Developer explicitly asked not to show the notice about the affiliate program.
24055 return false;
24056 }
24057
24058 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24059 // If not yet opted in/skipped, or pending activation, don't show the notice.
24060 return false;
24061 }
24062
24063 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
24064 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
24065
24066 /**
24067 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
24068 * activation with FS.
24069 */
24070 if ( $was_notice_shown_before ||
24071 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
24072 ) {
24073 return false;
24074 }
24075
24076 if ( ! $this->is_paying() &&
24077 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
24078 ) {
24079 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
24080 return false;
24081 }
24082
24083 $message = sprintf(
24084 $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' ),
24085 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
24086 $this->get_module_label( true )
24087 );
24088
24089 // HTML code for the "Learn more..." button.
24090 $button = ' ' . sprintf(
24091 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24092 $this->_get_admin_page_url( 'affiliation' ),
24093 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
24094 );
24095
24096 $this->_admin_notices->add_sticky(
24097 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
24098 'affiliate_program',
24099 '',
24100 'promotion'
24101 );
24102
24103 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
24104
24105 return true;
24106 }
24107
24108 /**
24109 * @author Vova Feldman (@svovaf)
24110 * @since 1.2.1.5
24111 */
24112 function _enqueue_common_css() {
24113 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
24114 // Add basic CSS for admin-notices and menu-item colors.
24115 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
24116 }
24117 }
24118
24119 /**
24120 * @author Leo Fajardo (@leorw)
24121 * @since 1.2.2
24122 */
24123 function _show_theme_activation_optin_dialog() {
24124 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
24125
24126 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
24127 }
24128
24129 /**
24130 * @author Leo Fajardo (@leorw)
24131 * @since 1.2.2
24132 */
24133 function _add_fs_theme_activation_dialog() {
24134 global $pagenow;
24135
24136 if ( 'themes.php' !== $pagenow ) {
24137 return;
24138 }
24139
24140 $vars = array( 'id' => $this->_module_id );
24141 fs_require_once_template( 'connect.php', $vars );
24142 }
24143
24144 /* Action Links
24145 ------------------------------------------------------------------------------------------------------------------*/
24146 private $_action_links_hooked = false;
24147 private $_action_links = array();
24148
24149 /**
24150 * Hook to plugin action links filter.
24151 *
24152 * @author Vova Feldman (@svovaf)
24153 * @since 1.0.0
24154 */
24155 private function hook_plugin_action_links() {
24156 $this->_logger->entrance();
24157
24158 $this->_action_links_hooked = true;
24159
24160 $this->_logger->log( 'Adding action links hooks.' );
24161
24162 // Add action link to settings page.
24163 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
24164 &$this,
24165 '_modify_plugin_action_links_hook'
24166 ), WP_FS__DEFAULT_PRIORITY, 2 );
24167 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
24168 &$this,
24169 '_modify_plugin_action_links_hook'
24170 ), WP_FS__DEFAULT_PRIORITY, 2 );
24171 }
24172
24173 /**
24174 * Add plugin action link.
24175 *
24176 * @author Vova Feldman (@svovaf)
24177 * @since 1.0.0
24178 *
24179 * @param $label
24180 * @param $url
24181 * @param bool $external
24182 * @param int $priority
24183 * @param bool $key
24184 */
24185 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
24186 $this->_logger->entrance();
24187
24188 if ( ! isset( $this->_action_links[ $priority ] ) ) {
24189 $this->_action_links[ $priority ] = array();
24190 }
24191
24192 if ( false === $key ) {
24193 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
24194 }
24195
24196 $this->_action_links[ $priority ][] = array(
24197 'label' => $label,
24198 'href' => $url,
24199 'key' => $key,
24200 'external' => $external
24201 );
24202 }
24203
24204 /**
24205 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
24206 *
24207 * @author Vova Feldman (@svovaf)
24208 * @since 1.0.0
24209 */
24210 function _add_upgrade_action_link() {
24211 $this->_logger->entrance();
24212
24213 $is_activation_mode = $this->is_activation_mode();
24214
24215 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
24216
24217 /**
24218 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
24219 * and "Add-Ons" menus should be added.
24220 *
24221 * @author Leo Fajardo (@leorw)
24222 * @since 2.3.0
24223 */
24224 $add_upgrade_link = (
24225 $add_action_links ||
24226 ( $is_activation_mode && $this->is_only_premium() )
24227 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
24228
24229 $add_addons_link = ( $add_action_links && $this->has_addons() );
24230
24231 if ( ! $add_upgrade_link && ! $add_addons_link ) {
24232 return;
24233 }
24234
24235 if (
24236 $add_upgrade_link &&
24237 $this->is_pricing_page_visible() &&
24238 $this->is_submenu_item_visible( 'pricing' )
24239 ) {
24240 $this->add_plugin_action_link(
24241 $this->get_text_inline( 'Upgrade', 'upgrade' ),
24242 $this->get_upgrade_url(),
24243 false,
24244 7,
24245 'upgrade'
24246 );
24247 }
24248
24249 if (
24250 $add_addons_link &&
24251 $this->has_addons() &&
24252 $this->is_submenu_item_visible( 'addons' )
24253 ) {
24254 $this->add_plugin_action_link(
24255 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
24256 $this->_get_admin_page_url( 'addons' ),
24257 false,
24258 9,
24259 'addons'
24260 );
24261 }
24262 }
24263
24264 /**
24265 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
24266 *
24267 * @author Leo Fajardo (@leorw)
24268 * @since 1.1.9
24269 */
24270 function _add_license_action_link() {
24271 $this->_logger->entrance();
24272
24273 if ( ! self::is_ajax() ) {
24274 // Inject license activation dialog UI and client side code.
24275 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
24276 }
24277
24278 $link_text = $this->is_free_plan() ?
24279 $this->get_text_inline( 'Activate License', 'activate-license' ) :
24280 $this->get_text_inline( 'Change License', 'change-license' );
24281
24282 $this->add_plugin_action_link(
24283 $link_text,
24284 '#',
24285 false,
24286 11,
24287 ( 'activate-license ' . $this->get_unique_affix() )
24288 );
24289 }
24290
24291 /**
24292 * @author Leo Fajardo (@leorw)
24293 * @since 2.0.2
24294 */
24295 function _add_premium_version_upgrade_selection_action() {
24296 $this->_logger->entrance();
24297
24298 if ( ! self::is_ajax() ) {
24299 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
24300 }
24301 }
24302
24303 /**
24304 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
24305 *
24306 * @author Leo Fajardo (@leorw)
24307 * @since 1.2.1.5
24308 */
24309 function _add_tracking_links() {
24310 if ( ! current_user_can( 'manage_options' ) ) {
24311 return;
24312 }
24313
24314 $this->_logger->entrance();
24315
24316 if ( $this->is_only_premium() && $this->is_free_plan() ) {
24317 // 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.
24318 return;
24319 }
24320
24321 if (
24322 $this->is_addon() &&
24323 ! $this->is_only_premium()
24324 ) {
24325 $parent = $this->get_parent_instance();
24326
24327 if ( is_object( $parent ) && $parent->is_anonymous() ) {
24328 return;
24329 }
24330 }
24331
24332 if ( fs_is_network_admin() ) {
24333 if ( ! $this->_is_network_active ) {
24334 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
24335 return;
24336 } else if ( $this->is_network_delegated_connection() ) {
24337 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
24338 return;
24339 }
24340 } else {
24341 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
24342 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
24343 return;
24344 }
24345 }
24346
24347 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
24348 if ( ! $this->is_registered() && $this->is_anonymous() ) {
24349 $this->connect_again();
24350
24351 return;
24352 }
24353 }
24354
24355 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
24356 ( $this->is_theme() && ! self::is_themes_page() )
24357 ) {
24358 // Only show tracking links on the plugins and themes pages.
24359 return;
24360 }
24361
24362 if (
24363 $this->is_activation_mode() &&
24364 $this->is_premium() &&
24365 ! $this->is_registered()
24366 ) {
24367 // If not yet registered and running the premium code base, a license activation link will already be shown.
24368 return;
24369 }
24370
24371 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24372 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
24373 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
24374 return;
24375 }
24376 }
24377
24378 if ( $this->add_ajax_action( 'toggle_permission_tracking', array( &$this, '_toggle_permission_tracking_callback' ) ) ) {
24379 return;
24380 }
24381
24382 $link_text_id = '';
24383 $url = '#';
24384
24385 if ( $this->is_registered( true ) ) {
24386 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24387 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
24388 } else {
24389 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24390 }
24391 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
24392 /**
24393 * Show opt-in link only if skipped or in activation mode.
24394 */
24395 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24396
24397 $params = ! $this->is_anonymous() ?
24398 array() :
24399 array(
24400 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
24401 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
24402 );
24403
24404 $url = $this->get_activation_url( $params );
24405 }
24406
24407 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
24408
24409 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
24410 $this->add_plugin_action_link(
24411 $link_text_id,
24412 $url,
24413 false,
24414 13,
24415 "opt-in-or-opt-out {$this->_slug}"
24416 );
24417 }
24418 }
24419
24420 /**
24421 * Get the URL of the page that should be loaded right after the plugin activation.
24422 *
24423 * @author Vova Feldman (@svovaf)
24424 * @since 1.1.7.4
24425 *
24426 * @return string
24427 */
24428 function get_after_plugin_activation_redirect_url() {
24429 $url = false;
24430
24431 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
24432 $first_time_path = $this->_menu->get_first_time_path(
24433 fs_is_network_admin() && $this->_is_network_active
24434 );
24435
24436 if ( $this->is_activation_mode() ) {
24437 $url = $this->get_activation_url();
24438 } else if ( ! empty( $first_time_path ) ) {
24439 $url = $first_time_path;
24440 } else {
24441 $page = '';
24442 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
24443 if ( $this->is_network_registered() ) {
24444 $page = 'account';
24445 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
24446 $this->maybe_set_slug_and_network_menu_exists_flag();
24447 }
24448 }
24449
24450 $url = $this->_get_admin_page_url( $page );
24451 }
24452 } else {
24453 $plugin_fs = false;
24454
24455 if ( $this->is_parent_plugin_installed() ) {
24456 $plugin_fs = self::get_parent_instance();
24457 }
24458
24459 if ( is_object( $plugin_fs ) ) {
24460 if ( ! $plugin_fs->is_registered() ) {
24461 // Forward to parent plugin connect when parent not registered.
24462 $url = $plugin_fs->get_activation_url();
24463 } else {
24464 // Forward to account page.
24465 $url = $plugin_fs->_get_admin_page_url( 'account' );
24466 }
24467 }
24468 }
24469
24470 return $url;
24471 }
24472
24473 /**
24474 * Forward page to activation page.
24475 *
24476 * @author Vova Feldman (@svovaf)
24477 * @since 1.0.3
24478 */
24479 function _redirect_on_activation_hook() {
24480 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
24481 $url = $this->get_after_plugin_activation_redirect_url();
24482
24483 if ( is_string( $url ) ) {
24484 fs_redirect( $url );
24485 }
24486 }
24487 }
24488
24489 /**
24490 * Modify plugin's page action links collection.
24491 *
24492 * @author Vova Feldman (@svovaf)
24493 * @since 1.0.0
24494 *
24495 * @param array $links
24496 * @param $file
24497 *
24498 * @return array
24499 */
24500 function _modify_plugin_action_links_hook( $links, $file ) {
24501 $this->_logger->entrance();
24502
24503 $passed_deactivate = false;
24504 $deactivate_link = '';
24505 $before_deactivate = array();
24506 $after_deactivate = array();
24507 foreach ( $links as $key => $link ) {
24508 if ( 'deactivate' === $key ) {
24509 $deactivate_link = $link;
24510 $passed_deactivate = true;
24511 continue;
24512 }
24513
24514 if ( ! $passed_deactivate ) {
24515 $before_deactivate[ $key ] = $link;
24516 } else {
24517 $after_deactivate[ $key ] = $link;
24518 }
24519 }
24520
24521 ksort( $this->_action_links );
24522
24523 foreach ( $this->_action_links as $new_links ) {
24524 foreach ( $new_links as $link ) {
24525 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
24526 }
24527 }
24528
24529 if ( ! empty( $deactivate_link ) ) {
24530 /**
24531 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
24532 *
24533 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
24534 */
24535 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
24536
24537 // Append deactivation link.
24538 $before_deactivate['deactivate'] = $deactivate_link;
24539 }
24540
24541 return array_merge( $before_deactivate, $after_deactivate );
24542 }
24543
24544 /**
24545 * Adds admin message.
24546 *
24547 * @author Vova Feldman (@svovaf)
24548 * @since 1.0.4
24549 *
24550 * @param string $message
24551 * @param string $title
24552 * @param string $type
24553 */
24554 function add_admin_message( $message, $title = '', $type = 'success' ) {
24555 $this->_admin_notices->add( $message, $title, $type );
24556 }
24557
24558 /**
24559 * Adds sticky admin message.
24560 *
24561 * @author Vova Feldman (@svovaf)
24562 * @since 1.1.0
24563 *
24564 * @param string $message
24565 * @param string $id
24566 * @param string $title
24567 * @param string $type
24568 */
24569 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
24570 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
24571 }
24572
24573 /**
24574 * Check if the paid version of the module is installed.
24575 *
24576 * @author Vova Feldman (@svovaf)
24577 * @since 2.2.0
24578 *
24579 * @return bool
24580 */
24581 private function is_premium_version_installed() {
24582 $premium_plugin_basename = $this->premium_plugin_basename();
24583
24584 if ( $this->is_theme() ) {
24585 return $this->can_activate_theme( $this->get_premium_slug() );
24586 }
24587
24588 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_plugin_basename ) );
24589 }
24590
24591 /**
24592 * Helper function that returns the final steps for the upgrade completion.
24593 *
24594 * If the module is already running the premium code, returns an empty string.
24595 *
24596 * @author Vova Feldman (@svovaf)
24597 * @since 1.2.1
24598 *
24599 * @param string $plan_title
24600 *
24601 * @return string
24602 */
24603 private function get_complete_upgrade_instructions( $plan_title = '' ) {
24604 $this->_logger->entrance();
24605
24606 $activate_license_string = $this->get_license_network_activation_notice();
24607
24608 if ( ! $this->has_premium_version() || $this->is_premium() ) {
24609 return '' . $activate_license_string;
24610 }
24611
24612 if ( empty( $plan_title ) ) {
24613 $plan_title = $this->get_plan_title();
24614 }
24615
24616 if ( $this->is_premium_version_installed() ) {
24617 /**
24618 * If the premium version is already installed, instead of showing the installation instructions,
24619 * tell the current user to activate it.
24620 *
24621 * @author Leo Fajardo (@leorw)
24622 * @since 2.2.1
24623 */
24624 $premium_theme_slug_or_plugin_basename = $this->is_theme() ?
24625 $this->get_premium_slug() :
24626 $this->premium_plugin_basename();
24627
24628 return sprintf(
24629 /* translators: %1$s: Product title; %2$s: Plan title */
24630 $this->get_text_inline( ' The paid version of %1$s is already installed. Please activate it to start benefiting the %2$s features. %3$s', 'activate-premium-version' ),
24631 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24632 $plan_title,
24633 sprintf(
24634 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
24635 ( $this->is_theme() ?
24636 wp_nonce_url( 'themes.php?action=activate&amp;stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) :
24637 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ),
24638 esc_html( sprintf(
24639 /* translators: %s: Plan title */
24640 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
24641 $plan_title
24642 ) )
24643 )
24644 );
24645 } else {
24646 // @since 1.2.1.5 The free version is auto deactivated.
24647 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
24648 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
24649 '';
24650
24651 return sprintf(
24652 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
24653 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
24654 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
24655 $this->get_latest_download_link( sprintf(
24656 /* translators: %s: Plan title */
24657 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
24658 $plan_title
24659 ) ),
24660 $deactivation_step,
24661 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
24662 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/wp-' . $this->_module_type . '-upload' ),
24663 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
24664 );
24665 }
24666 }
24667
24668 /**
24669 * @author Leo Fajardo (@leorw)
24670 * @since 2.5.3
24671 *
24672 * @param string $message_before_the_instructions
24673 * @param string $message_id
24674 * @param string $plan_title
24675 */
24676 private function add_complete_upgrade_instructions_notice(
24677 $message_before_the_instructions,
24678 $message_id,
24679 $plan_title = ''
24680 ) {
24681 $this->_admin_notices->add_sticky(
24682 $message_before_the_instructions .
24683 $this->get_complete_upgrade_instructions( $plan_title ),
24684 $message_id,
24685 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
24686 );
24687 }
24688
24689 /**
24690 * @author Leo Fajardo (@leorw)
24691 * @since 2.5.3
24692 *
24693 * @param bool $is_upgrade
24694 */
24695 private function add_after_plan_activation_or_upgrade_instructions_notice( $is_upgrade = true ) {
24696 $this->add_complete_upgrade_instructions_notice(
24697 $is_upgrade ?
24698 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) :
24699 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ),
24700 'plan_upgraded'
24701 );
24702 }
24703
24704 /**
24705 * @author Leo Fajardo (@leorw)
24706 * @since 2.1.0
24707 *
24708 * @param string $url
24709 * @param array $request
24710 * @param int $success_cache_expiration
24711 * @param int $failure_cache_expiration
24712 * @param bool $maybe_enrich_request_for_debug
24713 *
24714 * @return WP_Error|array
24715 */
24716 static function safe_remote_post(
24717 &$url,
24718 $request,
24719 $success_cache_expiration = 0,
24720 $failure_cache_expiration = 0,
24721 $maybe_enrich_request_for_debug = true
24722 ) {
24723 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
24724
24725 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
24726
24727 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
24728 get_transient( $cache_key ) :
24729 false;
24730
24731 if ( false === $response ) {
24732 if ( $maybe_enrich_request_for_debug ) {
24733 FS_DebugManager::enrich_request_for_debug( $url, $request );
24734 }
24735
24736 if ( ! isset( $request['method'] ) ) {
24737 $request['method'] = 'POST';
24738 }
24739
24740 $response = FS_Api::remote_request( $url, $request );
24741
24742 if (
24743 'https://' === substr( $url, 0, 8 ) &&
24744 FS_Api::is_ssl_error_response( $response )
24745 ) {
24746 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
24747 $url = 'http://' . substr( $url, 8 );
24748
24749 $request['timeout'] = 15;
24750 $response = FS_Api::remote_request( $url, $request );
24751 }
24752
24753 if ( false !== $cache_key ) {
24754 set_transient(
24755 $cache_key,
24756 $response,
24757 ( ( $response instanceof WP_Error ) ?
24758 $failure_cache_expiration :
24759 $success_cache_expiration )
24760 );
24761 }
24762 }
24763
24764 return $response;
24765 }
24766
24767 /**
24768 * This method is used to enrich the after upgrade notice instructions when the upgraded
24769 * license cannot be activated network wide (license quota isn't large enough).
24770 *
24771 * @author Vova Feldman (@svovaf)
24772 * @since 2.0.0
24773 *
24774 * @return string
24775 */
24776 private function get_license_network_activation_notice() {
24777 if ( ! $this->_is_network_active ) {
24778 // Module isn't network level activated.
24779 return '';
24780 }
24781
24782 if ( ! fs_is_network_admin() ) {
24783 // Not network level admin.
24784 return '';
24785 }
24786
24787 if ( get_blog_count() == 1 ) {
24788 // There's only a single site in the network so if there's a context license it was already activated.
24789 return '';
24790 }
24791
24792 if ( ! is_object( $this->_license ) ) {
24793 // No context license.
24794 return '';
24795 }
24796
24797 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
24798 // 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).
24799 return '';
24800 }
24801
24802 if ( $this->can_activate_license_on_network( $this->_license ) ) {
24803 // 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).
24804 return '';
24805 }
24806
24807 return sprintf(
24808 $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' ),
24809 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
24810 '</a>'
24811 );
24812 }
24813
24814 /**
24815 * @author Vova Feldman (@svovaf)
24816 * @since 1.2.1.7
24817 *
24818 * @param string $key
24819 *
24820 * @return string
24821 */
24822 function get_text( $key ) {
24823 return fs_text( $key, $this->_slug );
24824 }
24825
24826 /**
24827 * @author Vova Feldman (@svovaf)
24828 * @since 1.2.3
24829 *
24830 * @param string $text Translatable string.
24831 * @param string $key String key for overrides.
24832 *
24833 * @return string
24834 */
24835 function get_text_inline( $text, $key = '' ) {
24836 return _fs_text_inline( $text, $key, $this->_slug );
24837 }
24838
24839 /**
24840 * @author Vova Feldman (@svovaf)
24841 * @since 1.2.3
24842 *
24843 * @param string $text Translatable string.
24844 * @param string $context Context information for the translators.
24845 * @param string $key String key for overrides.
24846 *
24847 * @return string
24848 */
24849 function get_text_x_inline( $text, $context, $key ) {
24850 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
24851 }
24852
24853 /**
24854 * @author Vova Feldman (@svovaf)
24855 * @since 1.2.3
24856 *
24857 * @param string $text Translatable string.
24858 * @param string $key String key for overrides.
24859 *
24860 * @return string
24861 */
24862 function esc_html_inline( $text, $key ) {
24863 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
24864 }
24865
24866 #----------------------------------------------------------------------------------
24867 #region Versioning
24868 #----------------------------------------------------------------------------------
24869
24870 /**
24871 * Check if Freemius in SDK upgrade mode.
24872 *
24873 * @author Vova Feldman (@svovaf)
24874 * @since 1.0.9
24875 *
24876 * @return bool
24877 */
24878 function is_sdk_upgrade_mode() {
24879 return isset( $this->_storage->sdk_upgrade_mode ) ?
24880 $this->_storage->sdk_upgrade_mode :
24881 false;
24882 }
24883
24884 /**
24885 * Turn SDK upgrade mode off.
24886 *
24887 * @author Vova Feldman (@svovaf)
24888 * @since 1.0.9
24889 */
24890 function set_sdk_upgrade_complete() {
24891 $this->_storage->sdk_upgrade_mode = false;
24892 }
24893
24894 /**
24895 * Check if plugin upgrade mode.
24896 *
24897 * @author Vova Feldman (@svovaf)
24898 * @since 1.0.9
24899 *
24900 * @return bool
24901 */
24902 function is_plugin_upgrade_mode() {
24903 return isset( $this->_storage->plugin_upgrade_mode ) ?
24904 $this->_storage->plugin_upgrade_mode :
24905 false;
24906 }
24907
24908 /**
24909 * Turn plugin upgrade mode off.
24910 *
24911 * @author Vova Feldman (@svovaf)
24912 * @since 1.0.9
24913 */
24914 function set_plugin_upgrade_complete() {
24915 $this->_storage->plugin_upgrade_mode = false;
24916
24917 $license_migration = ! empty( $this->_storage->license_migration ) ?
24918 $this->_storage->license_migration :
24919 array();
24920
24921 $license_migration['is_migrating'] = false;
24922
24923 $this->_storage->license_migration = $license_migration;
24924 }
24925
24926 #endregion
24927
24928 #----------------------------------------------------------------------------------
24929 #region Permissions
24930 #----------------------------------------------------------------------------------
24931
24932 /**
24933 * Check if specific permission requested.
24934 *
24935 * @author Vova Feldman (@svovaf)
24936 * @since 1.1.6
24937 *
24938 * @param string $permission
24939 *
24940 * @return bool
24941 */
24942 function is_permission_requested( $permission ) {
24943 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
24944 }
24945
24946 #endregion
24947
24948 #----------------------------------------------------------------------------------
24949 #region Auto Activation
24950 #----------------------------------------------------------------------------------
24951
24952 /**
24953 * Hints the SDK if running an auto-installation.
24954 *
24955 * @var bool
24956 */
24957 private $_isAutoInstall = false;
24958
24959 /**
24960 * After upgrade callback to install and auto activate a plugin.
24961 * This code will only be executed on explicit request from the user,
24962 * following the practice Jetpack are using with their theme installations.
24963 *
24964 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
24965 *
24966 * @author Vova Feldman (@svovaf)
24967 * @since 1.2.1.7
24968 */
24969 function _install_premium_version_ajax_action() {
24970 $this->_logger->entrance();
24971
24972 $this->check_ajax_referer( 'install_premium_version' );
24973
24974 if ( ! $this->is_registered() ) {
24975 // Not registered.
24976 self::shoot_ajax_failure( array(
24977 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
24978 'code' => 'premium_installed',
24979 ) );
24980 }
24981
24982 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
24983
24984 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
24985 // Invalid ID.
24986 self::shoot_ajax_failure( array(
24987 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
24988 'code' => 'invalid_module_id',
24989 ) );
24990 }
24991
24992 if ( $plugin_id == $this->get_id() ) {
24993 if ( $this->is_premium() ) {
24994 // Already using the premium code version.
24995 self::shoot_ajax_failure( array(
24996 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
24997 'code' => 'premium_installed',
24998 ) );
24999 }
25000 if ( ! $this->can_use_premium_code() ) {
25001 // Don't have access to the premium code.
25002 self::shoot_ajax_failure( array(
25003 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
25004 'code' => 'invalid_license',
25005 ) );
25006 }
25007 if ( ! $this->has_release_on_freemius() ) {
25008 // Plugin is a serviceware, no premium code version.
25009 self::shoot_ajax_failure( array(
25010 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
25011 'code' => 'premium_version_missing',
25012 ) );
25013 }
25014 } else {
25015 $addon = $this->get_addon( $plugin_id );
25016
25017 if ( ! is_object( $addon ) ) {
25018 // Invalid add-on ID.
25019 self::shoot_ajax_failure( array(
25020 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25021 'code' => 'invalid_module_id',
25022 ) );
25023 }
25024
25025 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25026 // Premium add-on version is already activated.
25027 self::shoot_ajax_failure( array(
25028 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
25029 'code' => 'premium_installed',
25030 ) );
25031 }
25032 }
25033
25034 $this->_isAutoInstall = true;
25035
25036 // Try to install and activate.
25037 $updater = FS_Plugin_Updater::instance( $this );
25038 $result = $updater->install_and_activate_plugin( $plugin_id );
25039
25040 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
25041 self::shoot_ajax_failure( array(
25042 'message' => $result['message'],
25043 'code' => $result['code'],
25044 ) );
25045 }
25046
25047 self::shoot_ajax_success( $result );
25048 }
25049
25050 /**
25051 * Displays module activation dialog box after a successful upgrade
25052 * where the user explicitly requested to auto download and install
25053 * the premium version.
25054 *
25055 * @author Vova Feldman (@svovaf)
25056 * @since 1.2.1.7
25057 */
25058 function _add_auto_installation_dialog_box() {
25059 $this->_logger->entrance();
25060
25061 if ( ! $this->is_registered() ) {
25062 // Not registered.
25063 return;
25064 }
25065
25066 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
25067
25068 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25069 // Invalid module ID.
25070 return;
25071 }
25072
25073 if ( $plugin_id == $this->get_id() ) {
25074 if ( $this->is_premium() ) {
25075 // Already using the premium code version.
25076 return;
25077 }
25078 if ( ! $this->can_use_premium_code() ) {
25079 // Don't have access to the premium code.
25080 return;
25081 }
25082 if ( ! $this->has_release_on_freemius() ) {
25083 // Plugin is a serviceware, no premium code version.
25084 return;
25085 }
25086 } else {
25087 $addon = $this->get_addon( $plugin_id );
25088
25089 if ( ! is_object( $addon ) ) {
25090 // Invalid add-on ID.
25091 return;
25092 }
25093
25094 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25095 // Premium add-on version is already activated.
25096 return;
25097 }
25098 }
25099
25100 $vars = array(
25101 'id' => $this->_module_id,
25102 'target_module_id' => $plugin_id,
25103 'slug' => $this->_slug,
25104 );
25105
25106 fs_require_template( 'auto-installation.php', $vars );
25107 }
25108
25109 #endregion
25110
25111 #--------------------------------------------------------------------------------
25112 #region Tabs Integration
25113 #--------------------------------------------------------------------------------
25114
25115 #region Module's Original Tabs
25116
25117 /**
25118 * Inject a JavaScript logic to capture the theme tabs HTML.
25119 *
25120 * @author Vova Feldman (@svovaf)
25121 * @since 1.2.2.7
25122 */
25123 function _tabs_capture() {
25124 $this->_logger->entrance();
25125
25126 if (
25127 ! $this->is_product_settings_page() ||
25128 ! $this->should_page_include_tabs() ||
25129 ! $this->is_matching_url( $this->main_menu_url() )
25130 ) {
25131 return;
25132 }
25133
25134 $params = array(
25135 'id' => $this->_module_id,
25136 );
25137
25138 fs_require_once_template( 'tabs-capture-js.php', $params );
25139 }
25140
25141 /**
25142 * Cache theme's tabs HTML for a week. The cache will also be set as expired
25143 * after version and type (free/premium) changes, in addition to the week period.
25144 *
25145 * @author Vova Feldman (@svovaf)
25146 * @since 1.2.2.7
25147 */
25148 function _store_tabs_ajax_action() {
25149 $this->_logger->entrance();
25150
25151 $this->check_ajax_referer( 'store_tabs' );
25152
25153 // Init filesystem if not yet initiated.
25154 WP_Filesystem();
25155
25156 // Get POST body HTML data.
25157 global $wp_filesystem;
25158 $tabs_html = $wp_filesystem->get_contents( "php://input" );
25159
25160 if ( is_string( $tabs_html ) ) {
25161 $tabs_html = trim( $tabs_html );
25162 }
25163
25164 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
25165 self::shoot_ajax_failure();
25166 }
25167
25168 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25169
25170 self::shoot_ajax_success();
25171 }
25172
25173 /**
25174 * Cache theme's settings page custom styles. The cache will also be set as expired
25175 * after version and type (free/premium) changes, in addition to the week period.
25176 *
25177 * @author Vova Feldman (@svovaf)
25178 * @since 1.2.2.7
25179 */
25180 function _store_tabs_styles() {
25181 $this->_logger->entrance();
25182
25183 if (
25184 ! $this->is_product_settings_page() ||
25185 ! $this->should_page_include_tabs() ||
25186 ! $this->is_matching_url( $this->main_menu_url() )
25187 ) {
25188 return;
25189 }
25190
25191 $wp_styles = wp_styles();
25192
25193 $theme_styles_url = get_template_directory_uri();
25194
25195 $stylesheets = array();
25196 foreach ( $wp_styles->queue as $handler ) {
25197 if ( fs_starts_with( $handler, 'fs_' ) ) {
25198 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
25199 continue;
25200 }
25201
25202 /**
25203 * @var _WP_Dependency $stylesheet
25204 */
25205 $stylesheet = $wp_styles->registered[ $handler ];
25206
25207 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
25208 $stylesheets[] = $stylesheet->src;
25209 }
25210 }
25211
25212 if ( ! empty( $stylesheets ) ) {
25213 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25214 }
25215 }
25216
25217 /**
25218 * Check if module's original settings page has any tabs.
25219 *
25220 * @author Vova Feldman (@svovaf)
25221 * @since 1.2.2.7
25222 *
25223 * @return bool
25224 */
25225 private function has_tabs() {
25226 return $this->_cache->has( 'tabs' );
25227 }
25228
25229 /**
25230 * Get module's settings page HTML content, starting
25231 * from the beginning of the <div class="wrap"> element,
25232 * until the tabs HTML (including).
25233 *
25234 * @author Vova Feldman (@svovaf)
25235 * @since 1.2.2.7
25236 *
25237 * @return string
25238 */
25239 private function get_tabs_html() {
25240 $this->_logger->entrance();
25241
25242 return $this->_cache->get( 'tabs' );
25243 }
25244
25245 /**
25246 * Check if page should include tabs.
25247 *
25248 * @author Vova Feldman (@svovaf)
25249 * @since 1.2.2.7
25250 *
25251 * @return bool
25252 */
25253 private function should_page_include_tabs() {
25254 if ( ! $this->has_settings_menu() ) {
25255 // Don't add tabs if no settings at all.
25256 return false;
25257 }
25258
25259 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
25260 // Only add tabs to themes for now.
25261 return false;
25262 }
25263
25264 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
25265 // Only add tabs to monetizing themes.
25266 return false;
25267 }
25268
25269 if ( ! $this->is_product_settings_page() ) {
25270 // Only add tabs if browsing one of the product's setting pages.
25271 return false;
25272 }
25273
25274 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
25275 // Don't include tabs in the activation page.
25276 return false;
25277 }
25278
25279 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
25280 // Don't add tabs on checkout page, we want to reduce distractions
25281 // as much as possible.
25282 return false;
25283 }
25284
25285 return true;
25286 }
25287
25288 /**
25289 * Add the tabs HTML before the setting's page content and
25290 * enqueue any required stylesheets.
25291 *
25292 * @author Vova Feldman (@svovaf)
25293 * @since 1.2.2.7
25294 *
25295 * @return bool If tabs were included.
25296 */
25297 function _add_tabs_before_content() {
25298 $this->_logger->entrance();
25299
25300 if ( ! $this->should_page_include_tabs() ) {
25301 return false;
25302 }
25303
25304 $tabs_html = $this->get_tabs_html();
25305
25306 if ( empty( $tabs_html ) ) {
25307 return false;
25308 }
25309
25310 /**
25311 * Enqueue the original stylesheets that are included in the
25312 * theme settings page. That way, if the theme settings has
25313 * some custom _styled_ content above the tabs UI, this
25314 * will make sure that the styling is preserved.
25315 */
25316 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
25317 if ( is_array( $stylesheets ) ) {
25318 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
25319 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
25320 }
25321 }
25322
25323 // Cut closing </div> tag.
25324 echo substr( trim( $tabs_html ), 0, - 6 );
25325
25326 return true;
25327 }
25328
25329 /**
25330 * Add the tabs closing HTML after the setting's page content.
25331 *
25332 * @author Vova Feldman (@svovaf)
25333 * @since 1.2.2.7
25334 *
25335 * @return bool If tabs closing HTML was included.
25336 */
25337 function _add_tabs_after_content() {
25338 $this->_logger->entrance();
25339
25340 if ( ! $this->should_page_include_tabs() ) {
25341 return false;
25342 }
25343
25344 echo '</div>';
25345
25346 return true;
25347 }
25348
25349 #endregion
25350
25351 /**
25352 * Add in-page JavaScript to inject the Freemius tabs into
25353 * the module's setting tabs section.
25354 *
25355 * @author Vova Feldman (@svovaf)
25356 * @since 1.2.2.7
25357 */
25358 function _add_freemius_tabs() {
25359 $this->_logger->entrance();
25360
25361 if ( ! $this->should_page_include_tabs() ) {
25362 return;
25363 }
25364
25365 $params = array( 'id' => $this->_module_id );
25366 fs_require_once_template( 'tabs.php', $params );
25367 }
25368
25369 #endregion
25370
25371 #--------------------------------------------------------------------------------
25372 #region Customizer Integration for Themes
25373 #--------------------------------------------------------------------------------
25374
25375 /**
25376 * @author Vova Feldman (@svovaf)
25377 * @since 1.2.2.7
25378 *
25379 * @param WP_Customize_Manager $customizer
25380 */
25381 function _customizer_register( $customizer ) {
25382 $this->_logger->entrance();
25383
25384 if ( $this->is_pricing_page_visible() ) {
25385 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
25386
25387 $customizer->add_section( 'freemius_upsell', array(
25388 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
25389 'priority' => 1,
25390 ) );
25391 $customizer->add_setting( 'freemius_upsell', array(
25392 'sanitize_callback' => 'esc_html',
25393 ) );
25394
25395 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
25396 'fs' => $this,
25397 'section' => 'freemius_upsell',
25398 'priority' => 100,
25399 ) ) );
25400 }
25401
25402 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
25403 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
25404
25405 // Main Documentation Link In Customizer Root.
25406 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
25407 'fs' => $this,
25408 'priority' => 1000,
25409 ) ) );
25410 }
25411 }
25412
25413 #endregion
25414
25415 /**
25416 * If the theme has a paid version, add some custom
25417 * styling to the theme's premium version (if exists)
25418 * to highlight that it's the premium version of the
25419 * same theme, making it easier for identification
25420 * after the user upgrades and upload it to the site.
25421 *
25422 * @author Vova Feldman (@svovaf)
25423 * @since 1.2.2.7
25424 */
25425 function _style_premium_theme() {
25426 $this->_logger->entrance();
25427
25428 if ( ! self::is_themes_page() ) {
25429 // Only include in the themes page.
25430 return;
25431 }
25432
25433 if ( ! $this->has_paid_plan() ) {
25434 // Only include if has any paid plans.
25435 return;
25436 }
25437
25438 $params = null;
25439 fs_require_once_template( '/js/jquery.content-change.php', $params );
25440
25441 $params = array(
25442 'slug' => $this->_slug,
25443 'id' => $this->_module_id,
25444 );
25445
25446 fs_require_template( '/js/style-premium-theme.php', $params );
25447 }
25448
25449 /**
25450 * This method will return the absolute URL of the module's local icon.
25451 *
25452 * When you are running your plugin or theme on a **localhost** environment, if the icon
25453 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
25454 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
25455 * If an icon is found, this method will automatically attempt to download the icon and store it
25456 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
25457 *
25458 * It's important to mention that this method is NOT phoning home since the developer will deploy
25459 * the product with the local icon in the assets folder. The download process just simplifies
25460 * the process for the developer.
25461 *
25462 * @author Vova Feldman (@svovaf)
25463 * @since 2.0.0
25464 *
25465 * @return string
25466 */
25467 function get_local_icon_url() {
25468 global $fs_active_plugins;
25469
25470 /**
25471 * @since 1.1.7.5
25472 */
25473 $local_path = $this->apply_filters( 'plugin_icon', false );
25474
25475 if ( is_string( $local_path ) ) {
25476 $icons = array( $local_path );
25477 } else {
25478 $img_dir = WP_FS__DIR_IMG;
25479
25480 // Locate the main assets folder.
25481 if ( ! empty( $fs_active_plugins->plugins ) ) {
25482 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
25483
25484 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
25485 if ( $data->plugin_path == $this->get_plugin_basename() ) {
25486 $img_dir = $plugin_or_theme_img_dir
25487 . '/'
25488 /**
25489 * The basename will be `themes` or the basename of a custom themes directory.
25490 *
25491 * @author Leo Fajardo (@leorw)
25492 * @since 2.2.3
25493 */
25494 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
25495 . '/assets/img';
25496
25497 break;
25498 }
25499 }
25500 }
25501
25502 // Try to locate the icon in the assets folder.
25503 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
25504
25505 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
25506 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
25507 $icons = array(
25508 fs_normalize_path( $img_dir . '/theme-icon.png' )
25509 );
25510 } else {
25511 $icon_found = false;
25512 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
25513
25514 if ( ! function_exists( 'get_filesystem_method' ) ) {
25515 require_once ABSPATH . 'wp-admin/includes/file.php';
25516 }
25517
25518 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
25519
25520 /**
25521 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
25522 *
25523 * This code will only be executed once during the testing
25524 * of the plugin in a local environment. The plugin icon file WILL
25525 * already exist in the assets folder when the plugin is deployed to
25526 * the repository.
25527 */
25528 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
25529 // Fetch icon from Freemius.
25530 $icon = $this->fetch_remote_icon_url();
25531
25532 // Fetch icon from WordPress.org.
25533 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
25534 if ( ! function_exists( 'plugins_api' ) ) {
25535 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
25536 }
25537
25538 $plugin_information = plugins_api( 'plugin_information', array(
25539 'slug' => $this->_slug,
25540 'fields' => array(
25541 'sections' => false,
25542 'tags' => false,
25543 'icons' => true
25544 )
25545 ) );
25546
25547 if (
25548 ! is_wp_error( $plugin_information )
25549 && isset( $plugin_information->icons )
25550 && ! empty( $plugin_information->icons )
25551 ) {
25552 /**
25553 * Get the smallest icon.
25554 *
25555 * @author Leo Fajardo (@leorw)
25556 * @since 1.2.2
25557 */
25558 $icon = end( $plugin_information->icons );
25559 }
25560 }
25561
25562 if ( ! empty( $icon ) ) {
25563 if ( 0 !== strpos( $icon, 'http' ) ) {
25564 $icon = 'http:' . $icon;
25565 }
25566
25567 /**
25568 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
25569 *
25570 * @author Leo Fajardo (@leorw)
25571 * @since 1.2.2
25572 */
25573 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
25574
25575 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
25576
25577 // Try to download the icon.
25578 $icon_found = fs_download_image( $icon, $local_path );
25579 }
25580 }
25581
25582 if ( ! $icon_found ) {
25583 // No icons found, fallback to default icon.
25584 if ( $have_write_permissions ) {
25585 // If have write permissions, copy default icon.
25586 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
25587 } else {
25588 // If doesn't have write permissions, use default icon path.
25589 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
25590 }
25591 }
25592
25593 $icons = array( $local_path );
25594 }
25595 }
25596 }
25597
25598 $icon_dir = dirname( $icons[0] );
25599
25600 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
25601 }
25602
25603 /**
25604 * Fetch module's extended info.
25605 *
25606 * @author Vova Feldman (@svovaf)
25607 * @since 2.0.0
25608 *
25609 * @return object|mixed
25610 */
25611 private function fetch_module_info() {
25612 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
25613 }
25614
25615 /**
25616 * Fetch module's remote icon URL.
25617 *
25618 * @author Vova Feldman (@svovaf)
25619 * @since 2.0.0
25620 *
25621 * @return string
25622 */
25623 function fetch_remote_icon_url() {
25624 $info = $this->fetch_module_info();
25625
25626 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
25627 $info->icon :
25628 '';
25629 }
25630
25631 #--------------------------------------------------------------------------------
25632 #region GDPR
25633 #--------------------------------------------------------------------------------
25634
25635 /**
25636 * @author Leo Fajardo (@leorw)
25637 * @since 2.1.0
25638 *
25639 * @param array $user_plugins
25640 *
25641 * @return string
25642 */
25643 private function get_gdpr_admin_notice_string( $user_plugins ) {
25644 $this->_logger->entrance();
25645
25646 $addons = self::get_all_addons();
25647
25648 foreach ( $user_plugins as $user_plugin ) {
25649 $has_addons = isset( $addons[ $user_plugin->id ] );
25650
25651 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
25652 if ( $this->_module_id == $user_plugin->id ) {
25653 $addons = $this->get_addons();
25654 $has_addons = ( ! empty( $addons ) );
25655 } else {
25656 $plugin_api = FS_Api::instance(
25657 $user_plugin->id,
25658 'plugin',
25659 $user_plugin->id,
25660 $user_plugin->public_key,
25661 ! $user_plugin->is_live,
25662 false,
25663 $this->get_sdk_version()
25664 );
25665
25666 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
25667
25668 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
25669 is_array( $addons_result->plugins ) &&
25670 ! empty( $addons_result->plugins )
25671 ) {
25672 $has_addons = true;
25673 }
25674 }
25675 }
25676
25677 $user_plugin->has_addons = $has_addons;
25678 }
25679
25680 $is_single_parent_product = ( 1 === count( $user_plugins ) );
25681
25682 $multiple_products_text = '';
25683
25684 if ( $is_single_parent_product ) {
25685 $single_parent_product = reset( $user_plugins );
25686
25687 $thank_you = sprintf(
25688 "<span data-plugin-id='%d'>%s</span>",
25689 $single_parent_product->id,
25690 sprintf(
25691 $single_parent_product->has_addons ?
25692 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
25693 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
25694 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
25695 )
25696 );
25697
25698 $already_opted_in = sprintf(
25699 $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' ),
25700 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
25701 );
25702 } else {
25703 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
25704 $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' );
25705
25706 $products_and_add_ons = '';
25707 foreach ( $user_plugins as $user_plugin ) {
25708 if ( ! empty( $products_and_add_ons ) ) {
25709 $products_and_add_ons .= ', ';
25710 }
25711
25712 if ( ! $user_plugin->has_addons ) {
25713 $products_and_add_ons .= sprintf(
25714 "<span data-plugin-id='%d'>%s</span>",
25715 $user_plugin->id,
25716 $user_plugin->title
25717 );
25718 } else {
25719 $products_and_add_ons .= sprintf(
25720 "<span data-plugin-id='%d'>%s</span>",
25721 $user_plugin->id,
25722 sprintf(
25723 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
25724 $user_plugin->title
25725 )
25726 );
25727 }
25728 }
25729
25730 $multiple_products_text = sprintf(
25731 "<small class='products'><strong>%s:</strong> %s</small>",
25732 $this->get_text_inline( 'Products', 'products' ),
25733 $products_and_add_ons
25734 );
25735 }
25736
25737 $actions = sprintf(
25738 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
25739 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
25740 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
25741 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
25742 sprintf(
25743 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
25744 '<span class="underlined">',
25745 '</span>'
25746 )
25747 );
25748
25749 return sprintf(
25750 '%s %s %s',
25751 $thank_you,
25752 $already_opted_in,
25753 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>' ) .
25754 '<br><br>' .
25755 '<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>' .
25756 $actions .
25757 ( $is_single_parent_product ? '' : $multiple_products_text )
25758 );
25759 }
25760
25761 /**
25762 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
25763 * plugins and themes they've opted in to.
25764 *
25765 * @author Leo Fajardo (@leorw)
25766 * @since 2.1.0
25767 *
25768 * @param string $user_email
25769 * @param string $license_key
25770 * @param array $plugin_ids
25771 * @param string|null $license_key
25772 *
25773 * @return array|false
25774 */
25775 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
25776 $request = array(
25777 'method' => 'POST',
25778 'body' => array(),
25779 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
25780 );
25781
25782 if ( is_string( $user_email ) ) {
25783 $request['body']['email'] = $user_email;
25784 } else {
25785 $request['body']['license_key'] = $license_key;
25786 }
25787
25788 $result = array();
25789
25790 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
25791 $total_plugin_ids = count( $plugin_ids );
25792
25793 $plugin_ids_count_per_request = 10;
25794 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
25795 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
25796
25797 $request['body']['plugin_ids'] = $plugin_ids_set;
25798
25799 $response = self::safe_remote_post(
25800 $url,
25801 $request,
25802 WP_FS__TIME_24_HOURS_IN_SEC,
25803 WP_FS__TIME_12_HOURS_IN_SEC
25804 );
25805
25806 if ( ! is_wp_error( $response ) ) {
25807 $decoded = is_string( $response['body'] ) ?
25808 json_decode( $response['body'] ) :
25809 null;
25810
25811 if (
25812 !is_object($decoded) ||
25813 !isset($decoded->success) ||
25814 true !== $decoded->success ||
25815 !isset( $decoded->data ) ||
25816 !is_array( $decoded->data )
25817 ) {
25818 return false;
25819 }
25820
25821 $result = array_merge( $result, $decoded->data );
25822 }
25823 }
25824
25825 return $result;
25826 }
25827
25828 /**
25829 * @author Leo Fajardo (@leorw)
25830 * @since 2.1.0
25831 */
25832 function _maybe_show_gdpr_admin_notice() {
25833 if ( ! $this->is_user_in_admin() ) {
25834 return;
25835 }
25836
25837 if ( ! $this->should_handle_gdpr_admin_notice() ) {
25838 return;
25839 }
25840
25841 if ( ! $this->is_user_admin() ) {
25842 return;
25843 }
25844
25845 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25846
25847 $lock = FS_User_Lock::instance();
25848
25849 /**
25850 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
25851 */
25852 if ( ! $lock->try_lock( 60 ) ) {
25853 return;
25854 }
25855
25856 /**
25857 * @var $current_wp_user WP_User
25858 */
25859 $current_wp_user = self::_get_current_wp_user();
25860
25861 /**
25862 * @var FS_User $current_fs_user
25863 */
25864 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
25865
25866 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
25867
25868 if ( ! is_object( $current_fs_user ) ) {
25869 // 10-year lock.
25870 $lock->lock( $ten_years_in_sec );
25871
25872 return;
25873 }
25874
25875 $gdpr = FS_GDPR_Manager::instance();
25876
25877 if ( $gdpr->is_opt_in_notice_shown() ) {
25878 // 30-day lock.
25879 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
25880
25881 return;
25882 }
25883
25884 if ( ! $gdpr->should_show_opt_in_notice() ) {
25885 // 10-year lock.
25886 $lock->lock( $ten_years_in_sec );
25887
25888 return;
25889 }
25890
25891 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
25892 $was_notice_shown_before = ( false !== $last_time_notice_shown );
25893
25894 if ( $was_notice_shown_before &&
25895 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
25896 ) {
25897 // If the notice was shown before, show it again after 30 days from the last time it was shown.
25898 return;
25899 }
25900
25901 /**
25902 * Find all plugin IDs that were installed by the current admin.
25903 */
25904 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
25905
25906 if ( empty( $plugin_ids_map )) {
25907 $lock->lock( $ten_years_in_sec );
25908
25909 return;
25910 }
25911
25912 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
25913 $current_fs_user->email,
25914 null,
25915 array_keys( $plugin_ids_map )
25916 );
25917
25918 if ( empty( $user_plugins ) ) {
25919 $lock->lock(
25920 is_array($user_plugins) ?
25921 $ten_years_in_sec :
25922 // Lock for 24-hours on errors.
25923 WP_FS__TIME_24_HOURS_IN_SEC
25924 );
25925
25926 return;
25927 }
25928
25929 $has_unset_marketing_optin = false;
25930
25931 foreach ( $user_plugins as $user_plugin ) {
25932 if ( true == $user_plugin->is_marketing_allowed ) {
25933 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
25934 }
25935
25936 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
25937 $has_unset_marketing_optin = true;
25938 }
25939 }
25940
25941 if ( empty( $plugin_ids_map ) ||
25942 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
25943 ) {
25944 $lock->lock( $ten_years_in_sec );
25945
25946 return;
25947 }
25948
25949 $modules = array_merge(
25950 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
25951 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
25952 );
25953
25954 foreach ( $modules as $module ) {
25955 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
25956 $plugin_ids_map[ $module->id ] = $module;
25957 }
25958 }
25959
25960 $plugin_title = null;
25961 if ( 1 === count( $plugin_ids_map ) ) {
25962 $module = reset( $plugin_ids_map );
25963 $plugin_title = $module->title;
25964 }
25965
25966 $gdpr->add_opt_in_sticky_notice(
25967 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
25968 $plugin_title
25969 );
25970
25971 $this->add_gdpr_optin_ajax_handler_and_style();
25972
25973 $gdpr->notice_was_just_shown();
25974
25975 // 30-day lock.
25976 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
25977 }
25978
25979 /**
25980 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
25981 * marketing.
25982 *
25983 * @author Leo Fajardo (@leorw)
25984 * @since 2.1.0
25985 */
25986 private function disable_opt_in_notice_and_lock_user() {
25987 FS_GDPR_Manager::instance()->disable_opt_in_notice();
25988
25989 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25990
25991 // 10-year lock.
25992 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
25993 }
25994
25995 /**
25996 * @author Leo Fajardo (@leorw)
25997 * @since 2.5.4
25998 */
25999 static function _add_api_connectivity_notice_handler_js() {
26000 fs_require_once_template( 'api-connectivity-message-js.php' );
26001 }
26002
26003 /**
26004 * @author Leo Fajardo (@leorw)
26005 * @since 2.1.0
26006 */
26007 function _add_gdpr_optin_js() {
26008 $vars = array( 'id' => $this->_module_id );
26009
26010 fs_require_once_template( 'gdpr-optin-js.php', $vars );
26011 }
26012
26013 /**
26014 * @author Leo Fajardo (@leorw)
26015 * @since 2.1.0
26016 */
26017 function enqueue_gdpr_optin_notice_style() {
26018 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
26019 }
26020
26021 /**
26022 * @author Leo Fajardo (@leorw)
26023 * @since 2.1.0
26024 */
26025 function _maybe_add_gdpr_optin_ajax_handler() {
26026 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
26027
26028 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
26029 $this->add_gdpr_optin_ajax_handler_and_style();
26030 }
26031 }
26032
26033 /**
26034 * @author Leo Fajardo (@leorw)
26035 * @since 2.1.0
26036 */
26037 function _fetch_is_marketing_required_flag_value_ajax_action() {
26038 $this->_logger->entrance();
26039
26040 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
26041
26042 $license_key = fs_request_get_raw( 'license_key' );
26043
26044 if ( empty($license_key) ) {
26045 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
26046 }
26047
26048 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26049 null,
26050 $license_key,
26051 array( $this->_module_id )
26052 );
26053
26054 if ( ! is_array( $user_plugins ) ||
26055 empty($user_plugins) ||
26056 !isset($user_plugins[0]->plugin_id) ||
26057 $user_plugins[0]->plugin_id != $this->_module_id
26058 ) {
26059 /**
26060 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
26061 *
26062 * @author Vova Feldman (@svovaf)
26063 */
26064 self::shoot_ajax_success( array(
26065 'is_marketing_allowed' => null,
26066 'license_owner_id' => null
26067 ) );
26068 }
26069
26070 self::shoot_ajax_success( array(
26071 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
26072 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
26073 ) );
26074 }
26075
26076 /**
26077 * @author Leo Fajardo (@leorw)
26078 * @since 2.3.2
26079 *
26080 * @param number[] $install_ids
26081 *
26082 * @return array {
26083 * An array of objects containing the installs' licenses owners data.
26084 *
26085 * @property number $id User ID.
26086 * @property string $email User email (can be masked email).
26087 * }
26088 */
26089 private function fetch_installs_licenses_owners_data( $install_ids ) {
26090 $this->_logger->entrance();
26091
26092 $response = $this->get_api_user_scope()->get(
26093 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
26094 );
26095
26096 $license_owners = array();
26097
26098 if ( $this->is_api_result_object( $response, 'owners' ) ) {
26099 $license_owners = $response->owners;
26100 }
26101
26102 return $license_owners;
26103 }
26104
26105 /**
26106 * @author Leo Fajardo (@leorw)
26107 * @since 2.1.0
26108 */
26109 private function add_gdpr_optin_ajax_handler_and_style() {
26110 // Add GDPR action AJAX callback.
26111 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
26112
26113 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
26114 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
26115 }
26116
26117 /**
26118 * @author Leo Fajardo (@leorw)
26119 * @since 2.1.0
26120 */
26121 function _gdpr_optin_ajax_action() {
26122 $this->_logger->entrance();
26123
26124 $this->check_ajax_referer( 'gdpr_optin_action' );
26125
26126 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
26127 self::shoot_ajax_failure();
26128 }
26129
26130 $current_wp_user = self::_get_current_wp_user();
26131
26132 $plugin_ids = fs_request_get( 'plugin_ids', array() );
26133 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
26134 self::shoot_ajax_failure();
26135 }
26136
26137 $modules = array_merge(
26138 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26139 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26140 );
26141
26142 foreach ( $modules as $key => $module ) {
26143 if ( ! in_array( $module->id, $plugin_ids ) ) {
26144 unset( $modules[ $key ] );
26145 }
26146 }
26147
26148 if ( empty( $modules ) ) {
26149 self::shoot_ajax_failure();
26150 }
26151
26152 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
26153
26154 foreach ( $modules as $module ) {
26155 $user_api->call( "?plugin_id={$module->id}", 'put', array(
26156 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
26157 ) );
26158 }
26159
26160 FS_GDPR_Manager::instance()->remove_opt_in_notice();
26161
26162 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26163
26164 // 10-year lock.
26165 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26166
26167 self::shoot_ajax_success();
26168 }
26169
26170 /**
26171 * 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.
26172 *
26173 * @author Vova Feldman (@svovaf)
26174 * @since 2.1.0
26175 *
26176 * @return bool
26177 */
26178 private function should_handle_gdpr_admin_notice() {
26179 return $this->apply_filters(
26180 'handle_gdpr_admin_notice',
26181 // Default to false.
26182 false
26183 );
26184 }
26185
26186 #endregion
26187
26188 #----------------------------------------------------------------------------------
26189 #region Marketing
26190 #----------------------------------------------------------------------------------
26191
26192 /**
26193 * Check if current user purchased any other plugins before.
26194 *
26195 * @author Vova Feldman (@svovaf)
26196 * @since 1.0.9
26197 *
26198 * @return bool
26199 */
26200 function has_purchased_before() {
26201 // TODO: Implement has_purchased_before() method.
26202 throw new Exception( 'not implemented' );
26203 }
26204
26205 /**
26206 * Check if current user classified as an agency.
26207 *
26208 * @author Vova Feldman (@svovaf)
26209 * @since 1.0.9
26210 *
26211 * @return bool
26212 */
26213 function is_agency() {
26214 // TODO: Implement is_agency() method.
26215 throw new Exception( 'not implemented' );
26216 }
26217
26218 /**
26219 * Check if current user classified as a developer.
26220 *
26221 * @author Vova Feldman (@svovaf)
26222 * @since 1.0.9
26223 *
26224 * @return bool
26225 */
26226 function is_developer() {
26227 // TODO: Implement is_developer() method.
26228 throw new Exception( 'not implemented' );
26229 }
26230
26231 /**
26232 * Check if current user classified as a business.
26233 *
26234 * @author Vova Feldman (@svovaf)
26235 * @since 1.0.9
26236 *
26237 * @return bool
26238 */
26239 function is_business() {
26240 // TODO: Implement is_business() method.
26241 throw new Exception( 'not implemented' );
26242 }
26243
26244 #endregion
26245
26246 #----------------------------------------------------------------------------------
26247 #region Helper
26248 #----------------------------------------------------------------------------------
26249
26250 /**
26251 * If running with a secret key, assume it's the developer and show pending plans as well.
26252 *
26253 * @author Vova Feldman (@svovaf)
26254 * @since 2.1.2
26255 *
26256 * @param string $path
26257 *
26258 * @return string
26259 */
26260 function add_show_pending( $path ) {
26261 if ( ! $this->has_secret_key() ) {
26262 return $path;
26263 }
26264
26265 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
26266 }
26267
26268 #endregion
26269 }
26270